dexie-cloud-addon 4.1.0-beta.43 → 4.1.0-beta.45

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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.1.0-beta.43, Fri Feb 07 2025
11
+ * Version 4.1.0-beta.45, Mon Mar 31 2025
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -2802,6 +2802,7 @@
2802
2802
  break;
2803
2803
  case 'u-s':
2804
2804
  writeVarUint8Array(encoder, msg.u);
2805
+ writeVarString(encoder, msg.r || '');
2805
2806
  break;
2806
2807
  }
2807
2808
  }
@@ -3188,7 +3189,8 @@
3188
3189
  table,
3189
3190
  prop,
3190
3191
  k,
3191
- u: readVarUint8Array(decoder)
3192
+ u: readVarUint8Array(decoder),
3193
+ r: (decoder.pos < decoder.arr.length && readVarString(decoder)) || undefined,
3192
3194
  };
3193
3195
  default:
3194
3196
  throw new TypeError(`Unknown message type: ${type}`);
@@ -4465,6 +4467,7 @@
4465
4467
  baseRevs,
4466
4468
  changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
4467
4469
  y,
4470
+ dxcv: db.cloud.version
4468
4471
  };
4469
4472
  console.debug('Sync request', syncRequest);
4470
4473
  db.syncStateChangedEvent.next({
@@ -4607,9 +4610,11 @@
4607
4610
  return __awaiter(this, void 0, void 0, function* () {
4608
4611
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
4609
4612
  for (const { table: tableName, muts } of changes) {
4613
+ if (!db.dx._allTables[tableName]) {
4614
+ console.debug(`Server sent changes for table ${tableName} that we don't have. Ignoring.`);
4615
+ continue;
4616
+ }
4610
4617
  const table = db.table(tableName);
4611
- if (!table)
4612
- continue; // If server sends changes on a table we don't have, ignore it.
4613
4618
  const { primaryKey } = table.core.schema;
4614
4619
  const keyDecoder = (key) => {
4615
4620
  switch (key[0]) {
@@ -4795,9 +4800,15 @@
4795
4800
 
4796
4801
  function getUpdatesTable(db, table, ydocProp) {
4797
4802
  var _a, _b, _c;
4803
+ if (!db.dx._allTables[table])
4804
+ return undefined;
4798
4805
  const utbl = (_c = (_b = (_a = db.table(table)) === null || _a === void 0 ? void 0 : _a.schema.yProps) === null || _b === void 0 ? void 0 : _b.find(p => p.prop === ydocProp)) === null || _c === void 0 ? void 0 : _c.updatesTable;
4799
- if (!utbl)
4800
- throw new Error(`No updatesTable found for ${table}.${ydocProp}`);
4806
+ if (!utbl) {
4807
+ console.debug(`No updatesTable found for ${table}.${ydocProp}`);
4808
+ return undefined;
4809
+ }
4810
+ if (!db.dx._allTables[utbl])
4811
+ return undefined;
4801
4812
  return db.table(utbl);
4802
4813
  }
4803
4814
 
@@ -4808,74 +4819,91 @@
4808
4819
  let resyncNeeded = false;
4809
4820
  let yServerRevision;
4810
4821
  for (const m of yMessages) {
4811
- switch (m.type) {
4812
- case 'u-s': {
4813
- const utbl = getUpdatesTable(db, m.table, m.prop);
4814
- receivedUntils[utbl.name] = yield utbl.add({
4815
- k: m.k,
4816
- u: m.u,
4817
- });
4818
- break;
4819
- }
4820
- case 'u-ack': {
4821
- const utbl = getUpdatesTable(db, m.table, m.prop);
4822
- yield db.transaction('rw', utbl, (tx) => __awaiter(this, void 0, void 0, function* () {
4823
- let syncer = (yield tx
4824
- .table(utbl.name)
4825
- .get(DEXIE_CLOUD_SYNCER_ID));
4826
- yield tx.table(utbl.name).put(Object.assign(Object.assign({}, (syncer || { i: DEXIE_CLOUD_SYNCER_ID })), { unsentFrom: Math.max((syncer === null || syncer === void 0 ? void 0 : syncer.unsentFrom) || 1, m.i + 1) }));
4827
- }));
4828
- break;
4829
- }
4830
- case 'u-reject': {
4831
- // Acces control or constraint rejected the update.
4832
- // We delete it. It's not going to be sent again.
4833
- // What's missing is a way to notify consumers, such as Tiptap editor, that the update was rejected.
4834
- // This is only an issue when the document is open. We could find the open document and
4835
- // in a perfect world, we should send a reverse update to the open document to undo the change.
4836
- // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
4837
- console.debug(`Y update rejected. Deleting it.`);
4838
- const utbl = getUpdatesTable(db, m.table, m.prop);
4839
- // Delete the rejected update and all local updates since (avoid holes in the CRDT)
4840
- // and destroy it's open document if there is one.
4841
- const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
4842
- if (primaryKey != null) {
4843
- yield db.transaction('rw', utbl, (tx) => {
4844
- // @ts-ignore
4845
- tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
4846
- return utbl
4847
- .where('i')
4848
- .aboveOrEqual(m.i)
4849
- .filter((u) => Dexie.cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
4850
- .delete();
4851
- });
4852
- // Destroy active doc
4853
- const activeDoc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
4854
- if (activeDoc)
4855
- activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
4822
+ try {
4823
+ switch (m.type) {
4824
+ case 'u-s': {
4825
+ const utbl = getUpdatesTable(db, m.table, m.prop);
4826
+ if (utbl) {
4827
+ const updateRow = {
4828
+ k: m.k,
4829
+ u: m.u,
4830
+ };
4831
+ if (m.r) {
4832
+ // @ts-ignore
4833
+ updateRow.r = m.r;
4834
+ yServerRevision = m.r;
4835
+ }
4836
+ receivedUntils[utbl.name] = yield utbl.add(updateRow);
4837
+ }
4838
+ break;
4856
4839
  }
4857
- break;
4858
- }
4859
- case 'in-sync': {
4860
- const doc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, m.k, m.prop);
4861
- if (doc && !doc.isSynced) {
4862
- doc.emit('sync', [true]);
4840
+ case 'u-ack': {
4841
+ const utbl = getUpdatesTable(db, m.table, m.prop);
4842
+ if (utbl) {
4843
+ yield db.transaction('rw', utbl, (tx) => __awaiter(this, void 0, void 0, function* () {
4844
+ let syncer = (yield tx
4845
+ .table(utbl.name)
4846
+ .get(DEXIE_CLOUD_SYNCER_ID));
4847
+ yield tx.table(utbl.name).put(Object.assign(Object.assign({}, (syncer || { i: DEXIE_CLOUD_SYNCER_ID })), { unsentFrom: Math.max((syncer === null || syncer === void 0 ? void 0 : syncer.unsentFrom) || 1, m.i + 1) }));
4848
+ }));
4849
+ }
4850
+ break;
4863
4851
  }
4864
- break;
4865
- }
4866
- case 'y-complete-sync-done': {
4867
- yServerRevision = m.yServerRev;
4868
- break;
4852
+ case 'u-reject': {
4853
+ // Acces control or constraint rejected the update.
4854
+ // We delete it. It's not going to be sent again.
4855
+ // What's missing is a way to notify consumers, such as Tiptap editor, that the update was rejected.
4856
+ // This is only an issue when the document is open. We could find the open document and
4857
+ // in a perfect world, we should send a reverse update to the open document to undo the change.
4858
+ // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
4859
+ console.debug(`Y update rejected. Deleting it.`);
4860
+ const utbl = getUpdatesTable(db, m.table, m.prop);
4861
+ if (!utbl)
4862
+ break;
4863
+ // Delete the rejected update and all local updates since (avoid holes in the CRDT)
4864
+ // and destroy it's open document if there is one.
4865
+ const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
4866
+ if (primaryKey != null) {
4867
+ yield db.transaction('rw', utbl, (tx) => {
4868
+ // @ts-ignore
4869
+ tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
4870
+ return utbl
4871
+ .where('i')
4872
+ .aboveOrEqual(m.i)
4873
+ .filter((u) => Dexie.cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
4874
+ .delete();
4875
+ });
4876
+ // Destroy active doc
4877
+ const activeDoc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
4878
+ if (activeDoc)
4879
+ activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
4880
+ }
4881
+ break;
4882
+ }
4883
+ case 'in-sync': {
4884
+ const doc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, m.k, m.prop);
4885
+ if (doc && !doc.isSynced) {
4886
+ doc.emit('sync', [true]);
4887
+ }
4888
+ break;
4889
+ }
4890
+ case 'y-complete-sync-done': {
4891
+ yServerRevision = m.yServerRev;
4892
+ break;
4893
+ }
4894
+ case 'outdated-server-rev':
4895
+ resyncNeeded = true;
4896
+ break;
4869
4897
  }
4870
- case 'outdated-server-rev':
4871
- resyncNeeded = true;
4872
- break;
4898
+ }
4899
+ catch (e) {
4900
+ console.error(`Failed to apply YMessage`, m, e);
4873
4901
  }
4874
4902
  }
4875
4903
  return {
4876
4904
  receivedUntils,
4877
4905
  resyncNeeded,
4878
- yServerRevision
4906
+ yServerRevision,
4879
4907
  };
4880
4908
  });
4881
4909
  }
@@ -4986,7 +5014,9 @@
4986
5014
  throw new Error(`Protocol error from ${databaseUrl}/y/download`);
4987
5015
  }
4988
5016
  const yTable = getUpdatesTable(db, currentTable, currentProp);
4989
- yield yTable.bulkAdd(docsToInsert);
5017
+ if (yTable) {
5018
+ yield yTable.bulkAdd(docsToInsert);
5019
+ }
4990
5020
  docsToInsert = [];
4991
5021
  }
4992
5022
  if (currentRealmId &&
@@ -5322,12 +5352,12 @@
5322
5352
  return false; // Not needed anymore
5323
5353
  });
5324
5354
  }
5325
- function deleteObjectsFromRemovedRealms(db, res, prevState) {
5355
+ function deleteObjectsFromRemovedRealms(db, res, syncState) {
5326
5356
  return __awaiter(this, void 0, void 0, function* () {
5327
5357
  const deletedRealms = new Set();
5328
5358
  const rejectedRealms = new Set();
5329
- const previousRealmSet = prevState ? prevState.realms : [];
5330
- const previousInviteRealmSet = prevState ? prevState.inviteRealms : [];
5359
+ const previousRealmSet = syncState ? syncState.realms : [];
5360
+ const previousInviteRealmSet = syncState ? syncState.inviteRealms : [];
5331
5361
  const updatedRealmSet = new Set(res.realms);
5332
5362
  const updatedTotalRealmSet = new Set(res.realms.concat(res.inviteRealms));
5333
5363
  for (const realmId of previousRealmSet) {
@@ -5369,17 +5399,10 @@
5369
5399
  }
5370
5400
  }
5371
5401
  }
5372
- if (rejectedRealms.size > 0) {
5373
- // Remove rejected/deleted realms from yDownloadedRealms because of the following use case:
5374
- // 1. User becomes added to the realm
5375
- // 2. User syncs and all documents of the realm is downloaded (downloadYDocsFromServer.ts)
5376
- // 3. User leaves the realm and all docs are deleted locally (built-in-trigger of deleting their rows in this file)
5377
- // 4. User is yet again added to the realm. At this point, we must make sure the docs are not considered already downloaded.
5378
- const updateSpec = {};
5402
+ if (rejectedRealms.size > 0 && (syncState === null || syncState === void 0 ? void 0 : syncState.yDownloadedRealms)) {
5379
5403
  for (const realmId of rejectedRealms) {
5380
- updateSpec[`yDownloadedRealms.${realmId}`] = undefined; // Setting to undefined will delete the property
5404
+ delete syncState.yDownloadedRealms[realmId];
5381
5405
  }
5382
- yield db.$syncState.update('syncState', updateSpec);
5383
5406
  }
5384
5407
  });
5385
5408
  }
@@ -5918,8 +5941,6 @@
5918
5941
  */
5919
5942
  function setCurrentUser(db, user) {
5920
5943
  return __awaiter(this, void 0, void 0, function* () {
5921
- if (user.userId === db.cloud.currentUserId)
5922
- return; // Already this user.
5923
5944
  const $logins = db.table('$logins');
5924
5945
  yield db.transaction('rw', $logins, (tx) => __awaiter(this, void 0, void 0, function* () {
5925
5946
  const existingLogins = yield $logins.toArray();
@@ -6161,8 +6182,14 @@
6161
6182
  }
6162
6183
  return Object.assign(Object.assign({}, table), { mutate: (req) => {
6163
6184
  var _a, _b;
6164
- // @ts-ignore
6165
- if (req.trans.disableChangeTracking) {
6185
+ const idbtrans = req.trans;
6186
+ if (idbtrans.mode === 'versionchange') {
6187
+ // Tell all the other middlewares to skip bothering. We're in versionchange mode.
6188
+ // dexie-cloud is not initialized yet.
6189
+ idbtrans.disableChangeTracking = true;
6190
+ idbtrans.disableAccessControl = true;
6191
+ }
6192
+ if (idbtrans.disableChangeTracking) {
6166
6193
  // Disable ID policy checks and ID generation
6167
6194
  return table.mutate(req);
6168
6195
  }
@@ -6219,17 +6246,13 @@
6219
6246
  return Object.assign(Object.assign({}, core), { table: (tableName) => {
6220
6247
  const table = core.table(tableName);
6221
6248
  return Object.assign(Object.assign({}, table), { mutate: (req) => {
6222
- var _a, _b, _c, _d;
6223
- // @ts-ignore
6224
- if (req.trans.disableChangeTracking) {
6249
+ var _a, _b, _c, _d, _e, _f;
6250
+ const trans = req.trans;
6251
+ if (trans.disableChangeTracking) {
6225
6252
  return table.mutate(req);
6226
6253
  }
6227
- const trans = req.trans;
6228
- if ((_b = (_a = db.cloud.schema) === null || _a === void 0 ? void 0 : _a[tableName]) === null || _b === void 0 ? void 0 : _b.markedForSync) {
6229
- if (trans.mode === 'versionchange') {
6230
- // Don't mutate tables marked for sync in versionchange transactions.
6231
- return Promise.reject(new Dexie.UpgradeError(`Dexie Cloud Addon: Cannot upgrade or populate synced table "${tableName}". See https://dexie.org/cloud/docs/best-practices`));
6232
- }
6254
+ const currentUserId = (_b = (_a = trans.currentUser) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : UNAUTHORIZED_USER.userId;
6255
+ if ((_d = (_c = db.cloud.schema) === null || _c === void 0 ? void 0 : _c[tableName]) === null || _d === void 0 ? void 0 : _d.markedForSync) {
6233
6256
  if (req.type === 'add' || req.type === 'put') {
6234
6257
  if (tableName === 'members') {
6235
6258
  for (const member of req.values) {
@@ -6253,12 +6276,12 @@
6253
6276
  // and expect them to be returned. That scenario must work also when db.cloud.currentUserId === 'unauthorized'.
6254
6277
  for (const obj of req.values) {
6255
6278
  if (!obj.owner) {
6256
- obj.owner = trans.currentUser.userId;
6279
+ obj.owner = currentUserId;
6257
6280
  }
6258
6281
  if (!obj.realmId) {
6259
- obj.realmId = trans.currentUser.userId;
6282
+ obj.realmId = currentUserId;
6260
6283
  }
6261
- const key = (_d = (_c = table.schema.primaryKey).extractKey) === null || _d === void 0 ? void 0 : _d.call(_c, obj);
6284
+ const key = (_f = (_e = table.schema.primaryKey).extractKey) === null || _f === void 0 ? void 0 : _f.call(_e, obj);
6262
6285
  if (typeof key === 'string' && key[0] === '#') {
6263
6286
  // Add $ts prop for put operations and
6264
6287
  // disable update operations as well as consistent
@@ -6354,16 +6377,14 @@
6354
6377
  name: 'MutationTrackingMiddleware',
6355
6378
  level: 1,
6356
6379
  create: (core) => {
6380
+ const allTableNames = new Set(core.schema.tables.map((t) => t.name));
6357
6381
  const ordinaryTables = core.schema.tables.filter((t) => !/^\$/.test(t.name));
6358
- let mutTableMap;
6359
- try {
6360
- mutTableMap = new Map(ordinaryTables.map((tbl) => [
6361
- tbl.name,
6362
- core.table(`$${tbl.name}_mutations`),
6363
- ]));
6364
- }
6365
- catch (_a) {
6366
- throwVersionIncrementNeeded();
6382
+ const mutTableMap = new Map();
6383
+ for (const tbl of ordinaryTables) {
6384
+ const mutationTableName = `$${tbl.name}_mutations`;
6385
+ if (allTableNames.has(mutationTableName)) {
6386
+ mutTableMap.set(tbl.name, core.table(mutationTableName));
6387
+ }
6367
6388
  }
6368
6389
  return Object.assign(Object.assign({}, core), { transaction: (tables, mode) => {
6369
6390
  let tx;
@@ -6441,6 +6462,11 @@
6441
6462
  }
6442
6463
  const { schema } = table;
6443
6464
  const mutsTable = mutTableMap.get(tableName);
6465
+ if (!mutsTable) {
6466
+ // We cannot track mutations on this table because there is no mutations table for it.
6467
+ // This might happen in upgraders that executes before cloud schema is applied.
6468
+ return table;
6469
+ }
6444
6470
  return guardedTable(Object.assign(Object.assign({}, table), { mutate: (req) => {
6445
6471
  var _a, _b, _c;
6446
6472
  const trans = req.trans;
@@ -6899,13 +6925,13 @@
6899
6925
  const CLIENT_PING_INTERVAL = 30000;
6900
6926
  const FAIL_RETRY_WAIT_TIME = 60000;
6901
6927
  class WSObservable extends rxjs.Observable {
6902
- constructor(db, rev, realmSetHash, clientIdentity, messageProducer, webSocketStatus, user) {
6903
- super((subscriber) => new WSConnection(db, rev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus));
6928
+ constructor(db, rev, yrev, realmSetHash, clientIdentity, messageProducer, webSocketStatus, user) {
6929
+ super((subscriber) => new WSConnection(db, rev, yrev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus));
6904
6930
  }
6905
6931
  }
6906
6932
  let counter = 0;
6907
6933
  class WSConnection extends rxjs.Subscription {
6908
- constructor(db, rev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus) {
6934
+ constructor(db, rev, yrev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus) {
6909
6935
  super(() => this.teardown());
6910
6936
  this.id = ++counter;
6911
6937
  this.subscriptions = new Set();
@@ -6914,6 +6940,7 @@
6914
6940
  this.db = db;
6915
6941
  this.databaseUrl = db.cloud.options.databaseUrl;
6916
6942
  this.rev = rev;
6943
+ this.yrev = yrev;
6917
6944
  this.realmSetHash = realmSetHash;
6918
6945
  this.clientIdentity = clientIdentity;
6919
6946
  this.user = user;
@@ -6983,6 +7010,11 @@
6983
7010
  }
6984
7011
  this.webSocketStatus.next('connecting');
6985
7012
  this.pinger = setInterval(() => __awaiter(this, void 0, void 0, function* () {
7013
+ // setInterval here causes unnecessary pings when server is proved active anyway.
7014
+ // TODO: Use setTimout() here instead. When triggered, check if we really need to ping.
7015
+ // In case we've had server activity, we don't need to ping. Then schedule then next ping
7016
+ // to the time when we should ping next time (based on lastServerActivity + CLIENT_PING_INTERVAL).
7017
+ // Else, ping now and schedule next ping to CLIENT_PING_INTERVAL from now.
6986
7018
  if (this.closed) {
6987
7019
  console.debug('pinger check', this.id, 'CLOSED.');
6988
7020
  this.teardown();
@@ -7029,9 +7061,13 @@
7029
7061
  if (this.subscriber.closed)
7030
7062
  return;
7031
7063
  searchParams.set('v', '2');
7032
- searchParams.set('rev', this.rev);
7064
+ if (this.rev)
7065
+ searchParams.set('rev', this.rev);
7066
+ if (this.yrev)
7067
+ searchParams.set('yrev', this.yrev);
7033
7068
  searchParams.set('realmsHash', this.realmSetHash);
7034
7069
  searchParams.set('clientId', this.clientIdentity);
7070
+ searchParams.set('dxcv', this.db.cloud.version);
7035
7071
  if (this.user.accessToken) {
7036
7072
  searchParams.set('token', this.user.accessToken);
7037
7073
  }
@@ -7068,8 +7104,8 @@
7068
7104
  }
7069
7105
  }
7070
7106
  }
7071
- else if (msg.type === 'u-ack' || msg.type === 'u-reject' || msg.type === 'u-s' || msg.type === 'in-sync') {
7072
- applyYServerMessages([msg], this.db);
7107
+ else if (msg.type === 'pong') {
7108
+ // Do nothing
7073
7109
  }
7074
7110
  else if (msg.type === 'doc-open') {
7075
7111
  const docCache = Dexie.DexieYProvider.getDocCache(this.db.dx);
@@ -7078,11 +7114,26 @@
7078
7114
  getOpenDocSignal(doc).next(); // Make yHandler reopen the document on server.
7079
7115
  }
7080
7116
  }
7081
- else if (msg.type === 'outdated-server-rev' || msg.type === 'y-complete-sync-done') {
7082
- // Won't happen but need this for typing.
7083
- throw new Error('Outdated server revision or y-complete-sync-done not expected over WebSocket - only in sync using fetch()');
7117
+ else if (msg.type === 'u-ack' || msg.type === 'u-reject' || msg.type === 'u-s' || msg.type === 'in-sync' || msg.type === 'outdated-server-rev' || msg.type === 'y-complete-sync-done') {
7118
+ applyYServerMessages([msg], this.db).then((_a) => __awaiter(this, [_a], void 0, function* ({ resyncNeeded, yServerRevision, receivedUntils }) {
7119
+ if (yServerRevision) {
7120
+ yield this.db.$syncState.update('syncState', { yServerRevision: yServerRevision });
7121
+ }
7122
+ if (msg.type === 'u-s' && receivedUntils) {
7123
+ const utbl = getUpdatesTable(this.db, msg.table, msg.prop);
7124
+ if (utbl) {
7125
+ const receivedUntil = receivedUntils[utbl.name];
7126
+ if (receivedUntil) {
7127
+ yield utbl.update(DEXIE_CLOUD_SYNCER_ID, { receivedUntil });
7128
+ }
7129
+ }
7130
+ }
7131
+ if (resyncNeeded) {
7132
+ yield this.db.cloud.sync({ purpose: 'pull', wait: true });
7133
+ }
7134
+ }));
7084
7135
  }
7085
- else if (msg.type !== 'pong') {
7136
+ else {
7086
7137
  // Forward the request to our subscriber, wich is in messageFromServerQueue.ts (via connectWebSocket's subscribe() at the end!)
7087
7138
  this.subscriber.next(msg);
7088
7139
  }
@@ -7215,7 +7266,7 @@
7215
7266
  // If no new entries, server won't bother the client. If new entries, server sends only those
7216
7267
  // and the baseRev of the last from same client-ID.
7217
7268
  if (userLogin) {
7218
- return new WSObservable(db, db.cloud.persistedSyncState.value.serverRevision, realmSetHash, db.cloud.persistedSyncState.value.clientIdentity, messageProducer, db.cloud.webSocketStatus, userLogin);
7269
+ return new WSObservable(db, db.cloud.persistedSyncState.value.serverRevision, db.cloud.persistedSyncState.value.yServerRevision, realmSetHash, db.cloud.persistedSyncState.value.clientIdentity, messageProducer, db.cloud.webSocketStatus, userLogin);
7219
7270
  }
7220
7271
  else {
7221
7272
  return rxjs.from([]);
@@ -7345,61 +7396,95 @@
7345
7396
  }
7346
7397
 
7347
7398
  const SECONDS = 1000;
7348
- const MINUTES = 60 * SECONDS;
7349
7399
 
7350
7400
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
7351
7401
  let localSyncEventSubscription = null;
7352
- //let syncHandler: ((event: Event) => void) | null = null;
7353
- //let periodicSyncHandler: ((event: Event) => void) | null = null;
7354
7402
  let cancelToken = { cancelled: false };
7355
- let retryHandle = null;
7356
- let retryPurpose = null; // "pull" is superset of "push"
7357
- function syncAndRetry(purpose, retryNum = 1) {
7403
+ let nextRetryTime = 0;
7404
+ let syncStartTime = 0;
7405
+ function syncAndRetry(retryNum = 1) {
7358
7406
  // Use setTimeout() to get onto a clean stack and
7359
7407
  // break free from possible active transaction:
7360
7408
  setTimeout(() => {
7361
- if (retryHandle)
7362
- clearTimeout(retryHandle);
7363
- const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
7364
- retryHandle = null;
7365
- retryPurpose = null;
7409
+ const purpose = pullSignalled ? 'pull' : 'push';
7410
+ syncStartTime = Date.now();
7366
7411
  syncIfPossible(db, cloudOptions, cloudSchema, {
7367
7412
  cancelToken,
7368
7413
  retryImmediatelyOnFetchError: true, // workaround for "net::ERR_NETWORK_CHANGED" in chrome.
7369
- purpose: combPurpose,
7370
- }).catch((e) => {
7371
- console.error('error in syncIfPossible()', e);
7414
+ purpose,
7415
+ }).then(() => {
7372
7416
  if (cancelToken.cancelled) {
7373
7417
  stop();
7374
7418
  }
7375
- else if (retryNum < 3) {
7376
- // Mimic service worker sync event: retry 3 times
7377
- // * first retry after 5 minutes
7378
- // * second retry 15 minutes later
7379
- const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
7380
- const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
7381
- // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
7382
- if (retryHandle)
7383
- clearTimeout(retryHandle);
7384
- retryHandle = handle;
7385
- retryPurpose = combinedPurpose;
7419
+ else {
7420
+ if (pullSignalled || pushSignalled) {
7421
+ // If we have signalled for more sync, do it now.
7422
+ pullSignalled = false;
7423
+ pushSignalled = false;
7424
+ return syncAndRetry();
7425
+ }
7426
+ }
7427
+ ongoingSync = false;
7428
+ nextRetryTime = 0;
7429
+ syncStartTime = 0;
7430
+ }).catch((error) => {
7431
+ console.error('error in syncIfPossible()', error);
7432
+ if (cancelToken.cancelled) {
7433
+ stop();
7434
+ ongoingSync = false;
7435
+ nextRetryTime = 0;
7436
+ syncStartTime = 0;
7437
+ }
7438
+ else if (retryNum < 5) {
7439
+ // Mimic service worker sync event but a bit more eager: retry 4 times
7440
+ // * first retry after 20 seconds
7441
+ // * second retry 40 seconds later
7442
+ // * third retry 5 minutes later
7443
+ // * last retry 15 minutes later
7444
+ const retryIn = [0, 20, 40, 300, 900][retryNum] * SECONDS;
7445
+ nextRetryTime = Date.now() + retryIn;
7446
+ syncStartTime = 0;
7447
+ setTimeout(() => syncAndRetry(retryNum + 1), retryIn);
7448
+ }
7449
+ else {
7450
+ ongoingSync = false;
7451
+ nextRetryTime = 0;
7452
+ syncStartTime = 0;
7386
7453
  }
7387
7454
  });
7388
7455
  }, 0);
7389
7456
  }
7457
+ let pullSignalled = false;
7458
+ let pushSignalled = false;
7459
+ let ongoingSync = false;
7460
+ const consumer = (purpose) => {
7461
+ if (cancelToken.cancelled)
7462
+ return;
7463
+ if (purpose === 'pull') {
7464
+ pullSignalled = true;
7465
+ }
7466
+ if (purpose === 'push') {
7467
+ pushSignalled = true;
7468
+ }
7469
+ if (ongoingSync) {
7470
+ if (nextRetryTime) {
7471
+ console.debug(`Sync is paused until ${new Date(nextRetryTime).toISOString()} due to error in last sync attempt`);
7472
+ }
7473
+ else if (syncStartTime > 0 && Date.now() - syncStartTime > 20 * SECONDS) {
7474
+ console.debug(`An existing sync operation is taking more than 20 seconds. Will resync when done.`);
7475
+ }
7476
+ return;
7477
+ }
7478
+ ongoingSync = true;
7479
+ syncAndRetry();
7480
+ };
7390
7481
  const start = () => {
7391
7482
  // Sync eagerly whenever a change has happened (+ initially when there's no syncState yet)
7392
7483
  // This initial subscribe will also trigger an sync also now.
7393
7484
  console.debug('Starting LocalSyncWorker', db.localSyncEvent['id']);
7394
7485
  localSyncEventSubscription = db.localSyncEvent.subscribe(({ purpose }) => {
7395
- try {
7396
- syncAndRetry(purpose || 'pull');
7397
- }
7398
- catch (err) {
7399
- console.error('What-the....', err);
7400
- }
7486
+ consumer(purpose || 'pull');
7401
7487
  });
7402
- //setTimeout(()=>db.localSyncEvent.next({}), 5000);
7403
7488
  };
7404
7489
  const stop = () => {
7405
7490
  console.debug('Stopping LocalSyncWorker');
@@ -8191,7 +8276,7 @@
8191
8276
  const syncComplete = new rxjs.Subject();
8192
8277
  dexie.cloud = {
8193
8278
  // @ts-ignore
8194
- version: "4.1.0-beta.43",
8279
+ version: "4.1.0-beta.45",
8195
8280
  options: Object.assign({}, DEFAULT_OPTIONS),
8196
8281
  schema: null,
8197
8282
  get currentUserId() {
@@ -8509,7 +8594,7 @@
8509
8594
  }
8510
8595
  }
8511
8596
  // @ts-ignore
8512
- dexieCloud.version = "4.1.0-beta.43";
8597
+ dexieCloud.version = "4.1.0-beta.45";
8513
8598
  Dexie.Cloud = dexieCloud;
8514
8599
 
8515
8600
  // In case the SW lives for a while, let it reuse already opened connections: