@aws-amplify/datastore 4.1.12-unstable.43d494b.2 → 4.2.0

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.
package/src/sync/index.ts CHANGED
@@ -225,199 +225,201 @@ export class SyncEngine {
225
225
 
226
226
  // this is awaited at the bottom. so, we don't need to register
227
227
  // this explicitly with the context. it's already contained.
228
- const startPromise = new Promise((doneStarting, failedStarting) => {
229
- this.datastoreConnectivity.status().subscribe(
230
- async ({ online }) =>
231
- this.runningProcesses.isOpen &&
232
- this.runningProcesses.add(async onTerminate => {
233
- // From offline to online
234
- if (online && !this.online) {
235
- this.online = online;
236
-
237
- observer.next({
238
- type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
239
- data: {
240
- active: this.online,
241
- },
242
- });
243
-
244
- let ctlSubsObservable: Observable<CONTROL_MSG>;
245
- let dataSubsObservable: Observable<
246
- [TransformerMutationType, SchemaModel, PersistentModel]
247
- >;
248
-
249
- // NOTE: need a way to override this conditional for testing.
250
- if (isNode) {
251
- logger.warn(
252
- 'Realtime disabled when in a server-side environment'
253
- );
254
- } else {
255
- this.stopDisruptionListener =
256
- this.startDisruptionListener();
257
- //#region GraphQL Subscriptions
258
- [ctlSubsObservable, dataSubsObservable] =
259
- this.subscriptionsProcessor.start();
228
+ const startPromise = new Promise<void>(
229
+ (doneStarting, failedStarting) => {
230
+ this.datastoreConnectivity.status().subscribe(
231
+ async ({ online }) =>
232
+ this.runningProcesses.isOpen &&
233
+ this.runningProcesses.add(async onTerminate => {
234
+ // From offline to online
235
+ if (online && !this.online) {
236
+ this.online = online;
237
+
238
+ observer.next({
239
+ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
240
+ data: {
241
+ active: this.online,
242
+ },
243
+ });
244
+
245
+ let ctlSubsObservable: Observable<CONTROL_MSG>;
246
+ let dataSubsObservable: Observable<
247
+ [TransformerMutationType, SchemaModel, PersistentModel]
248
+ >;
249
+
250
+ // NOTE: need a way to override this conditional for testing.
251
+ if (isNode) {
252
+ logger.warn(
253
+ 'Realtime disabled when in a server-side environment'
254
+ );
255
+ } else {
256
+ this.stopDisruptionListener =
257
+ this.startDisruptionListener();
258
+ //#region GraphQL Subscriptions
259
+ [ctlSubsObservable, dataSubsObservable] =
260
+ this.subscriptionsProcessor.start();
261
+
262
+ try {
263
+ await new Promise<void>((resolve, reject) => {
264
+ onTerminate.then(reject);
265
+ const ctlSubsSubscription =
266
+ ctlSubsObservable.subscribe({
267
+ next: msg => {
268
+ if (msg === CONTROL_MSG.CONNECTED) {
269
+ resolve();
270
+ }
271
+ },
272
+ error: err => {
273
+ reject(err);
274
+ const handleDisconnect =
275
+ this.disconnectionHandler();
276
+ handleDisconnect(err);
277
+ },
278
+ });
279
+
280
+ subscriptions.push(ctlSubsSubscription);
281
+ });
282
+ } catch (err) {
283
+ observer.error(err);
284
+ failedStarting();
285
+ return;
286
+ }
260
287
 
288
+ logger.log('Realtime ready');
289
+
290
+ observer.next({
291
+ type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED,
292
+ });
293
+
294
+ //#endregion
295
+ }
296
+
297
+ //#region Base & Sync queries
261
298
  try {
262
- await new Promise((resolve, reject) => {
263
- onTerminate.then(reject);
264
- const ctlSubsSubscription = ctlSubsObservable.subscribe(
265
- {
266
- next: msg => {
267
- if (msg === CONTROL_MSG.CONNECTED) {
299
+ await new Promise<void>((resolve, reject) => {
300
+ const syncQuerySubscription =
301
+ this.syncQueriesObservable().subscribe({
302
+ next: message => {
303
+ const { type } = message;
304
+
305
+ if (
306
+ type ===
307
+ ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
308
+ ) {
268
309
  resolve();
269
310
  }
311
+
312
+ observer.next(message);
270
313
  },
271
- error: err => {
272
- reject(err);
273
- const handleDisconnect =
274
- this.disconnectionHandler();
275
- handleDisconnect(err);
314
+ complete: () => {
315
+ resolve();
276
316
  },
277
- }
278
- );
317
+ error: error => {
318
+ reject(error);
319
+ },
320
+ });
279
321
 
280
- subscriptions.push(ctlSubsSubscription);
322
+ if (syncQuerySubscription) {
323
+ subscriptions.push(syncQuerySubscription);
324
+ }
281
325
  });
282
- } catch (err) {
283
- observer.error(err);
326
+ } catch (error) {
327
+ observer.error(error);
284
328
  failedStarting();
285
329
  return;
286
330
  }
331
+ //#endregion
287
332
 
288
- logger.log('Realtime ready');
289
-
290
- observer.next({
291
- type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED,
292
- });
293
-
333
+ //#region process mutations (outbox)
334
+ subscriptions.push(
335
+ this.mutationsProcessor
336
+ .start()
337
+ .subscribe(
338
+ ({ modelDefinition, model: item, hasMore }) =>
339
+ this.runningProcesses.add(async () => {
340
+ const modelConstructor = this.userModelClasses[
341
+ modelDefinition.name
342
+ ] as PersistentModelConstructor<any>;
343
+
344
+ const model = this.modelInstanceCreator(
345
+ modelConstructor,
346
+ item
347
+ );
348
+
349
+ await this.storage.runExclusive(storage =>
350
+ this.modelMerger.merge(
351
+ storage,
352
+ model,
353
+ modelDefinition
354
+ )
355
+ );
356
+
357
+ observer.next({
358
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED,
359
+ data: {
360
+ model: modelConstructor,
361
+ element: model,
362
+ },
363
+ });
364
+
365
+ observer.next({
366
+ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
367
+ data: {
368
+ isEmpty: !hasMore,
369
+ },
370
+ });
371
+ }, 'mutation processor event')
372
+ )
373
+ );
294
374
  //#endregion
295
- }
296
375
 
297
- //#region Base & Sync queries
298
- try {
299
- await new Promise((resolve, reject) => {
300
- const syncQuerySubscription =
301
- this.syncQueriesObservable().subscribe({
302
- next: message => {
303
- const { type } = message;
304
-
305
- if (
306
- type ===
307
- ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY
308
- ) {
309
- resolve();
310
- }
311
-
312
- observer.next(message);
313
- },
314
- complete: () => {
315
- resolve();
316
- },
317
- error: error => {
318
- reject(error);
319
- },
320
- });
376
+ //#region Merge subscriptions buffer
377
+ // TODO: extract to function
378
+ if (!isNode) {
379
+ subscriptions.push(
380
+ dataSubsObservable!.subscribe(
381
+ ([_transformerMutationType, modelDefinition, item]) =>
382
+ this.runningProcesses.add(async () => {
383
+ const modelConstructor = this.userModelClasses[
384
+ modelDefinition.name
385
+ ] as PersistentModelConstructor<any>;
386
+
387
+ const model = this.modelInstanceCreator(
388
+ modelConstructor,
389
+ item
390
+ );
391
+
392
+ await this.storage.runExclusive(storage =>
393
+ this.modelMerger.merge(
394
+ storage,
395
+ model,
396
+ modelDefinition
397
+ )
398
+ );
399
+ }, 'subscription dataSubsObservable event')
400
+ )
401
+ );
402
+ }
403
+ //#endregion
404
+ } else if (!online) {
405
+ this.online = online;
321
406
 
322
- if (syncQuerySubscription) {
323
- subscriptions.push(syncQuerySubscription);
324
- }
407
+ observer.next({
408
+ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
409
+ data: {
410
+ active: this.online,
411
+ },
325
412
  });
326
- } catch (error) {
327
- observer.error(error);
328
- failedStarting();
329
- return;
330
- }
331
- //#endregion
332
-
333
- //#region process mutations (outbox)
334
- subscriptions.push(
335
- this.mutationsProcessor
336
- .start()
337
- .subscribe(({ modelDefinition, model: item, hasMore }) =>
338
- this.runningProcesses.add(async () => {
339
- const modelConstructor = this.userModelClasses[
340
- modelDefinition.name
341
- ] as PersistentModelConstructor<any>;
342
-
343
- const model = this.modelInstanceCreator(
344
- modelConstructor,
345
- item
346
- );
347
-
348
- await this.storage.runExclusive(storage =>
349
- this.modelMerger.merge(
350
- storage,
351
- model,
352
- modelDefinition
353
- )
354
- );
355
-
356
- observer.next({
357
- type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED,
358
- data: {
359
- model: modelConstructor,
360
- element: model,
361
- },
362
- });
363
413
 
364
- observer.next({
365
- type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
366
- data: {
367
- isEmpty: !hasMore,
368
- },
369
- });
370
- }, 'mutation processor event')
371
- )
372
- );
373
- //#endregion
374
-
375
- //#region Merge subscriptions buffer
376
- // TODO: extract to function
377
- if (!isNode) {
378
- subscriptions.push(
379
- dataSubsObservable!.subscribe(
380
- ([_transformerMutationType, modelDefinition, item]) =>
381
- this.runningProcesses.add(async () => {
382
- const modelConstructor = this.userModelClasses[
383
- modelDefinition.name
384
- ] as PersistentModelConstructor<any>;
385
-
386
- const model = this.modelInstanceCreator(
387
- modelConstructor,
388
- item
389
- );
390
-
391
- await this.storage.runExclusive(storage =>
392
- this.modelMerger.merge(
393
- storage,
394
- model,
395
- modelDefinition
396
- )
397
- );
398
- }, 'subscription dataSubsObservable event')
399
- )
400
- );
414
+ subscriptions.forEach(sub => sub.unsubscribe());
415
+ subscriptions = [];
401
416
  }
402
- //#endregion
403
- } else if (!online) {
404
- this.online = online;
405
-
406
- observer.next({
407
- type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
408
- data: {
409
- active: this.online,
410
- },
411
- });
412
-
413
- subscriptions.forEach(sub => sub.unsubscribe());
414
- subscriptions = [];
415
- }
416
417
 
417
- doneStarting();
418
- }, 'datastore connectivity event')
419
- );
420
- });
418
+ doneStarting();
419
+ }, 'datastore connectivity event')
420
+ );
421
+ }
422
+ );
421
423
 
422
424
  this.storage
423
425
  .observe(null, null, ownSymbol)
@@ -567,7 +569,7 @@ export class SyncEngine {
567
569
  let start: number;
568
570
  let syncDuration: number;
569
571
  let lastStartedAt: number;
570
- await new Promise((resolve, reject) => {
572
+ await new Promise<void>((resolve, reject) => {
571
573
  if (!this.runningProcesses.isOpen) resolve();
572
574
  onTerminate.then(() => resolve());
573
575
  syncQueriesSubscription = this.syncQueriesProcessor
@@ -1122,10 +1124,15 @@ export class SyncEngine {
1122
1124
  * Schedule a sync to start when syncQueriesObservable enters sleep state
1123
1125
  * Start sync immediately if syncQueriesObservable is already in sleep state
1124
1126
  */
1125
- private scheduleSync(): Promise<void> {
1126
- return this.waitForSleepState.then(() => {
1127
- // unsleepSyncQueriesObservable will be set if waitForSleepState has resolved
1128
- this.unsleepSyncQueriesObservable!();
1129
- });
1127
+ private scheduleSync() {
1128
+ return (
1129
+ this.runningProcesses.isOpen &&
1130
+ this.runningProcesses.add(() =>
1131
+ this.waitForSleepState.then(() => {
1132
+ // unsleepSyncQueriesObservable will be set if waitForSleepState has resolved
1133
+ this.unsleepSyncQueriesObservable!();
1134
+ })
1135
+ )
1136
+ );
1130
1137
  }
1131
1138
  }
@@ -22,6 +22,7 @@ export const mutationErrorMap: ErrorMap = {
22
22
  ConfigError: () => false,
23
23
  Transient: error => connectionTimeout(error) || serverError(error),
24
24
  Unauthorized: error =>
25
+ error.message === 'Unauthorized' ||
25
26
  /^Request failed with status code 401/.test(error.message),
26
27
  };
27
28
 
@@ -44,7 +45,7 @@ export const syncErrorMap: ErrorMap = {
44
45
  BadRecord: error => /^Cannot return \w+ for [\w-_]+ type/.test(error.message),
45
46
  ConfigError: () => false,
46
47
  Transient: error => connectionTimeout(error) || serverError(error),
47
- Unauthorized: () => false,
48
+ Unauthorized: error => (error as any).errorType === 'Unauthorized',
48
49
  };
49
50
 
50
51
  /**
@@ -231,6 +231,22 @@ class MutationProcessor {
231
231
  operationAuthModes[authModeAttempts - 1]
232
232
  }`
233
233
  );
234
+ try {
235
+ await this.errorHandler({
236
+ recoverySuggestion:
237
+ 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues',
238
+ localModel: null!,
239
+ message: error.message,
240
+ model: modelConstructor.name,
241
+ operation: opName,
242
+ errorType: getMutationErrorType(error),
243
+ process: ProcessName.sync,
244
+ remoteModel: null!,
245
+ cause: error,
246
+ });
247
+ } catch (e) {
248
+ logger.error('Mutation error handler failed with:', e);
249
+ }
234
250
  throw error;
235
251
  }
236
252
  logger.debug(
@@ -227,6 +227,7 @@ class SyncProcessor {
227
227
  // Catch client-side (GraphQLAuthError) & 401/403 errors here so that we don't continue to retry
228
228
  const clientOrForbiddenErrorMessage =
229
229
  getClientSideAuthError(error) || getForbiddenError(error);
230
+
230
231
  if (clientOrForbiddenErrorMessage) {
231
232
  logger.error('Sync processor retry error:', error);
232
233
  throw new NonRetryableError(clientOrForbiddenErrorMessage);
@@ -284,20 +285,44 @@ class SyncProcessor {
284
285
  });
285
286
  }
286
287
 
288
+ /**
289
+ * Handle $util.unauthorized() in resolver request mapper, which responses with something
290
+ * like this:
291
+ *
292
+ * ```
293
+ * {
294
+ * data: { syncYourModel: null },
295
+ * errors: [
296
+ * {
297
+ * path: ['syncLegacyJSONComments'],
298
+ * data: null,
299
+ * errorType: 'Unauthorized',
300
+ * errorInfo: null,
301
+ * locations: [{ line: 2, column: 3, sourceName: null }],
302
+ * message:
303
+ * 'Not Authorized to access syncYourModel on type Query',
304
+ * },
305
+ * ],
306
+ * }
307
+ * ```
308
+ *
309
+ * The correct handling for this is to signal that we've encountered a non-retryable error,
310
+ * since the server has responded with an auth error and *NO DATA* at this point.
311
+ */
287
312
  if (unauthorized) {
288
- logger.warn(
289
- 'queryError',
290
- `User is unauthorized to query ${opName}, some items could not be returned.`
291
- );
292
-
293
- result.data = result.data || {};
294
-
295
- result.data[opName] = {
296
- ...opResultDefaults,
297
- ...result.data[opName],
298
- };
299
-
300
- return result;
313
+ this.errorHandler({
314
+ recoverySuggestion:
315
+ 'Ensure app code is up to date, auth directives exist and are correct on each model, and that server-side data has not been invalidated by a schema change. If the problem persists, search for or create an issue: https://github.com/aws-amplify/amplify-js/issues',
316
+ localModel: null!,
317
+ message: error.message,
318
+ model: modelDefinition.name,
319
+ operation: opName,
320
+ errorType: getSyncErrorType(error.errors[0]),
321
+ process: ProcessName.sync,
322
+ remoteModel: null!,
323
+ cause: error,
324
+ });
325
+ throw new NonRetryableError(error);
301
326
  }
302
327
 
303
328
  if (result.data?.[opName].items?.length) {
@@ -405,7 +430,15 @@ class SyncProcessor {
405
430
  } catch (e) {
406
431
  logger.error('Sync error handler failed with:', e);
407
432
  }
408
- return res();
433
+ /**
434
+ * If there's an error, this model fails, but the rest of the sync should
435
+ * continue. To facilitate this, we explicitly mark this model as `done`
436
+ * with no items and allow the loop to continue organically. This ensures
437
+ * all callbacks (subscription messages) happen as normal, so anything
438
+ * waiting on them knows the model is as done as it can be.
439
+ */
440
+ done = true;
441
+ items = [];
409
442
  }
410
443
 
411
444
  recordsReceived += items.length;