@@ -38,6 +38,11 @@ class DataSyncController {
38
38
// We store messages here that cannot be send because the connection is not available
39
39
this . outbox = [ ] ;
40
40
this . reconnectTimeout = null ;
41
+
42
+ this . dataSubscriptions = [ ] ;
43
+
44
+ // Ids of records that are visible in the UI already, but yet still have to be confirmed by the server
45
+ this . optimisticCreatedPendingRecordIds = [ ] ;
41
46
}
42
47
43
48
async startConnection ( ) {
@@ -205,15 +210,16 @@ class DataSubscription {
205
210
this . onDataSyncReconnect = this . onDataSyncReconnect . bind ( this ) ;
206
211
this . onMessage = this . onMessage . bind ( this ) ;
207
212
208
-
209
213
// When a new record is inserted, do we put it at the end or at the beginning?
210
214
this . newRecordBehaviour = this . detectNewRecordBehaviour ( ) ;
215
+
216
+ this . optimisticCreatedPendingRecordIds = [ ] ;
211
217
}
212
218
213
219
detectNewRecordBehaviour ( ) {
214
220
// If the query is ordered by the createdAt column, and the latest is at the top
215
221
// we want to prepend new record
216
- const isOrderByCreatedAtDesc = this . query . orderByClause . length > 0 && this . query . orderByClause [ 0 ] . orderByColumn === 'createdAt' && this . query . orderByClause [ 0 ] . orderByDirection === 'Desc ' ;
222
+ const isOrderByCreatedAtDesc = this . query . orderByClause . length > 0 && this . query . orderByClause [ 0 ] . orderByColumn === 'createdAt' && this . query . orderByClause [ 0 ] . orderByColumn === 'createdAt ' ;
217
223
218
224
if ( isOrderByCreatedAtDesc ) {
219
225
return PREPEND_NEW_RECORD ;
@@ -236,6 +242,7 @@ class DataSubscription {
236
242
dataSyncController . addEventListener ( 'message' , this . onMessage ) ;
237
243
dataSyncController . addEventListener ( 'close' , this . onDataSyncClosed ) ;
238
244
dataSyncController . addEventListener ( 'reconnect' , this . onDataSyncReconnect ) ;
245
+ dataSyncController . dataSubscriptions . push ( this ) ;
239
246
}
240
247
241
248
this . isConnected = true ;
@@ -289,6 +296,7 @@ class DataSubscription {
289
296
dataSyncController . removeEventListener ( 'message' , this . onMessage ) ;
290
297
dataSyncController . removeEventListener ( 'close' , this . onDataSyncClosed ) ;
291
298
dataSyncController . removeEventListener ( 'reconnect' , this . onDataSyncReconnect ) ;
299
+ dataSyncController . dataSubscriptions . splice ( dataSyncController . dataSubscriptions . indexOf ( this ) , 1 ) ;
292
300
293
301
this . isClosed = true ;
294
302
this . isConnected = false ;
@@ -319,10 +327,27 @@ class DataSubscription {
319
327
320
328
onCreate ( newRecord ) {
321
329
const shouldAppend = this . newRecordBehaviour === APPEND_NEW_RECORD ;
322
- this . records = shouldAppend ? [ ...this . records , newRecord ] : [ newRecord , ...this . records ] ;
330
+
331
+ const isOptimisticallyCreatedAlready = this . optimisticCreatedPendingRecordIds . indexOf ( newRecord . id ) !== - 1 ;
332
+ if ( isOptimisticallyCreatedAlready ) {
333
+ this . onUpdate ( newRecord . id , newRecord ) ;
334
+ this . optimisticCreatedPendingRecordIds . slice ( this . optimisticCreatedPendingRecordIds . indexOf ( newRecord . id ) , 1 ) ;
335
+ } else {
336
+ this . records = shouldAppend ? [ ...this . records , newRecord ] : [ newRecord , ...this . records ] ;
337
+ }
338
+
323
339
this . updateSubscribers ( ) ;
324
340
}
325
341
342
+ onCreateOptimistic ( newRecord ) {
343
+ if ( ! ( 'id' in newRecord ) ) {
344
+ throw new Error ( 'Requires the record to have an id' ) ;
345
+ }
346
+
347
+ this . onCreate ( newRecord ) ;
348
+ this . optimisticCreatedPendingRecordIds . push ( newRecord . id ) ;
349
+ }
350
+
326
351
onDelete ( id ) {
327
352
this . records = this . records . filter ( record => record . id !== id ) ;
328
353
this . updateSubscribers ( ) ;
@@ -391,9 +416,14 @@ export async function createRecord(table, record, options = {}) {
391
416
const request = { tag : 'CreateRecordMessage' , table, record, transactionId } ;
392
417
393
418
try {
419
+ createOptimisticRecord ( table , record ) ;
420
+ await waitPendingChanges ( table , record ) ;
421
+
394
422
const response = await DataSyncController . getInstance ( ) . sendMessage ( request ) ;
395
423
return response . record ;
396
424
} catch ( e ) {
425
+ undoCreateOptimisticRecord ( table , record ) ;
426
+
397
427
// We rethrow the error here for improved callstacks
398
428
// Without this, the error location in the callstack would show the error to be caused
399
429
// somewhere in DataSyncController. But the user is not really using DataSyncController
@@ -459,11 +489,13 @@ export async function deleteRecord(table, id, options = {}) {
459
489
const transactionId = 'transactionId' in options ? options . transactionId : null ;
460
490
const request = { tag : 'DeleteRecordMessage' , table, id, transactionId } ;
461
491
492
+ let undoOptimisticDeleteRecord = deleteRecordOptimistic ( table , id ) ; ;
462
493
try {
463
494
const response = await DataSyncController . getInstance ( ) . sendMessage ( request ) ;
464
495
465
496
return ;
466
497
} catch ( e ) {
498
+ undoOptimisticDeleteRecord ( ) ;
467
499
throw new Error ( e . message ) ;
468
500
}
469
501
}
@@ -508,4 +540,99 @@ export async function createRecords(table, records, options = {}) {
508
540
}
509
541
}
510
542
543
+ function createOptimisticRecord ( table , record ) {
544
+ // Ensure that the record has an ID
545
+ if ( record . id === null || record . id === undefined ) {
546
+ record . id = crypto . randomUUID ( ) ;
547
+ }
548
+ if ( record . createdAt === null || record . createdAt === undefined ) {
549
+ record . createdAt = new Date ( ) ;
550
+ }
551
+
552
+ const dataSyncController = DataSyncController . getInstance ( ) ;
553
+ for ( const dataSubscription of dataSyncController . dataSubscriptions ) {
554
+ if ( dataSubscription . query . table !== table ) {
555
+ continue ;
556
+ }
557
+
558
+ // TODO: we need to check that this data subscription's WHERE condition matches the new record
559
+ dataSubscription . onCreateOptimistic ( record ) ;
560
+ }
561
+
562
+ dataSyncController . optimisticCreatedPendingRecordIds . push ( record . id ) ;
563
+ }
564
+
565
+ function undoCreateOptimisticRecord ( table , record ) {
566
+ const dataSyncController = DataSyncController . getInstance ( ) ;
567
+ for ( const dataSubscription of dataSyncController . dataSubscriptions ) {
568
+ if ( dataSubscription . query . table !== table ) {
569
+ continue ;
570
+ }
571
+
572
+ dataSubscription . onDelete ( record . id ) ;
573
+ }
574
+
575
+ dataSyncController . optimisticCreatedPendingRecordIds . slice ( dataSyncController . optimisticCreatedPendingRecordIds . indexOf ( record . id ) , 1 ) ;
576
+ }
577
+
578
+ function deleteRecordOptimistic ( table , id ) {
579
+ const dataSyncController = DataSyncController . getInstance ( ) ;
580
+ const undoOperations = [ ] ;
581
+ for ( const dataSubscription of dataSyncController . dataSubscriptions ) {
582
+ if ( dataSubscription . query . table !== table ) {
583
+ continue ;
584
+ }
585
+
586
+ const deletedRecord = dataSubscription . records . find ( record => record . id === id ) ;
587
+ if ( deletedRecord ) {
588
+ dataSubscription . onDelete ( id ) ;
589
+ undoOperations . push ( ( ) => dataSubscription . onCreate ( deleteRecord ) ) ;
590
+ }
591
+ }
592
+
593
+ return ( ) => {
594
+ for ( const undoOperation of undoOperations ) {
595
+ undoOperation ( ) ;
596
+ }
597
+ }
598
+ }
599
+
600
+
601
+ function doesRecordReferencePendingOptimisticRecord ( record ) {
602
+ const dataSyncController = DataSyncController . getInstance ( ) ;
603
+ const optimisticIds = dataSyncController . optimisticCreatedPendingRecordIds ;
604
+
605
+ for ( const attribute in record ) {
606
+ if ( attribute === 'id' ) {
607
+ continue ; // The current record's id is always optimistic
608
+ }
609
+ if ( optimisticIds . indexOf ( record [ attribute ] ) !== - 1 ) {
610
+ return true ;
611
+ }
612
+ }
613
+
614
+ return false ;
615
+ }
616
+
617
+ async function waitPendingChanges ( table , record ) {
618
+ if ( doesRecordReferencePendingOptimisticRecord ( record ) ) {
619
+ return waitForMessageMatching ( message => message . tag === 'DidCreateRecord' && message . record && message . record . id === record . id ) ;
620
+ }
621
+ }
622
+
623
+ function waitForMessageMatching ( condition ) {
624
+ const dataSyncController = DataSyncController . getInstance ( ) ;
625
+
626
+ return new Promise ( ( resolve , reject ) => {
627
+ const callback = ( ) => {
628
+ if ( condition ( ) ) {
629
+ dataSyncController . removeEventListener ( 'message' , callback ) ;
630
+ resolve ( ) ;
631
+ }
632
+ }
633
+
634
+ dataSyncController . addEventListener ( 'message' , callback ) ;
635
+ } ) ;
636
+ }
637
+
511
638
export { DataSyncController , DataSubscription , initIHPBackend } ;
0 commit comments