@aws-amplify/datastore 3.14.1-unstable.2 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/aws-amplify-datastore.js +2798 -1458
  3. package/dist/aws-amplify-datastore.js.map +1 -1
  4. package/dist/aws-amplify-datastore.min.js +10 -10
  5. package/dist/aws-amplify-datastore.min.js.map +1 -1
  6. package/lib/authModeStrategies/multiAuthStrategy.js +11 -0
  7. package/lib/authModeStrategies/multiAuthStrategy.js.map +1 -1
  8. package/lib/datastore/datastore.js +524 -323
  9. package/lib/datastore/datastore.js.map +1 -1
  10. package/lib/storage/adapter/IndexedDBAdapter.js +76 -25
  11. package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
  12. package/lib/storage/storage.js +2 -2
  13. package/lib/storage/storage.js.map +1 -1
  14. package/lib/sync/datastoreConnectivity.js +45 -0
  15. package/lib/sync/datastoreConnectivity.js.map +1 -1
  16. package/lib/sync/index.js +518 -395
  17. package/lib/sync/index.js.map +1 -1
  18. package/lib/sync/merger.js +6 -0
  19. package/lib/sync/merger.js.map +1 -1
  20. package/lib/sync/outbox.js +66 -62
  21. package/lib/sync/outbox.js.map +1 -1
  22. package/lib/sync/processors/mutation.js +207 -165
  23. package/lib/sync/processors/mutation.js.map +1 -1
  24. package/lib/sync/processors/subscription.js +210 -175
  25. package/lib/sync/processors/subscription.js.map +1 -1
  26. package/lib/sync/processors/sync.js +95 -72
  27. package/lib/sync/processors/sync.js.map +1 -1
  28. package/lib/sync/utils.js +1 -3
  29. package/lib/sync/utils.js.map +1 -1
  30. package/lib/util.js +89 -0
  31. package/lib/util.js.map +1 -1
  32. package/lib-esm/authModeStrategies/multiAuthStrategy.d.ts +11 -0
  33. package/lib-esm/authModeStrategies/multiAuthStrategy.js +11 -0
  34. package/lib-esm/authModeStrategies/multiAuthStrategy.js.map +1 -1
  35. package/lib-esm/datastore/datastore.d.ts +95 -2
  36. package/lib-esm/datastore/datastore.js +524 -323
  37. package/lib-esm/datastore/datastore.js.map +1 -1
  38. package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +21 -0
  39. package/lib-esm/storage/adapter/IndexedDBAdapter.js +77 -26
  40. package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
  41. package/lib-esm/storage/storage.js +2 -2
  42. package/lib-esm/storage/storage.js.map +1 -1
  43. package/lib-esm/sync/datastoreConnectivity.d.ts +1 -0
  44. package/lib-esm/sync/datastoreConnectivity.js +45 -0
  45. package/lib-esm/sync/datastoreConnectivity.js.map +1 -1
  46. package/lib-esm/sync/index.d.ts +9 -1
  47. package/lib-esm/sync/index.js +519 -396
  48. package/lib-esm/sync/index.js.map +1 -1
  49. package/lib-esm/sync/merger.d.ts +6 -0
  50. package/lib-esm/sync/merger.js +6 -0
  51. package/lib-esm/sync/merger.js.map +1 -1
  52. package/lib-esm/sync/outbox.js +66 -62
  53. package/lib-esm/sync/outbox.js.map +1 -1
  54. package/lib-esm/sync/processors/mutation.d.ts +2 -0
  55. package/lib-esm/sync/processors/mutation.js +208 -166
  56. package/lib-esm/sync/processors/mutation.js.map +1 -1
  57. package/lib-esm/sync/processors/subscription.d.ts +2 -0
  58. package/lib-esm/sync/processors/subscription.js +211 -176
  59. package/lib-esm/sync/processors/subscription.js.map +1 -1
  60. package/lib-esm/sync/processors/sync.d.ts +2 -0
  61. package/lib-esm/sync/processors/sync.js +96 -73
  62. package/lib-esm/sync/processors/sync.js.map +1 -1
  63. package/lib-esm/sync/utils.js +1 -3
  64. package/lib-esm/sync/utils.js.map +1 -1
  65. package/lib-esm/util.d.ts +11 -0
  66. package/lib-esm/util.js +89 -0
  67. package/lib-esm/util.js.map +1 -1
  68. package/package.json +7 -7
  69. package/src/authModeStrategies/multiAuthStrategy.ts +11 -0
  70. package/src/datastore/datastore.ts +572 -366
  71. package/src/storage/adapter/IndexedDBAdapter.ts +50 -9
  72. package/src/storage/storage.ts +2 -2
  73. package/src/sync/datastoreConnectivity.ts +6 -0
  74. package/src/sync/index.ts +492 -400
  75. package/src/sync/merger.ts +6 -0
  76. package/src/sync/outbox.ts +1 -1
  77. package/src/sync/processors/mutation.ts +139 -104
  78. package/src/sync/processors/subscription.ts +287 -250
  79. package/src/sync/processors/sync.ts +88 -60
  80. package/src/sync/utils.ts +1 -3
  81. package/src/util.ts +92 -2
  82. package/lib/authModeStrategies/defaultAuthStrategy.d.ts +0 -2
  83. package/lib/authModeStrategies/index.d.ts +0 -2
  84. package/lib/authModeStrategies/multiAuthStrategy.d.ts +0 -2
  85. package/lib/datastore/datastore.d.ts +0 -63
  86. package/lib/index.d.ts +0 -15
  87. package/lib/predicates/index.d.ts +0 -16
  88. package/lib/predicates/sort.d.ts +0 -8
  89. package/lib/ssr/index.d.ts +0 -3
  90. package/lib/storage/adapter/AsyncStorageAdapter.d.ts +0 -41
  91. package/lib/storage/adapter/AsyncStorageDatabase.d.ts +0 -39
  92. package/lib/storage/adapter/InMemoryStore.d.ts +0 -11
  93. package/lib/storage/adapter/InMemoryStore.native.d.ts +0 -1
  94. package/lib/storage/adapter/IndexedDBAdapter.d.ts +0 -38
  95. package/lib/storage/adapter/getDefaultAdapter/index.d.ts +0 -3
  96. package/lib/storage/adapter/getDefaultAdapter/index.native.d.ts +0 -3
  97. package/lib/storage/adapter/index.d.ts +0 -9
  98. package/lib/storage/storage.d.ts +0 -49
  99. package/lib/sync/datastoreConnectivity.d.ts +0 -15
  100. package/lib/sync/datastoreReachability/index.d.ts +0 -3
  101. package/lib/sync/datastoreReachability/index.native.d.ts +0 -3
  102. package/lib/sync/index.d.ts +0 -81
  103. package/lib/sync/merger.d.ts +0 -11
  104. package/lib/sync/outbox.d.ts +0 -27
  105. package/lib/sync/processors/errorMaps.d.ts +0 -17
  106. package/lib/sync/processors/mutation.d.ts +0 -56
  107. package/lib/sync/processors/subscription.d.ts +0 -31
  108. package/lib/sync/processors/sync.d.ts +0 -26
  109. package/lib/sync/utils.d.ts +0 -42
  110. package/lib/types.d.ts +0 -501
  111. package/lib/util.d.ts +0 -145
