@aws-amplify/datastore
Version:
AppSyncLocal support for aws-amplify
548 lines (547 loc) • 76.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var core_1 = require("@aws-amplify/core");
var pubsub_1 = require("@aws-amplify/pubsub");
var zen_observable_ts_1 = tslib_1.__importDefault(require("zen-observable-ts"));
var predicates_1 = require("../predicates");
var types_1 = require("../types");
var util_1 = require("../util");
var datastoreConnectivity_1 = tslib_1.__importDefault(require("./datastoreConnectivity"));
var merger_1 = require("./merger");
var outbox_1 = require("./outbox");
var mutation_1 = require("./processors/mutation");
var subscription_1 = require("./processors/subscription");
var sync_1 = require("./processors/sync");
var utils_1 = require("./utils");
var isNode = core_1.browserOrNode().isNode;
var logger = new core_1.ConsoleLogger('DataStore');
var ownSymbol = Symbol('sync');
var ControlMessage;
(function (ControlMessage) {
ControlMessage["SYNC_ENGINE_STORAGE_SUBSCRIBED"] = "storageSubscribed";
ControlMessage["SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED"] = "subscriptionsEstablished";
ControlMessage["SYNC_ENGINE_SYNC_QUERIES_STARTED"] = "syncQueriesStarted";
ControlMessage["SYNC_ENGINE_SYNC_QUERIES_READY"] = "syncQueriesReady";
ControlMessage["SYNC_ENGINE_MODEL_SYNCED"] = "modelSynced";
ControlMessage["SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED"] = "outboxMutationEnqueued";
ControlMessage["SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED"] = "outboxMutationProcessed";
ControlMessage["SYNC_ENGINE_OUTBOX_STATUS"] = "outboxStatus";
ControlMessage["SYNC_ENGINE_NETWORK_STATUS"] = "networkStatus";
ControlMessage["SYNC_ENGINE_READY"] = "ready";
})(ControlMessage = exports.ControlMessage || (exports.ControlMessage = {}));
var SyncEngine = /** @class */ (function () {
function SyncEngine(schema, namespaceResolver, modelClasses, userModelClasses, storage, modelInstanceCreator, conflictHandler, errorHandler, syncPredicates, amplifyConfig, authModeStrategy, amplifyContext, connectivityMonitor) {
var _this = this;
if (amplifyConfig === void 0) { amplifyConfig = {}; }
this.schema = schema;
this.namespaceResolver = namespaceResolver;
this.modelClasses = modelClasses;
this.userModelClasses = userModelClasses;
this.storage = storage;
this.modelInstanceCreator = modelInstanceCreator;
this.syncPredicates = syncPredicates;
this.amplifyConfig = amplifyConfig;
this.authModeStrategy = authModeStrategy;
this.amplifyContext = amplifyContext;
this.connectivityMonitor = connectivityMonitor;
this.online = false;
this.modelSyncedStatus = new WeakMap();
this.connectionDisrupted = false;
this.runningProcesses = new core_1.BackgroundProcessManager();
this.waitForSleepState = new Promise(function (resolve) {
_this.syncQueriesObservableStartSleeping = resolve;
});
var MutationEvent = this.modelClasses['MutationEvent'];
this.outbox = new outbox_1.MutationEventOutbox(this.schema, MutationEvent, modelInstanceCreator, ownSymbol);
this.modelMerger = new merger_1.ModelMerger(this.outbox, ownSymbol);
this.syncQueriesProcessor = new sync_1.SyncProcessor(this.schema, this.syncPredicates, this.amplifyConfig, this.authModeStrategy, errorHandler, this.amplifyContext);
this.subscriptionsProcessor = new subscription_1.SubscriptionProcessor(this.schema, this.syncPredicates, this.amplifyConfig, this.authModeStrategy, errorHandler, this.amplifyContext);
this.mutationsProcessor = new mutation_1.MutationProcessor(this.schema, this.storage, this.userModelClasses, this.outbox, this.modelInstanceCreator, MutationEvent, this.amplifyConfig, this.authModeStrategy, errorHandler, conflictHandler, this.amplifyContext);
this.datastoreConnectivity =
this.connectivityMonitor || new datastoreConnectivity_1.default();
}
SyncEngine.prototype.getModelSyncedStatus = function (modelConstructor) {
return this.modelSyncedStatus.get(modelConstructor);
};
SyncEngine.prototype.start = function (params) {
var _this = this;
return new zen_observable_ts_1.default(function (observer) {
logger.log('starting sync engine...');
var subscriptions = [];
_this.runningProcesses.add(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var err_1, startPromise, hasMutationsInOutbox;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.setupModels(params)];
case 1:
_a.sent();
return [3 /*break*/, 3];
case 2:
err_1 = _a.sent();
observer.error(err_1);
return [2 /*return*/];
case 3:
startPromise = new Promise(function (doneStarting, failedStarting) {
_this.datastoreConnectivity.status().subscribe(function (_a) {
var online = _a.online;
return tslib_1.__awaiter(_this, void 0, void 0, function () {
var _this = this;
return tslib_1.__generator(this, function (_b) {
return [2 /*return*/, this.runningProcesses.isOpen &&
this.runningProcesses.add(function (onTerminate) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var ctlSubsObservable_1, dataSubsObservable, err_2, error_1;
var _a;
var _this = this;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
if (!(online && !this.online)) return [3 /*break*/, 10];
this.online = online;
observer.next({
type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
data: {
active: this.online,
},
});
dataSubsObservable = void 0;
if (!isNode) return [3 /*break*/, 1];
logger.warn('Realtime disabled when in a server-side environment');
return [3 /*break*/, 6];
case 1:
this.stopDisruptionListener =
this.startDisruptionListener();
//#region GraphQL Subscriptions
_a = tslib_1.__read(this.subscriptionsProcessor.start(), 2), ctlSubsObservable_1 = _a[0], dataSubsObservable = _a[1];
_b.label = 2;
case 2:
_b.trys.push([2, 4, , 5]);
return [4 /*yield*/, new Promise(function (resolve, reject) {
onTerminate.then(reject);
var ctlSubsSubscription = ctlSubsObservable_1.subscribe({
next: function (msg) {
if (msg === subscription_1.CONTROL_MSG.CONNECTED) {
resolve();
}
},
error: function (err) {
reject(err);
var handleDisconnect = _this.disconnectionHandler();
handleDisconnect(err);
},
});
subscriptions.push(ctlSubsSubscription);
})];
case 3:
_b.sent();
return [3 /*break*/, 5];
case 4:
err_2 = _b.sent();
observer.error(err_2);
failedStarting();
return [2 /*return*/];
case 5:
logger.log('Realtime ready');
observer.next({
type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED,
});
_b.label = 6;
case 6:
_b.trys.push([6, 8, , 9]);
return [4 /*yield*/, new Promise(function (resolve, reject) {
var syncQuerySubscription = _this.syncQueriesObservable().subscribe({
next: function (message) {
var type = message.type;
if (type ===
ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY) {
resolve();
}
observer.next(message);
},
complete: function () {
resolve();
},
error: function (error) {
reject(error);
},
});
if (syncQuerySubscription) {
subscriptions.push(syncQuerySubscription);
}
})];
case 7:
_b.sent();
return [3 /*break*/, 9];
case 8:
error_1 = _b.sent();
observer.error(error_1);
failedStarting();
return [2 /*return*/];
case 9:
//#endregion
//#region process mutations (outbox)
subscriptions.push(this.mutationsProcessor
.start()
.subscribe(function (_a) {
var modelDefinition = _a.modelDefinition, item = _a.model, hasMore = _a.hasMore;
return _this.runningProcesses.add(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var modelConstructor, model;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
modelConstructor = this.userModelClasses[modelDefinition.name];
model = this.modelInstanceCreator(modelConstructor, item);
return [4 /*yield*/, this.storage.runExclusive(function (storage) {
return _this.modelMerger.merge(storage, model, modelDefinition);
})];
case 1:
_a.sent();
observer.next({
type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED,
data: {
model: modelConstructor,
element: model,
},
});
observer.next({
type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
data: {
isEmpty: !hasMore,
},
});
return [2 /*return*/];
}
});
}); }, 'mutation processor event');
}));
//#endregion
//#region Merge subscriptions buffer
// TODO: extract to function
if (!isNode) {
subscriptions.push(dataSubsObservable.subscribe(function (_a) {
var _b = tslib_1.__read(_a, 3), _transformerMutationType = _b[0], modelDefinition = _b[1], item = _b[2];
return _this.runningProcesses.add(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var modelConstructor, model;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
modelConstructor = this.userModelClasses[modelDefinition.name];
model = this.modelInstanceCreator(modelConstructor, item);
return [4 /*yield*/, this.storage.runExclusive(function (storage) {
return _this.modelMerger.merge(storage, model, modelDefinition);
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); }, 'subscription dataSubsObservable event');
}));
}
return [3 /*break*/, 11];
case 10:
if (!online) {
this.online = online;
observer.next({
type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS,
data: {
active: this.online,
},
});
subscriptions.forEach(function (sub) { return sub.unsubscribe(); });
subscriptions = [];
}
_b.label = 11;
case 11:
doneStarting();
return [2 /*return*/];
}
});
}); }, 'datastore connectivity event')];
});
});
});
});
this.storage
.observe(null, null, ownSymbol)
.filter(function (_a) {
var model = _a.model;
var modelDefinition = _this.getModelDefinition(model);
return modelDefinition.syncable === true;
})
.subscribe({
next: function (_a) {
var opType = _a.opType, model = _a.model, element = _a.element, condition = _a.condition;
return tslib_1.__awaiter(_this, void 0, void 0, function () {
var _this = this;
return tslib_1.__generator(this, function (_b) {
return [2 /*return*/, this.runningProcesses.add(function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var namespace, MutationEventConstructor, modelDefinition, graphQLCondition, mutationEvent;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
namespace = this.schema.namespaces[this.namespaceResolver(model)];
MutationEventConstructor = this.modelClasses['MutationEvent'];
modelDefinition = this.getModelDefinition(model);
graphQLCondition = utils_1.predicateToGraphQLCondition(condition, modelDefinition);
mutationEvent = utils_1.createMutationInstanceFromModelOperation(namespace.relationships, this.getModelDefinition(model), opType, model, element, graphQLCondition, MutationEventConstructor, this.modelInstanceCreator);
return [4 /*yield*/, this.outbox.enqueue(this.storage, mutationEvent)];
case 1:
_a.sent();
observer.next({
type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED,
data: {
model: model,
element: element,
},
});
observer.next({
type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
data: {
isEmpty: false,
},
});
return [4 /*yield*/, startPromise];
case 2:
_a.sent();
// Set by the this.datastoreConnectivity.status().subscribe() loop
if (this.online) {
this.mutationsProcessor.resume();
}
return [2 /*return*/];
}
});
}); }, 'storage event')];
});
});
},
});
observer.next({
type: ControlMessage.SYNC_ENGINE_STORAGE_SUBSCRIBED,
});
return [4 /*yield*/, this.outbox.peek(this.storage)];
case 4:
hasMutationsInOutbox = (_a.sent()) === undefined;
observer.next({
type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS,
data: {
isEmpty: hasMutationsInOutbox,
},
});
return [4 /*yield*/, startPromise];
case 5:
_a.sent();
observer.next({
type: ControlMessage.SYNC_ENGINE_READY,
});
return [2 /*return*/];
}
});
}); }, 'sync start');
});
};
SyncEngine.prototype.getModelsMetadataWithNextFullSync = function (currentTimeStamp) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var modelLastSync, _a;
var _this = this;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
_a = Map.bind;
return [4 /*yield*/, this.runningProcesses.add(function () { return _this.getModelsMetadata(); }, 'sync/index getModelsMetadataWithNextFullSync')];
case 1:
modelLastSync = new (_a.apply(Map, [void 0, (_b.sent()).map(function (_a) {
var namespace = _a.namespace, model = _a.model, lastSync = _a.lastSync, lastFullSync = _a.lastFullSync, fullSyncInterval = _a.fullSyncInterval, lastSyncPredicate = _a.lastSyncPredicate;
var nextFullSync = lastFullSync + fullSyncInterval;
var syncFrom = !lastFullSync || nextFullSync < currentTimeStamp
? 0 // perform full sync if expired
: lastSync; // perform delta sync
return [
_this.schema.namespaces[namespace].models[model],
[namespace, syncFrom],
];
})]))();
return [2 /*return*/, modelLastSync];
}
});
});
};
SyncEngine.prototype.syncQueriesObservable = function () {
var _this = this;
if (!this.online) {
return zen_observable_ts_1.default.of();
}
return new zen_observable_ts_1.default(function (observer) {
var syncQueriesSubscription;
_this.runningProcesses.isOpen &&
_this.runningProcesses.add(function (onTerminate) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var terminated, _loop_1, this_1;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
terminated = false;
_loop_1 = function () {
var count, modelLastSync, paginatingModels, lastFullSyncStartedAt, syncInterval, start, syncDuration, lastStartedAt, msNextFullSync;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
count = new WeakMap();
return [4 /*yield*/, this_1.getModelsMetadataWithNextFullSync(Date.now())];
case 1:
modelLastSync = _a.sent();
paginatingModels = new Set(modelLastSync.keys());
return [4 /*yield*/, new Promise(function (resolve, reject) {
if (!_this.runningProcesses.isOpen)
resolve();
onTerminate.then(function () { return resolve(); });
syncQueriesSubscription = _this.syncQueriesProcessor
.start(modelLastSync)
.subscribe({
next: function (_a) {
var namespace = _a.namespace, modelDefinition = _a.modelDefinition, items = _a.items, done = _a.done, startedAt = _a.startedAt, isFullSync = _a.isFullSync;
return tslib_1.__awaiter(_this, void 0, void 0, function () {
var modelConstructor, modelName, modelMetadata_1, lastFullSync, fullSyncInterval, counts;
var _this = this;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
modelConstructor = this.userModelClasses[modelDefinition.name];
if (!count.has(modelConstructor)) {
count.set(modelConstructor, {
new: 0,
updated: 0,
deleted: 0,
});
start = util_1.getNow();
lastStartedAt =
lastStartedAt === undefined
? startedAt
: Math.max(lastStartedAt, startedAt);
}
/**
* If there are mutations in the outbox for a given id, those need to be
* merged individually. Otherwise, we can merge them in batches.
*/
return [4 /*yield*/, this.storage.runExclusive(function (storage) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var idsInOutbox, oneByOne, page, opTypeCount, oneByOne_1, oneByOne_1_1, item, opType, e_1_1, _a, _b, _c, counts;
var e_1, _d;
return tslib_1.__generator(this, function (_e) {
switch (_e.label) {
case 0: return [4 /*yield*/, this.outbox.getModelIds(storage)];
case 1:
idsInOutbox = _e.sent();
oneByOne = [];
page = items.filter(function (item) {
var itemId = utils_1.getIdentifierValue(modelDefinition, item);
if (!idsInOutbox.has(itemId)) {
return true;
}
oneByOne.push(item);
return false;
});
opTypeCount = [];
_e.label = 2;
case 2:
_e.trys.push([2, 7, 8, 9]);
oneByOne_1 = tslib_1.__values(oneByOne), oneByOne_1_1 = oneByOne_1.next();
_e.label = 3;
case 3:
if (!!oneByOne_1_1.done) return [3 /*break*/, 6];
item = oneByOne_1_1.value;
return [4 /*yield*/, this.modelMerger.merge(storage, item, modelDefinition)];
case 4:
opType = _e.sent();
if (opType !== undefined) {
opTypeCount.push([item, opType]);
}
_e.label = 5;
case 5:
oneByOne_1_1 = oneByOne_1.next();
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 9];
case 7:
e_1_1 = _e.sent();
e_1 = { error: e_1_1 };
return [3 /*break*/, 9];
case 8:
try {
if (oneByOne_1_1 && !oneByOne_1_1.done && (_d = oneByOne_1.return)) _d.call(oneByOne_1);
}
finally { if (e_1) throw e_1.error; }
return [7 /*endfinally*/];
case 9:
_b = (_a = opTypeCount.push).apply;
_c = [opTypeCount];
return [4 /*yield*/, this.modelMerger.mergePage(storage, modelConstructor, page, modelDefinition)];
case 10:
_b.apply(_a, _c.concat([tslib_1.__spread.apply(void 0, [(_e.sent())])]));
counts = count.get(modelConstructor);
opTypeCount.forEach(function (_a) {
var _b = tslib_1.__read(_a, 2), opType = _b[1];
switch (opType) {
case types_1.OpType.INSERT:
counts.new++;
break;
case types_1.OpType.UPDATE:
counts.updated++;
break;
case types_1.OpType.DELETE:
counts.deleted++;
break;
default:
throw new Error("Invalid opType " + opType);
}
});
return [2 /*return*/];
}
});
}); })];
case 1:
/**
* If there are mutations in the outbox for a given id, those need to be
* merged individually. Otherwise, we can merge them in batches.
*/
_b.sent();
if (!done) return [3 /*break*/, 4];
modelName = modelDefinition.name;
return [4 /*yield*/, this.getModelMetadata(namespace, modelName)];
case 2:
modelMetadata_1 = _b.sent();
lastFullSync = modelMetadata_1.lastFullSync, fullSyncInterval = modelMetadata_1.fullSyncInterval;
syncInterval = fullSyncInterval;
lastFullSyncStartedAt =
lastFullSyncStartedAt === undefined
? lastFullSync
: Math.max(lastFullSyncStartedAt, isFullSync ? startedAt : lastFullSync);
modelMetadata_1 = this.modelClasses
.ModelMetadata.copyOf(modelMetadata_1, function (draft) {
draft.lastSync = startedAt;
draft.lastFullSync = isFullSync
? startedAt
: modelMetadata_1.lastFullSync;
});
return [4 /*yield*/, this.storage.save(modelMetadata_1, undefined, ownSymbol)];
case 3:
_b.sent();
counts = count.get(modelConstructor);
this.modelSyncedStatus.set(modelConstructor, true);
observer.next({
type: ControlMessage.SYNC_ENGINE_MODEL_SYNCED,
data: {
model: modelConstructor,
isFullSync: isFullSync,
isDeltaSync: !isFullSync,
counts: counts,
},
});
paginatingModels.delete(modelDefinition);
if (paginatingModels.size === 0) {
syncDuration = util_1.getNow() - start;
resolve();
observer.next({