package/src/sync/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { browserOrNode, ConsoleLogger as Logger } from '@aws-amplify/core';
1
+ import {
2
+ browserOrNode,
3
+ ConsoleLogger as Logger,
4
+ BackgroundProcessManager,
5
+ } from '@aws-amplify/core';
2
6
  import { CONTROL_MSG as PUBSUB_CONTROL_MSG } from '@aws-amplify/pubsub';
3
7
  import Observable, { ZenObservable } from 'zen-observable-ts';
4
8
  import { ModelInstanceCreator } from '../datastore/datastore';
@@ -113,6 +117,8 @@ export class SyncEngine {
113
117
  boolean
114
118
  > = new WeakMap();
115
119
 
120
+ private runningProcesses: BackgroundProcessManager;
121
+
116
122
  public getModelSyncedStatus(
117
123
  modelConstructor: PersistentModelConstructor<any>
118
124
  ): boolean {
@@ -131,8 +137,11 @@ export class SyncEngine {
131
137
  private readonly syncPredicates: WeakMap<SchemaModel, ModelPredicate<any>>,
132
138
  private readonly amplifyConfig: Record<string, any> = {},
133
139
  private readonly authModeStrategy: AuthModeStrategy,
134
- private readonly amplifyContext: AmplifyContext
140
+ private readonly amplifyContext: AmplifyContext,
141
+ private readonly connectivityMonitor?: DataStoreConnectivity
135
142
  ) {
143
+ this.runningProcesses = new BackgroundProcessManager();
144
+
136
145
  const MutationEvent = this.modelClasses[
137
146
  'MutationEvent'
138
147
  ] as PersistentModelConstructor<MutationEvent>;
@@ -178,7 +187,8 @@ export class SyncEngine {
178
187
  this.amplifyContext
179
188
  );
180
189
 
181
- this.datastoreConnectivity = new DataStoreConnectivity();
190
+ this.datastoreConnectivity =
191
+ this.connectivityMonitor || new DataStoreConnectivity();
182
192
  }
183
193
 
184
194
  start(params: StartParams) {
@@ -187,7 +197,7 @@ export class SyncEngine {
187
197
 
188
198
  let subscriptions: ZenObservable.Subscription[] = [];
189
199
 
190
- (async () => {
200
+ this.runningProcesses.add(async () => {
191
201
  try {
192
202
  await this.setupModels(params);
193
203
  } catch (err) {
@@ -195,179 +205,204 @@ export class SyncEngine {
195
205
  return;
196
206
  }
197
207
 
198
- const startPromise = new Promise(resolve => {
199
- this.datastoreConnectivity.status().subscribe(async ({ online }) => {
200
- // From offline to online
201
- if (online && !this.online) {
202
- this.online = online;
203
-
204
- observer.next({
205
- type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
206
- data: {
207
- active: this.online,
208
- },
209
- });
210
-
211
- let ctlSubsObservable: Observable<CONTROL_MSG>;
212
- let dataSubsObservable: Observable<
213
- [TransformerMutationType, SchemaModel, PersistentModel]
214
- >;
215
-
216
- if (isNode) {
217
- logger.warn(
218
- 'Realtime disabled when in a server-side environment'
219
- );
220
- } else {
221
- //#region GraphQL Subscriptions
222
- [
223
- // const ctlObservable: Observable<CONTROL_MSG>
224
- ctlSubsObservable,
225
- // const dataObservable: Observable<[TransformerMutationType, SchemaModel, Readonly<{
226
- // id: string;
227
- // } & Record<string, any>>]>
228
- dataSubsObservable,
229
- ] = this.subscriptionsProcessor.start();
230
-
231
- try {
232
- await new Promise((resolve, reject) => {
233
- const ctlSubsSubscription = ctlSubsObservable.subscribe({
234
- next: msg => {
235
- if (msg === CONTROL_MSG.CONNECTED) {
236
- resolve();
237
- }
238
- },
239
- error: err => {
240
- reject(err);
241
- const handleDisconnect = this.disconnectionHandler();
242
- handleDisconnect(err);
243
- },
244
- });
245
-
246
- subscriptions.push(ctlSubsSubscription);
208
+ // this is awaited at the bottom. so, we don't need to register
209
+ // this explicitly with the context. it's already contained.
210
+ const startPromise = new Promise((doneStarting, failedStarting) => {
211
+ this.datastoreConnectivity.status().subscribe(
212
+ async ({ online }) =>
213
+ this.runningProcesses.isOpen &&
214
+ this.runningProcesses.add(async onTerminate => {
215
+ // From offline to online
216
+ if (online && !this.online) {
217
+ this.online = online;
218
+
219
+ observer.next({
220
+ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
221
+ data: {
222
+ active: this.online,
223
+ },
247
224
  });
248
- } catch (err) {
249
- observer.error(err);
250
- return;
251
- }
252
-
253
- logger.log('Realtime ready');
254
-
255
- observer.next({
256
- type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED,
257
- });
258
-
259
- //#endregion
260
- }
261
-
262
- //#region Base & Sync queries
263
- try {
264
- await new Promise((resolve, reject) => {
265
- const syncQuerySubscription =
266
- this.syncQueriesObservable().subscribe({
267
- next: message => {
268
- const { type } = message;
269
-
270
- if (
271
- type === ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
272
- ) {
273
- resolve();
274
- }
275
-
276
- observer.next(message);
277
- },
278
- complete: () => {
279
- resolve();
280
- },
281
- error: error => {
282
- reject(error);
283
- },
284
- });
285
-
286
- if (syncQuerySubscription) {
287
- subscriptions.push(syncQuerySubscription);
288
- }
289
- });
290
- } catch (error) {
291
- observer.error(error);
292
- return;
293
- }
294
- //#endregion
295
225
 
296
- //#region process mutations
297
- subscriptions.push(
298
- this.mutationsProcessor
299
- .start()
300
- .subscribe(({ modelDefinition, model: item, hasMore }) => {
301
- const modelConstructor = this.userModelClasses[
302
- modelDefinition.name
303
- ] as PersistentModelConstructor<any>;
304
-
305
- const model = this.modelInstanceCreator(
306
- modelConstructor,
307
- item
308
- );
226
+ let ctlSubsObservable: Observable<CONTROL_MSG>;
227
+ let dataSubsObservable: Observable<
228
+ [TransformerMutationType, SchemaModel, PersistentModel]
229
+ >;
309
230
 
310
- this.storage.runExclusive(storage =>
311
- this.modelMerger.merge(storage, model, modelDefinition)
231
+ // NOTE: need a way to override this conditional for testing.
232
+ if (isNode) {
233
+ logger.warn(
234
+ 'Realtime disabled when in a server-side environment'
312
235
  );
236
+ } else {
237
+ //#region GraphQL Subscriptions
238
+ [
239
+ // const ctlObservable: Observable<CONTROL_MSG>
240
+ ctlSubsObservable,
241
+ // const dataObservable: Observable<[TransformerMutationType, SchemaModel, Readonly<{
242
+ // id: string;
243
+ // } & Record<string, any>>]>
244
+ dataSubsObservable,
245
+ ] = this.subscriptionsProcessor.start();
246
+
247
+ try {
248
+ await new Promise((resolve, reject) => {
249
+ onTerminate.then(reject);
250
+ const ctlSubsSubscription = ctlSubsObservable.subscribe(
251
+ {
252
+ next: msg => {
253
+ if (msg === CONTROL_MSG.CONNECTED) {
254
+ resolve();
255
+ }
256
+ },
257
+ error: err => {
258
+ reject(err);
259
+ const handleDisconnect =
260
+ this.disconnectionHandler();
261
+ handleDisconnect(err);
262
+ },
263
+ }
264
+ );
265
+
266
+ subscriptions.push(ctlSubsSubscription);
267
+ });
268
+ } catch (err) {
269
+ observer.error(err);
270
+ failedStarting();
271
+ return;
272
+ }
313
273
 
314
- observer.next({
315
- type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED,
316
- data: {
317
- model: modelConstructor,
318
- element: model,
319
- },
320
- });
274
+ logger.log('Realtime ready');
321
275
 
322
276
  observer.next({
323
- type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
324
- data: {
325
- isEmpty: !hasMore,
326
- },
277
+ type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED,
327
278
  });
328
- })
329
- );
330
- //#endregion
331
-
332
- //#region Merge subscriptions buffer
333
- // TODO: extract to function
334
- if (!isNode) {
335
- subscriptions.push(
336
- dataSubsObservable.subscribe(
337
- ([_transformerMutationType, modelDefinition, item]) => {
338
- const modelConstructor = this.userModelClasses[
339
- modelDefinition.name
340
- ] as PersistentModelConstructor<any>;
341
-
342
- const model = this.modelInstanceCreator(
343
- modelConstructor,
344
- item
345
- );
346
279
 
347
- this.storage.runExclusive(storage =>
348
- this.modelMerger.merge(storage, model, modelDefinition)
349
- );
350
- }
351
- )
352
- );
353
- }
354
- //#endregion
355
- } else if (!online) {
356
- this.online = online;
280
+ //#endregion
281
+ }
357
282
 
358
- observer.next({
359
- type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
360
- data: {
361
- active: this.online,
362
- },
363
- });
283
+ //#region Base & Sync queries
284
+ try {
285
+ await new Promise((resolve, reject) => {
286
+ const syncQuerySubscription =
287
+ this.syncQueriesObservable().subscribe({
288
+ next: message => {
289
+ const { type } = message;
290
+
291
+ if (
292
+ type ===
293
+ ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
294
+ ) {
295
+ resolve();
296
+ }
297
+
298
+ observer.next(message);
299
+ },
300
+ complete: () => {
301
+ resolve();
302
+ },
303
+ error: error => {
304
+ reject(error);
305
+ },
306
+ });
307
+
308
+ if (syncQuerySubscription) {
309
+ subscriptions.push(syncQuerySubscription);
310
+ }
311
+ });
312
+ } catch (error) {
313
+ observer.error(error);
314
+ failedStarting();
315
+ return;
316
+ }
317
+ //#endregion
318
+
319
+ //#region process mutations (outbox)
320
+ subscriptions.push(
321
+ this.mutationsProcessor
322
+ .start()
323
+ .subscribe(({ modelDefinition, model: item, hasMore }) =>
324
+ this.runningProcesses.add(async () => {
325
+ const modelConstructor = this.userModelClasses[
326
+ modelDefinition.name
327
+ ] as PersistentModelConstructor<any>;
328
+
329
+ const model = this.modelInstanceCreator(
330
+ modelConstructor,
331
+ item
332
+ );
333
+
334
+ await this.storage.runExclusive(storage =>
335
+ this.modelMerger.merge(
336
+ storage,
337
+ model,
338
+ modelDefinition
339
+ )
340
+ );
341
+
342
+ observer.next({
343
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED,
344
+ data: {
345
+ model: modelConstructor,
346
+ element: model,
347
+ },
348
+ });
349
+
350
+ observer.next({
351
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
352
+ data: {
353
+ isEmpty: !hasMore,
354
+ },
355
+ });
356
+ }, 'mutation processor event')
357
+ )
358
+ );
359
+ //#endregion
360
+
361
+ //#region Merge subscriptions buffer
362
+ // TODO: extract to function
363
+ if (!isNode) {
364
+ subscriptions.push(
365
+ dataSubsObservable.subscribe(
366
+ ([_transformerMutationType, modelDefinition, item]) =>
367
+ this.runningProcesses.add(async () => {
368
+ const modelConstructor = this.userModelClasses[
369
+ modelDefinition.name
370
+ ] as PersistentModelConstructor<any>;
371
+
372
+ const model = this.modelInstanceCreator(
373
+ modelConstructor,
374
+ item
375
+ );
376
+
377
+ await this.storage.runExclusive(storage =>
378
+ this.modelMerger.merge(
379
+ storage,
380
+ model,
381
+ modelDefinition
382
+ )
383
+ );
384
+ }, 'subscription dataSubsObservable event')
385
+ )
386
+ );
387
+ }
388
+ //#endregion
389
+ } else if (!online) {
390
+ this.online = online;
391
+
392
+ observer.next({
393
+ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
394
+ data: {
395
+ active: this.online,
396
+ },
397
+ });
364
398
 
365
- subscriptions.forEach(sub => sub.unsubscribe());
366
- subscriptions = [];
367
- }
399
+ subscriptions.forEach(sub => sub.unsubscribe());
400
+ subscriptions = [];
401
+ }
368
402
 
369
- resolve();
370
- });
403
+ doneStarting();
404
+ }, 'datastore connectivity event')
405
+ );
371
406
  });
372
407
 
373
408
  this.storage
@@ -377,51 +412,52 @@ export class SyncEngine {
377
412
  return modelDefinition.syncable === true;
378
413
  })
379
414
  .subscribe({
380
- next: async ({ opType, model, element, condition }) => {
381
- const namespace =
382
- this.schema.namespaces[this.namespaceResolver(model)];
383
- const MutationEventConstructor = this.modelClasses[
384
- 'MutationEvent'
385
- ] as PersistentModelConstructor<MutationEvent>;
386
- const modelDefinition = this.getModelDefinition(model);
387
- const graphQLCondition = predicateToGraphQLCondition(
388
- condition,
389
- modelDefinition
390
- );
391
- const mutationEvent = createMutationInstanceFromModelOperation(
392
- namespace.relationships,
393
- this.getModelDefinition(model),
394
- opType,
395
- model,
396
- element,
397
- graphQLCondition,
398
- MutationEventConstructor,
399
- this.modelInstanceCreator
400
- );
401
-
402
- await this.outbox.enqueue(this.storage, mutationEvent);
403
-
404
- observer.next({
405
- type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED,
406
- data: {
415
+ next: async ({ opType, model, element, condition }) =>
416
+ this.runningProcesses.add(async () => {
417
+ const namespace =
418
+ this.schema.namespaces[this.namespaceResolver(model)];
419
+ const MutationEventConstructor = this.modelClasses[
420
+ 'MutationEvent'
421
+ ] as PersistentModelConstructor<MutationEvent>;
422
+ const modelDefinition = this.getModelDefinition(model);
423
+ const graphQLCondition = predicateToGraphQLCondition(
424
+ condition,
425
+ modelDefinition
426
+ );
427
+ const mutationEvent = createMutationInstanceFromModelOperation(
428
+ namespace.relationships,
429
+ this.getModelDefinition(model),
430
+ opType,
407
431
  model,
408
432
  element,
409
- },
410
- });
433
+ graphQLCondition,
434
+ MutationEventConstructor,
435
+ this.modelInstanceCreator
436
+ );
411
437
 
412
- observer.next({
413
- type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
414
- data: {
415
- isEmpty: false,
416
- },
417
- });
438
+ await this.outbox.enqueue(this.storage, mutationEvent);
418
439
 
419
- await startPromise;
440
+ observer.next({
441
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED,
442
+ data: {
443
+ model,
444
+ element,
445
+ },
446
+ });
420
447
 
421
- if (this.online) {
422
- this.mutationsProcessor.resume();
423
- }
424
- },
448
+ observer.next({
449
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
450
+ data: {
451
+ isEmpty: false,
452
+ },
453
+ });
454
+
455
+ await startPromise;
456
+
457
+ if (this.online) {
458
+ this.mutationsProcessor.resume();
459
+ }
460
+ }, 'storage event'),
425
461
  });
426
462
 
427
463
  observer.next({
@@ -442,11 +478,7 @@ export class SyncEngine {
442
478
  observer.next({
443
479
  type: ControlMessage.SYNC_ENGINE_READY,
444
480
  });
445
- })();
446
-
447
- return () => {
448
- subscriptions.forEach(sub => sub.unsubscribe());
449
- };
481
+ }, 'sync start');
450
482
  });
451
483
  }
452
484
 
@@ -454,7 +486,12 @@ export class SyncEngine {
454
486
  currentTimeStamp: number
455
487
  ): Promise<Map<SchemaModel, [string, number]>> {
456
488
  const modelLastSync: Map<SchemaModel, [string, number]> = new Map(
457
- (await this.getModelsMetadata()).map(
489
+ (
490
+ await this.runningProcesses.add(
491
+ () => this.getModelsMetadata(),
492
+ 'sync/index getModelsMetadataWithNextFullSync'
493
+ )
494
+ ).map(
458
495
  ({
459
496
  namespace,
460
497
  model,
@@ -489,224 +526,249 @@ export class SyncEngine {
489
526
 
490
527
  return new Observable<ControlMessageType<ControlMessage>>(observer => {
491
528
  let syncQueriesSubscription: ZenObservable.Subscription;
492
- let waitTimeoutId: ReturnType<typeof setTimeout>;
493
-
494
- (async () => {
495
- while (!observer.closed) {
496
- const count: WeakMap<
497
- PersistentModelConstructor<any>,
498
- {
499
- new: number;
500
- updated: number;
501
- deleted: number;
502
- }
503
- > = new WeakMap();
504
-
505
- const modelLastSync = await this.getModelsMetadataWithNextFullSync(
506
- Date.now()
507
- );
508
- const paginatingModels = new Set(modelLastSync.keys());
509
-
510
- let newestFullSyncStartedAt: number;
511
- let theInterval: number;
512
-
513
- let start: number;
514
- let duration: number;
515
- let newestStartedAt: number;
516
- await new Promise(resolve => {
517
- syncQueriesSubscription = this.syncQueriesProcessor
518
- .start(modelLastSync)
519
- .subscribe({
520
- next: async ({
521
- namespace,
522
- modelDefinition,
523
- items,
524
- done,
525
- startedAt,
526
- isFullSync,
527
- }) => {
528
- const modelConstructor = this.userModelClasses[
529
- modelDefinition.name
530
- ] as PersistentModelConstructor<any>;
531
-
532
- if (!count.has(modelConstructor)) {
533
- count.set(modelConstructor, {
534
- new: 0,
535
- updated: 0,
536
- deleted: 0,
537
- });
538
529
 
539
- start = getNow();
540
- newestStartedAt =
541
- newestStartedAt === undefined
542
- ? startedAt
543
- : Math.max(newestStartedAt, startedAt);
544
- }
530
+ this.runningProcesses.isOpen &&
531
+ this.runningProcesses.add(async onTerminate => {
532
+ let terminated = false;
533
+
534
+ while (!observer.closed && !terminated) {
535
+ const count: WeakMap<
536
+ PersistentModelConstructor<any>,
537
+ {
538
+ new: number;
539
+ updated: number;
540
+ deleted: number;
541
+ }
542
+ > = new WeakMap();
543
+
544
+ const modelLastSync = await this.getModelsMetadataWithNextFullSync(
545
+ Date.now()
546
+ );
547
+ const paginatingModels = new Set(modelLastSync.keys());
548
+
549
+ let newestFullSyncStartedAt: number;
550
+ let theInterval: number;
551
+
552
+ let start: number;
553
+ let duration: number;
554
+ let newestStartedAt: number;
555
+ await new Promise((resolve, reject) => {
556
+ if (!this.runningProcesses.isOpen) resolve();
557
+ onTerminate.then(() => resolve());
558
+ syncQueriesSubscription = this.syncQueriesProcessor
559
+ .start(modelLastSync)
560
+ .subscribe({
561
+ next: async ({
562
+ namespace,
563
+ modelDefinition,
564
+ items,
565
+ done,
566
+ startedAt,
567
+ isFullSync,
568
+ }) => {
569
+ const modelConstructor = this.userModelClasses[
570
+ modelDefinition.name
571
+ ] as PersistentModelConstructor<any>;
545
572
 
546
- /**
547
- * If there are mutations in the outbox for a given id, those need to be
548
- * merged individually. Otherwise, we can merge them in batches.
549
- */
550
- await this.storage.runExclusive(async storage => {
551
- const idsInOutbox = await this.outbox.getModelIds(storage);
573
+ if (!count.has(modelConstructor)) {
574
+ count.set(modelConstructor, {
575
+ new: 0,
576
+ updated: 0,
577
+ deleted: 0,
578
+ });
552
579
 
553
- const oneByOne: ModelInstanceMetadata[] = [];
554
- const page = items.filter(item => {
555
- const itemId = getIdentifierValue(modelDefinition, item);
580
+ start = getNow();
581
+ newestStartedAt =
582
+ newestStartedAt === undefined
583
+ ? startedAt
584
+ : Math.max(newestStartedAt, startedAt);
585
+ }
556
586
 
557
- if (!idsInOutbox.has(itemId)) {
558
- return true;
559
- }
587
+ /**
588
+ * If there are mutations in the outbox for a given id, those need to be
589
+ * merged individually. Otherwise, we can merge them in batches.
590
+ */
591
+ await this.storage.runExclusive(async storage => {
592
+ const idsInOutbox = await this.outbox.getModelIds(
593
+ storage
594
+ );
560
595
 
561
- oneByOne.push(item);
562
- return false;
563
- });
596
+ const oneByOne: ModelInstanceMetadata[] = [];
597
+ const page = items.filter(item => {
598
+ const itemId = getIdentifierValue(
599
+ modelDefinition,
600
+ item
601
+ );
564
602
 
565
- const opTypeCount: [any, OpType][] = [];
603
+ if (!idsInOutbox.has(itemId)) {
604
+ return true;
605
+ }
566
606
 
567
- for (const item of oneByOne) {
568
- const opType = await this.modelMerger.merge(
569
- storage,
570
- item,
571
- modelDefinition
572
- );
607
+ oneByOne.push(item);
608
+ return false;
609
+ });
573
610
 
574
- if (opType !== undefined) {
575
- opTypeCount.push([item, opType]);
576
- }
577
- }
611
+ const opTypeCount: [any, OpType][] = [];
578
612
 
579
- opTypeCount.push(
580
- ...(await this.modelMerger.mergePage(
581
- storage,
582
- modelConstructor,
583
- page,
584
- modelDefinition
585
- ))
586
- );
613
+ for (const item of oneByOne) {
614
+ const opType = await this.modelMerger.merge(
615
+ storage,
616
+ item,
617
+ modelDefinition
618
+ );
587
619
 
588
- const counts = count.get(modelConstructor);
589
-
590
- opTypeCount.forEach(([, opType]) => {
591
- switch (opType) {
592
- case OpType.INSERT:
593
- counts.new++;
594
- break;
595
- case OpType.UPDATE:
596
- counts.updated++;
597
- break;
598
- case OpType.DELETE:
599
- counts.deleted++;
600
- break;
601
- default:
602
- exhaustiveCheck(opType);
620
+ if (opType !== undefined) {
621
+ opTypeCount.push([item, opType]);
622
+ }
603
623
  }
604
- });
605
- });
606
-
607
- if (done) {
608
- const { name: modelName } = modelDefinition;
609
624
 
610
- //#region update last sync for type
611
- let modelMetadata = await this.getModelMetadata(
612
- namespace,
613
- modelName
614
- );
625
+ opTypeCount.push(
626
+ ...(await this.modelMerger.mergePage(
627
+ storage,
628
+ modelConstructor,
629
+ page,
630
+ modelDefinition
631
+ ))
632
+ );
615
633
 
616
- const { lastFullSync, fullSyncInterval } = modelMetadata;
617
-
618
- theInterval = fullSyncInterval;
619
-
620
- newestFullSyncStartedAt =
621
- newestFullSyncStartedAt === undefined
622
- ? lastFullSync
623
- : Math.max(
624
- newestFullSyncStartedAt,
625
- isFullSync ? startedAt : lastFullSync
626
- );
627
-
628
- modelMetadata = (
629
- this.modelClasses
630
- .ModelMetadata as PersistentModelConstructor<ModelMetadata>
631
- ).copyOf(modelMetadata, draft => {
632
- draft.lastSync = startedAt;
633
- draft.lastFullSync = isFullSync
634
- ? startedAt
635
- : modelMetadata.lastFullSync;
634
+ const counts = count.get(modelConstructor);
635
+
636
+ opTypeCount.forEach(([, opType]) => {
637
+ switch (opType) {
638
+ case OpType.INSERT:
639
+ counts.new++;
640
+ break;
641
+ case OpType.UPDATE:
642
+ counts.updated++;
643
+ break;
644
+ case OpType.DELETE:
645
+ counts.deleted++;
646
+ break;
647
+ default:
648
+ exhaustiveCheck(opType);
649
+ }
650
+ });
636
651
  });
637
652
 
638
- await this.storage.save(
639
- modelMetadata,
640
- undefined,
641
- ownSymbol
642
- );
643
- //#endregion
653
+ if (done) {
654
+ const { name: modelName } = modelDefinition;
644
655
 
645
- const counts = count.get(modelConstructor);
656
+ //#region update last sync for type
657
+ let modelMetadata = await this.getModelMetadata(
658
+ namespace,
659
+ modelName
660
+ );
646
661
 
647
- this.modelSyncedStatus.set(modelConstructor, true);
662
+ const { lastFullSync, fullSyncInterval } = modelMetadata;
663
+
664
+ theInterval = fullSyncInterval;
665
+
666
+ newestFullSyncStartedAt =
667
+ newestFullSyncStartedAt === undefined
668
+ ? lastFullSync
669
+ : Math.max(
670
+ newestFullSyncStartedAt,
671
+ isFullSync ? startedAt : lastFullSync
672
+ );
673
+
674
+ modelMetadata = (
675
+ this.modelClasses
676
+ .ModelMetadata as PersistentModelConstructor<ModelMetadata>
677
+ ).copyOf(modelMetadata, draft => {
678
+ draft.lastSync = startedAt;
679
+ draft.lastFullSync = isFullSync
680
+ ? startedAt
681
+ : modelMetadata.lastFullSync;
682
+ });
648
683
 
649
- observer.next({
650
- type: ControlMessage.SYNC_ENGINE_MODEL_SYNCED,
651
- data: {
652
- model: modelConstructor,
653
- isFullSync,
654
- isDeltaSync: !isFullSync,
655
- counts,
656
- },
657
- });
684
+ await this.storage.save(
685
+ modelMetadata,
686
+ undefined,
687
+ ownSymbol
688
+ );
689
+ //#endregion
658
690
 
659
- paginatingModels.delete(modelDefinition);
691
+ const counts = count.get(modelConstructor);
692
+
693
+ this.modelSyncedStatus.set(modelConstructor, true);
660
694
 
661
- if (paginatingModels.size === 0) {
662
- duration = getNow() - start;
663
- resolve();
664
695
  observer.next({
665
- type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY,
696
+ type: ControlMessage.SYNC_ENGINE_MODEL_SYNCED,
697
+ data: {
698
+ model: modelConstructor,
699
+ isFullSync,
700
+ isDeltaSync: !isFullSync,
701
+ counts,
702
+ },
666
703
  });
667
- syncQueriesSubscription.unsubscribe();
704
+
705
+ paginatingModels.delete(modelDefinition);
706
+
707
+ if (paginatingModels.size === 0) {
708
+ duration = getNow() - start;
709
+ resolve();
710
+ observer.next({
711
+ type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY,
712
+ });
713
+ syncQueriesSubscription.unsubscribe();
714
+ }
668
715
  }
669
- }
670
- },
671
- error: error => {
672
- observer.error(error);
716
+ },
717
+ error: error => {
718
+ observer.error(error);
719
+ },
720
+ });
721
+
722
+ observer.next({
723
+ type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_STARTED,
724
+ data: {
725
+ models: Array.from(paginatingModels).map(({ name }) => name),
673
726
  },
674
727
  });
675
-
676
- observer.next({
677
- type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_STARTED,
678
- data: {
679
- models: Array.from(paginatingModels).map(({ name }) => name),
680
- },
681
728
  });
682
- });
683
729
 
684
- const msNextFullSync =
685
- newestFullSyncStartedAt +
686
- theInterval -
687
- (newestStartedAt + duration);
688
-
689
- logger.debug(
690
- `Next fullSync in ${msNextFullSync / 1000} seconds. (${new Date(
691
- Date.now() + msNextFullSync
692
- )})`
693
- );
694
-
695
- await new Promise(res => {
696
- waitTimeoutId = setTimeout(res, msNextFullSync);
697
- });
698
- }
699
- })();
730
+ const msNextFullSync =
731
+ newestFullSyncStartedAt +
732
+ theInterval -
733
+ (newestStartedAt + duration);
734
+
735
+ logger.debug(
736
+ `Next fullSync in ${msNextFullSync / 1000} seconds. (${new Date(
737
+ Date.now() + msNextFullSync
738
+ )})`
739
+ );
740
+
741
+ // TODO: create `BackgroundProcessManager.sleep()` ... but, need to put
742
+ // a lot of thought into what that contract looks like to
743
+ // support possible use-cases:
744
+ //
745
+ // 1. non-cancelable
746
+ // 2. cancelable, unsleep on exit()
747
+ // 3. cancelable, throw Error on exit()
748
+ // 4. cancelable, callback first on exit()?
749
+ // 5. ... etc. ? ...
750
+ //
751
+ // TLDR; this is a lot of complexity here for a sleep(),
752
+ // but, it's not clear to me yet how to support an
753
+ // extensible, centralized cancelable `sleep()` elegantly.
754
+ await this.runningProcesses.add(async onTerminate => {
755
+ let sleepTimer;
756
+ let unsleep;
757
+
758
+ const sleep = new Promise(_unsleep => {
759
+ unsleep = _unsleep;
760
+ sleepTimer = setTimeout(unsleep, msNextFullSync);
761
+ });
700
762
 
701
- return () => {
702
- if (syncQueriesSubscription) {
703
- syncQueriesSubscription.unsubscribe();
704
- }
763
+ onTerminate.then(() => {
764
+ terminated = true;
765
+ unsleep();
766
+ });
705
767
 
706
- if (waitTimeoutId) {
707
- clearTimeout(waitTimeoutId);
708
- }
709
- };
768
+ return sleep;
769
+ }, 'syncQueriesObservable sleep');
770
+ }
771
+ }, 'syncQueriesObservable main');
710
772
  });
711
773
  }
712
774
 
@@ -726,6 +788,36 @@ export class SyncEngine {
726
788
  this.datastoreConnectivity.unsubscribe();
727
789
  }
728
790
 
791
+ /**
792
+ * Stops all subscription activities and resolves when all activies report
793
+ * that they're disconnected, done retrying, etc..
794
+ */
795
+ public async stop() {
796
+ logger.debug('stopping sync engine');
797
+
798
+ /**
799
+ * Gracefully disconnecting subscribers first just prevents *more* work
800
+ * from entering the pipelines.
801
+ */
802
+ this.unsubscribeConnectivity();
803
+
804
+ /**
805
+ * aggressively shut down any lingering background processes.
806
+ * some of this might be semi-redundant with unsubscribing. however,
807
+ * unsubscribing doesn't allow us to wait for settling.
808
+ * (Whereas `stop()` does.)
809
+ */
810
+
811
+ await this.mutationsProcessor.stop();
812
+ await this.subscriptionsProcessor.stop();
813
+ await this.datastoreConnectivity.stop();
814
+ await this.syncQueriesProcessor.stop();
815
+ await this.runningProcesses.close();
816
+ await this.runningProcesses.open();
817
+
818
+ logger.debug('sync engine stopped and ready to restart');
819
+ }
820
+
729
821
  private async setupModels(params: StartParams) {
730
822
  const { fullSyncInterval } = params;
731
823
  const ModelMetadataConstructor = this.modelClasses