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
  *
@@ -1014,6 +1014,7 @@ function encodeYMessage(msg) {
1014
1014
  break;
1015
1015
  case 'u-s':
1016
1016
  writeVarUint8Array(encoder, msg.u);
1017
+ writeVarString(encoder, msg.r || '');
1017
1018
  break;
1018
1019
  }
1019
1020
  }
@@ -1400,7 +1401,8 @@ function decodeYMessage(a) {
1400
1401
  table,
1401
1402
  prop,
1402
1403
  k,
1403
- u: readVarUint8Array(decoder)
1404
+ u: readVarUint8Array(decoder),
1405
+ r: (decoder.pos < decoder.arr.length && readVarString(decoder)) || undefined,
1404
1406
  };
1405
1407
  default:
1406
1408
  throw new TypeError(`Unknown message type: ${type}`);
@@ -3677,8 +3679,6 @@ function prodLog(level, ...args) {
3677
3679
  */
3678
3680
  function setCurrentUser(db, user) {
3679
3681
  return __awaiter(this, void 0, void 0, function* () {
3680
- if (user.userId === db.cloud.currentUserId)
3681
- return; // Already this user.
3682
3682
  const $logins = db.table('$logins');
3683
3683
  yield db.transaction('rw', $logins, (tx) => __awaiter(this, void 0, void 0, function* () {
3684
3684
  const existingLogins = yield $logins.toArray();
@@ -4768,6 +4768,7 @@ function syncWithServer(changes, y, syncState, baseRevs, db, databaseUrl, schema
4768
4768
  baseRevs,
4769
4769
  changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
4770
4770
  y,
4771
+ dxcv: db.cloud.version
4771
4772
  };
4772
4773
  console.debug('Sync request', syncRequest);
4773
4774
  db.syncStateChangedEvent.next({
@@ -4910,9 +4911,11 @@ function applyServerChanges(changes, db) {
4910
4911
  return __awaiter(this, void 0, void 0, function* () {
4911
4912
  console.debug('Applying server changes', changes, Dexie.currentTransaction);
4912
4913
  for (const { table: tableName, muts } of changes) {
4914
+ if (!db.dx._allTables[tableName]) {
4915
+ console.debug(`Server sent changes for table ${tableName} that we don't have. Ignoring.`);
4916
+ continue;
4917
+ }
4913
4918
  const table = db.table(tableName);
4914
- if (!table)
4915
- continue; // If server sends changes on a table we don't have, ignore it.
4916
4919
  const { primaryKey } = table.core.schema;
4917
4920
  const keyDecoder = (key) => {
4918
4921
  switch (key[0]) {
@@ -5098,9 +5101,15 @@ function listYClientMessagesAndStateVector(db, tablesToSync) {
5098
5101
 
5099
5102
  function getUpdatesTable(db, table, ydocProp) {
5100
5103
  var _a, _b, _c;
5104
+ if (!db.dx._allTables[table])
5105
+ return undefined;
5101
5106
  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;
5102
- if (!utbl)
5103
- throw new Error(`No updatesTable found for ${table}.${ydocProp}`);
5107
+ if (!utbl) {
5108
+ console.debug(`No updatesTable found for ${table}.${ydocProp}`);
5109
+ return undefined;
5110
+ }
5111
+ if (!db.dx._allTables[utbl])
5112
+ return undefined;
5104
5113
  return db.table(utbl);
5105
5114
  }
5106
5115
 
@@ -5111,74 +5120,91 @@ function applyYServerMessages(yMessages, db) {
5111
5120
  let resyncNeeded = false;
5112
5121
  let yServerRevision;
5113
5122
  for (const m of yMessages) {
5114
- switch (m.type) {
5115
- case 'u-s': {
5116
- const utbl = getUpdatesTable(db, m.table, m.prop);
5117
- receivedUntils[utbl.name] = yield utbl.add({
5118
- k: m.k,
5119
- u: m.u,
5120
- });
5121
- break;
5122
- }
5123
- case 'u-ack': {
5124
- const utbl = getUpdatesTable(db, m.table, m.prop);
5125
- yield db.transaction('rw', utbl, (tx) => __awaiter(this, void 0, void 0, function* () {
5126
- let syncer = (yield tx
5127
- .table(utbl.name)
5128
- .get(DEXIE_CLOUD_SYNCER_ID));
5129
- 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) }));
5130
- }));
5131
- break;
5132
- }
5133
- case 'u-reject': {
5134
- // Acces control or constraint rejected the update.
5135
- // We delete it. It's not going to be sent again.
5136
- // What's missing is a way to notify consumers, such as Tiptap editor, that the update was rejected.
5137
- // This is only an issue when the document is open. We could find the open document and
5138
- // in a perfect world, we should send a reverse update to the open document to undo the change.
5139
- // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
5140
- console.debug(`Y update rejected. Deleting it.`);
5141
- const utbl = getUpdatesTable(db, m.table, m.prop);
5142
- // Delete the rejected update and all local updates since (avoid holes in the CRDT)
5143
- // and destroy it's open document if there is one.
5144
- const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
5145
- if (primaryKey != null) {
5146
- yield db.transaction('rw', utbl, (tx) => {
5147
- // @ts-ignore
5148
- tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
5149
- return utbl
5150
- .where('i')
5151
- .aboveOrEqual(m.i)
5152
- .filter((u) => cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
5153
- .delete();
5154
- });
5155
- // Destroy active doc
5156
- const activeDoc = DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
5157
- if (activeDoc)
5158
- activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
5123
+ try {
5124
+ switch (m.type) {
5125
+ case 'u-s': {
5126
+ const utbl = getUpdatesTable(db, m.table, m.prop);
5127
+ if (utbl) {
5128
+ const updateRow = {
5129
+ k: m.k,
5130
+ u: m.u,
5131
+ };
5132
+ if (m.r) {
5133
+ // @ts-ignore
5134
+ updateRow.r = m.r;
5135
+ yServerRevision = m.r;
5136
+ }
5137
+ receivedUntils[utbl.name] = yield utbl.add(updateRow);
5138
+ }
5139
+ break;
5159
5140
  }
5160
- break;
5161
- }
5162
- case 'in-sync': {
5163
- const doc = DexieYProvider.getDocCache(db.dx).find(m.table, m.k, m.prop);
5164
- if (doc && !doc.isSynced) {
5165
- doc.emit('sync', [true]);
5141
+ case 'u-ack': {
5142
+ const utbl = getUpdatesTable(db, m.table, m.prop);
5143
+ if (utbl) {
5144
+ yield db.transaction('rw', utbl, (tx) => __awaiter(this, void 0, void 0, function* () {
5145
+ let syncer = (yield tx
5146
+ .table(utbl.name)
5147
+ .get(DEXIE_CLOUD_SYNCER_ID));
5148
+ 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) }));
5149
+ }));
5150
+ }
5151
+ break;
5166
5152
  }
5167
- break;
5168
- }
5169
- case 'y-complete-sync-done': {
5170
- yServerRevision = m.yServerRev;
5171
- break;
5153
+ case 'u-reject': {
5154
+ // Acces control or constraint rejected the update.
5155
+ // We delete it. It's not going to be sent again.
5156
+ // What's missing is a way to notify consumers, such as Tiptap editor, that the update was rejected.
5157
+ // This is only an issue when the document is open. We could find the open document and
5158
+ // in a perfect world, we should send a reverse update to the open document to undo the change.
5159
+ // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
5160
+ console.debug(`Y update rejected. Deleting it.`);
5161
+ const utbl = getUpdatesTable(db, m.table, m.prop);
5162
+ if (!utbl)
5163
+ break;
5164
+ // Delete the rejected update and all local updates since (avoid holes in the CRDT)
5165
+ // and destroy it's open document if there is one.
5166
+ const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
5167
+ if (primaryKey != null) {
5168
+ yield db.transaction('rw', utbl, (tx) => {
5169
+ // @ts-ignore
5170
+ tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
5171
+ return utbl
5172
+ .where('i')
5173
+ .aboveOrEqual(m.i)
5174
+ .filter((u) => cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
5175
+ .delete();
5176
+ });
5177
+ // Destroy active doc
5178
+ const activeDoc = DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
5179
+ if (activeDoc)
5180
+ activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
5181
+ }
5182
+ break;
5183
+ }
5184
+ case 'in-sync': {
5185
+ const doc = DexieYProvider.getDocCache(db.dx).find(m.table, m.k, m.prop);
5186
+ if (doc && !doc.isSynced) {
5187
+ doc.emit('sync', [true]);
5188
+ }
5189
+ break;
5190
+ }
5191
+ case 'y-complete-sync-done': {
5192
+ yServerRevision = m.yServerRev;
5193
+ break;
5194
+ }
5195
+ case 'outdated-server-rev':
5196
+ resyncNeeded = true;
5197
+ break;
5172
5198
  }
5173
- case 'outdated-server-rev':
5174
- resyncNeeded = true;
5175
- break;
5199
+ }
5200
+ catch (e) {
5201
+ console.error(`Failed to apply YMessage`, m, e);
5176
5202
  }
5177
5203
  }
5178
5204
  return {
5179
5205
  receivedUntils,
5180
5206
  resyncNeeded,
5181
- yServerRevision
5207
+ yServerRevision,
5182
5208
  };
5183
5209
  });
5184
5210
  }
@@ -5289,7 +5315,9 @@ function downloadYDocsFromServer(db_1, databaseUrl_1, _a) {
5289
5315
  throw new Error(`Protocol error from ${databaseUrl}/y/download`);
5290
5316
  }
5291
5317
  const yTable = getUpdatesTable(db, currentTable, currentProp);
5292
- yield yTable.bulkAdd(docsToInsert);
5318
+ if (yTable) {
5319
+ yield yTable.bulkAdd(docsToInsert);
5320
+ }
5293
5321
  docsToInsert = [];
5294
5322
  }
5295
5323
  if (currentRealmId &&
@@ -5625,12 +5653,12 @@ function _sync(db_1, options_1, schema_1) {
5625
5653
  return false; // Not needed anymore
5626
5654
  });
5627
5655
  }
5628
- function deleteObjectsFromRemovedRealms(db, res, prevState) {
5656
+ function deleteObjectsFromRemovedRealms(db, res, syncState) {
5629
5657
  return __awaiter(this, void 0, void 0, function* () {
5630
5658
  const deletedRealms = new Set();
5631
5659
  const rejectedRealms = new Set();
5632
- const previousRealmSet = prevState ? prevState.realms : [];
5633
- const previousInviteRealmSet = prevState ? prevState.inviteRealms : [];
5660
+ const previousRealmSet = syncState ? syncState.realms : [];
5661
+ const previousInviteRealmSet = syncState ? syncState.inviteRealms : [];
5634
5662
  const updatedRealmSet = new Set(res.realms);
5635
5663
  const updatedTotalRealmSet = new Set(res.realms.concat(res.inviteRealms));
5636
5664
  for (const realmId of previousRealmSet) {
@@ -5672,17 +5700,10 @@ function deleteObjectsFromRemovedRealms(db, res, prevState) {
5672
5700
  }
5673
5701
  }
5674
5702
  }
5675
- if (rejectedRealms.size > 0) {
5676
- // Remove rejected/deleted realms from yDownloadedRealms because of the following use case:
5677
- // 1. User becomes added to the realm
5678
- // 2. User syncs and all documents of the realm is downloaded (downloadYDocsFromServer.ts)
5679
- // 3. User leaves the realm and all docs are deleted locally (built-in-trigger of deleting their rows in this file)
5680
- // 4. User is yet again added to the realm. At this point, we must make sure the docs are not considered already downloaded.
5681
- const updateSpec = {};
5703
+ if (rejectedRealms.size > 0 && (syncState === null || syncState === void 0 ? void 0 : syncState.yDownloadedRealms)) {
5682
5704
  for (const realmId of rejectedRealms) {
5683
- updateSpec[`yDownloadedRealms.${realmId}`] = undefined; // Setting to undefined will delete the property
5705
+ delete syncState.yDownloadedRealms[realmId];
5684
5706
  }
5685
- yield db.$syncState.update('syncState', updateSpec);
5686
5707
  }
5687
5708
  });
5688
5709
  }
@@ -6158,8 +6179,14 @@ function createIdGenerationMiddleware(db) {
6158
6179
  }
6159
6180
  return Object.assign(Object.assign({}, table), { mutate: (req) => {
6160
6181
  var _a, _b;
6161
- // @ts-ignore
6162
- if (req.trans.disableChangeTracking) {
6182
+ const idbtrans = req.trans;
6183
+ if (idbtrans.mode === 'versionchange') {
6184
+ // Tell all the other middlewares to skip bothering. We're in versionchange mode.
6185
+ // dexie-cloud is not initialized yet.
6186
+ idbtrans.disableChangeTracking = true;
6187
+ idbtrans.disableAccessControl = true;
6188
+ }
6189
+ if (idbtrans.disableChangeTracking) {
6163
6190
  // Disable ID policy checks and ID generation
6164
6191
  return table.mutate(req);
6165
6192
  }
@@ -6216,17 +6243,13 @@ function createImplicitPropSetterMiddleware(db) {
6216
6243
  return Object.assign(Object.assign({}, core), { table: (tableName) => {
6217
6244
  const table = core.table(tableName);
6218
6245
  return Object.assign(Object.assign({}, table), { mutate: (req) => {
6219
- var _a, _b, _c, _d;
6220
- // @ts-ignore
6221
- if (req.trans.disableChangeTracking) {
6246
+ var _a, _b, _c, _d, _e, _f;
6247
+ const trans = req.trans;
6248
+ if (trans.disableChangeTracking) {
6222
6249
  return table.mutate(req);
6223
6250
  }
6224
- const trans = req.trans;
6225
- if ((_b = (_a = db.cloud.schema) === null || _a === void 0 ? void 0 : _a[tableName]) === null || _b === void 0 ? void 0 : _b.markedForSync) {
6226
- if (trans.mode === 'versionchange') {
6227
- // Don't mutate tables marked for sync in versionchange transactions.
6228
- return Promise.reject(new Dexie.UpgradeError(`Dexie Cloud Addon: Cannot upgrade or populate synced table "${tableName}". See https://dexie.org/cloud/docs/best-practices`));
6229
- }
6251
+ const currentUserId = (_b = (_a = trans.currentUser) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : UNAUTHORIZED_USER.userId;
6252
+ if ((_d = (_c = db.cloud.schema) === null || _c === void 0 ? void 0 : _c[tableName]) === null || _d === void 0 ? void 0 : _d.markedForSync) {
6230
6253
  if (req.type === 'add' || req.type === 'put') {
6231
6254
  if (tableName === 'members') {
6232
6255
  for (const member of req.values) {
@@ -6250,12 +6273,12 @@ function createImplicitPropSetterMiddleware(db) {
6250
6273
  // and expect them to be returned. That scenario must work also when db.cloud.currentUserId === 'unauthorized'.
6251
6274
  for (const obj of req.values) {
6252
6275
  if (!obj.owner) {
6253
- obj.owner = trans.currentUser.userId;
6276
+ obj.owner = currentUserId;
6254
6277
  }
6255
6278
  if (!obj.realmId) {
6256
- obj.realmId = trans.currentUser.userId;
6279
+ obj.realmId = currentUserId;
6257
6280
  }
6258
- const key = (_d = (_c = table.schema.primaryKey).extractKey) === null || _d === void 0 ? void 0 : _d.call(_c, obj);
6281
+ const key = (_f = (_e = table.schema.primaryKey).extractKey) === null || _f === void 0 ? void 0 : _f.call(_e, obj);
6259
6282
  if (typeof key === 'string' && key[0] === '#') {
6260
6283
  // Add $ts prop for put operations and
6261
6284
  // disable update operations as well as consistent
@@ -6351,16 +6374,14 @@ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
6351
6374
  name: 'MutationTrackingMiddleware',
6352
6375
  level: 1,
6353
6376
  create: (core) => {
6377
+ const allTableNames = new Set(core.schema.tables.map((t) => t.name));
6354
6378
  const ordinaryTables = core.schema.tables.filter((t) => !/^\$/.test(t.name));
6355
- let mutTableMap;
6356
- try {
6357
- mutTableMap = new Map(ordinaryTables.map((tbl) => [
6358
- tbl.name,
6359
- core.table(`$${tbl.name}_mutations`),
6360
- ]));
6361
- }
6362
- catch (_a) {
6363
- throwVersionIncrementNeeded();
6379
+ const mutTableMap = new Map();
6380
+ for (const tbl of ordinaryTables) {
6381
+ const mutationTableName = `$${tbl.name}_mutations`;
6382
+ if (allTableNames.has(mutationTableName)) {
6383
+ mutTableMap.set(tbl.name, core.table(mutationTableName));
6384
+ }
6364
6385
  }
6365
6386
  return Object.assign(Object.assign({}, core), { transaction: (tables, mode) => {
6366
6387
  let tx;
@@ -6438,6 +6459,11 @@ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
6438
6459
  }
6439
6460
  const { schema } = table;
6440
6461
  const mutsTable = mutTableMap.get(tableName);
6462
+ if (!mutsTable) {
6463
+ // We cannot track mutations on this table because there is no mutations table for it.
6464
+ // This might happen in upgraders that executes before cloud schema is applied.
6465
+ return table;
6466
+ }
6441
6467
  return guardedTable(Object.assign(Object.assign({}, table), { mutate: (req) => {
6442
6468
  var _a, _b, _c;
6443
6469
  const trans = req.trans;
@@ -6896,13 +6922,13 @@ const SERVER_PING_TIMEOUT = 20000;
6896
6922
  const CLIENT_PING_INTERVAL = 30000;
6897
6923
  const FAIL_RETRY_WAIT_TIME = 60000;
6898
6924
  class WSObservable extends Observable$1 {
6899
- constructor(db, rev, realmSetHash, clientIdentity, messageProducer, webSocketStatus, user) {
6900
- super((subscriber) => new WSConnection(db, rev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus));
6925
+ constructor(db, rev, yrev, realmSetHash, clientIdentity, messageProducer, webSocketStatus, user) {
6926
+ super((subscriber) => new WSConnection(db, rev, yrev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus));
6901
6927
  }
6902
6928
  }
6903
6929
  let counter = 0;
6904
6930
  class WSConnection extends Subscription$1 {
6905
- constructor(db, rev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus) {
6931
+ constructor(db, rev, yrev, realmSetHash, clientIdentity, user, subscriber, messageProducer, webSocketStatus) {
6906
6932
  super(() => this.teardown());
6907
6933
  this.id = ++counter;
6908
6934
  this.subscriptions = new Set();
@@ -6911,6 +6937,7 @@ class WSConnection extends Subscription$1 {
6911
6937
  this.db = db;
6912
6938
  this.databaseUrl = db.cloud.options.databaseUrl;
6913
6939
  this.rev = rev;
6940
+ this.yrev = yrev;
6914
6941
  this.realmSetHash = realmSetHash;
6915
6942
  this.clientIdentity = clientIdentity;
6916
6943
  this.user = user;
@@ -6980,6 +7007,11 @@ class WSConnection extends Subscription$1 {
6980
7007
  }
6981
7008
  this.webSocketStatus.next('connecting');
6982
7009
  this.pinger = setInterval(() => __awaiter(this, void 0, void 0, function* () {
7010
+ // setInterval here causes unnecessary pings when server is proved active anyway.
7011
+ // TODO: Use setTimout() here instead. When triggered, check if we really need to ping.
7012
+ // In case we've had server activity, we don't need to ping. Then schedule then next ping
7013
+ // to the time when we should ping next time (based on lastServerActivity + CLIENT_PING_INTERVAL).
7014
+ // Else, ping now and schedule next ping to CLIENT_PING_INTERVAL from now.
6983
7015
  if (this.closed) {
6984
7016
  console.debug('pinger check', this.id, 'CLOSED.');
6985
7017
  this.teardown();
@@ -7026,9 +7058,13 @@ class WSConnection extends Subscription$1 {
7026
7058
  if (this.subscriber.closed)
7027
7059
  return;
7028
7060
  searchParams.set('v', '2');
7029
- searchParams.set('rev', this.rev);
7061
+ if (this.rev)
7062
+ searchParams.set('rev', this.rev);
7063
+ if (this.yrev)
7064
+ searchParams.set('yrev', this.yrev);
7030
7065
  searchParams.set('realmsHash', this.realmSetHash);
7031
7066
  searchParams.set('clientId', this.clientIdentity);
7067
+ searchParams.set('dxcv', this.db.cloud.version);
7032
7068
  if (this.user.accessToken) {
7033
7069
  searchParams.set('token', this.user.accessToken);
7034
7070
  }
@@ -7065,8 +7101,8 @@ class WSConnection extends Subscription$1 {
7065
7101
  }
7066
7102
  }
7067
7103
  }
7068
- else if (msg.type === 'u-ack' || msg.type === 'u-reject' || msg.type === 'u-s' || msg.type === 'in-sync') {
7069
- applyYServerMessages([msg], this.db);
7104
+ else if (msg.type === 'pong') {
7105
+ // Do nothing
7070
7106
  }
7071
7107
  else if (msg.type === 'doc-open') {
7072
7108
  const docCache = DexieYProvider.getDocCache(this.db.dx);
@@ -7075,11 +7111,26 @@ class WSConnection extends Subscription$1 {
7075
7111
  getOpenDocSignal(doc).next(); // Make yHandler reopen the document on server.
7076
7112
  }
7077
7113
  }
7078
- else if (msg.type === 'outdated-server-rev' || msg.type === 'y-complete-sync-done') {
7079
- // Won't happen but need this for typing.
7080
- throw new Error('Outdated server revision or y-complete-sync-done not expected over WebSocket - only in sync using fetch()');
7114
+ 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') {
7115
+ applyYServerMessages([msg], this.db).then((_a) => __awaiter(this, [_a], void 0, function* ({ resyncNeeded, yServerRevision, receivedUntils }) {
7116
+ if (yServerRevision) {
7117
+ yield this.db.$syncState.update('syncState', { yServerRevision: yServerRevision });
7118
+ }
7119
+ if (msg.type === 'u-s' && receivedUntils) {
7120
+ const utbl = getUpdatesTable(this.db, msg.table, msg.prop);
7121
+ if (utbl) {
7122
+ const receivedUntil = receivedUntils[utbl.name];
7123
+ if (receivedUntil) {
7124
+ yield utbl.update(DEXIE_CLOUD_SYNCER_ID, { receivedUntil });
7125
+ }
7126
+ }
7127
+ }
7128
+ if (resyncNeeded) {
7129
+ yield this.db.cloud.sync({ purpose: 'pull', wait: true });
7130
+ }
7131
+ }));
7081
7132
  }
7082
- else if (msg.type !== 'pong') {
7133
+ else {
7083
7134
  // Forward the request to our subscriber, wich is in messageFromServerQueue.ts (via connectWebSocket's subscribe() at the end!)
7084
7135
  this.subscriber.next(msg);
7085
7136
  }
@@ -7212,7 +7263,7 @@ function connectWebSocket(db) {
7212
7263
  // If no new entries, server won't bother the client. If new entries, server sends only those
7213
7264
  // and the baseRev of the last from same client-ID.
7214
7265
  if (userLogin) {
7215
- return new WSObservable(db, db.cloud.persistedSyncState.value.serverRevision, realmSetHash, db.cloud.persistedSyncState.value.clientIdentity, messageProducer, db.cloud.webSocketStatus, userLogin);
7266
+ 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);
7216
7267
  }
7217
7268
  else {
7218
7269
  return from$1([]);
@@ -7342,61 +7393,95 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
7342
7393
  }
7343
7394
 
7344
7395
  const SECONDS = 1000;
7345
- const MINUTES = 60 * SECONDS;
7346
7396
 
7347
7397
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
7348
7398
  let localSyncEventSubscription = null;
7349
- //let syncHandler: ((event: Event) => void) | null = null;
7350
- //let periodicSyncHandler: ((event: Event) => void) | null = null;
7351
7399
  let cancelToken = { cancelled: false };
7352
- let retryHandle = null;
7353
- let retryPurpose = null; // "pull" is superset of "push"
7354
- function syncAndRetry(purpose, retryNum = 1) {
7400
+ let nextRetryTime = 0;
7401
+ let syncStartTime = 0;
7402
+ function syncAndRetry(retryNum = 1) {
7355
7403
  // Use setTimeout() to get onto a clean stack and
7356
7404
  // break free from possible active transaction:
7357
7405
  setTimeout(() => {
7358
- if (retryHandle)
7359
- clearTimeout(retryHandle);
7360
- const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
7361
- retryHandle = null;
7362
- retryPurpose = null;
7406
+ const purpose = pullSignalled ? 'pull' : 'push';
7407
+ syncStartTime = Date.now();
7363
7408
  syncIfPossible(db, cloudOptions, cloudSchema, {
7364
7409
  cancelToken,
7365
7410
  retryImmediatelyOnFetchError: true, // workaround for "net::ERR_NETWORK_CHANGED" in chrome.
7366
- purpose: combPurpose,
7367
- }).catch((e) => {
7368
- console.error('error in syncIfPossible()', e);
7411
+ purpose,
7412
+ }).then(() => {
7369
7413
  if (cancelToken.cancelled) {
7370
7414
  stop();
7371
7415
  }
7372
- else if (retryNum < 3) {
7373
- // Mimic service worker sync event: retry 3 times
7374
- // * first retry after 5 minutes
7375
- // * second retry 15 minutes later
7376
- const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
7377
- const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
7378
- // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
7379
- if (retryHandle)
7380
- clearTimeout(retryHandle);
7381
- retryHandle = handle;
7382
- retryPurpose = combinedPurpose;
7416
+ else {
7417
+ if (pullSignalled || pushSignalled) {
7418
+ // If we have signalled for more sync, do it now.
7419
+ pullSignalled = false;
7420
+ pushSignalled = false;
7421
+ return syncAndRetry();
7422
+ }
7423
+ }
7424
+ ongoingSync = false;
7425
+ nextRetryTime = 0;
7426
+ syncStartTime = 0;
7427
+ }).catch((error) => {
7428
+ console.error('error in syncIfPossible()', error);
7429
+ if (cancelToken.cancelled) {
7430
+ stop();
7431
+ ongoingSync = false;
7432
+ nextRetryTime = 0;
7433
+ syncStartTime = 0;
7434
+ }
7435
+ else if (retryNum < 5) {
7436
+ // Mimic service worker sync event but a bit more eager: retry 4 times
7437
+ // * first retry after 20 seconds
7438
+ // * second retry 40 seconds later
7439
+ // * third retry 5 minutes later
7440
+ // * last retry 15 minutes later
7441
+ const retryIn = [0, 20, 40, 300, 900][retryNum] * SECONDS;
7442
+ nextRetryTime = Date.now() + retryIn;
7443
+ syncStartTime = 0;
7444
+ setTimeout(() => syncAndRetry(retryNum + 1), retryIn);
7445
+ }
7446
+ else {
7447
+ ongoingSync = false;
7448
+ nextRetryTime = 0;
7449
+ syncStartTime = 0;
7383
7450
  }
7384
7451
  });
7385
7452
  }, 0);
7386
7453
  }
7454
+ let pullSignalled = false;
7455
+ let pushSignalled = false;
7456
+ let ongoingSync = false;
7457
+ const consumer = (purpose) => {
7458
+ if (cancelToken.cancelled)
7459
+ return;
7460
+ if (purpose === 'pull') {
7461
+ pullSignalled = true;
7462
+ }
7463
+ if (purpose === 'push') {
7464
+ pushSignalled = true;
7465
+ }
7466
+ if (ongoingSync) {
7467
+ if (nextRetryTime) {
7468
+ console.debug(`Sync is paused until ${new Date(nextRetryTime).toISOString()} due to error in last sync attempt`);
7469
+ }
7470
+ else if (syncStartTime > 0 && Date.now() - syncStartTime > 20 * SECONDS) {
7471
+ console.debug(`An existing sync operation is taking more than 20 seconds. Will resync when done.`);
7472
+ }
7473
+ return;
7474
+ }
7475
+ ongoingSync = true;
7476
+ syncAndRetry();
7477
+ };
7387
7478
  const start = () => {
7388
7479
  // Sync eagerly whenever a change has happened (+ initially when there's no syncState yet)
7389
7480
  // This initial subscribe will also trigger an sync also now.
7390
7481
  console.debug('Starting LocalSyncWorker', db.localSyncEvent['id']);
7391
7482
  localSyncEventSubscription = db.localSyncEvent.subscribe(({ purpose }) => {
7392
- try {
7393
- syncAndRetry(purpose || 'pull');
7394
- }
7395
- catch (err) {
7396
- console.error('What-the....', err);
7397
- }
7483
+ consumer(purpose || 'pull');
7398
7484
  });
7399
- //setTimeout(()=>db.localSyncEvent.next({}), 5000);
7400
7485
  };
7401
7486
  const stop = () => {
7402
7487
  console.debug('Stopping LocalSyncWorker');
@@ -8157,8 +8242,8 @@ function getTiedObjectId(realmId) {
8157
8242
 
8158
8243
  const ydocTriggers = {};
8159
8244
  const middlewares = new WeakMap();
8160
- const txRunner = TriggerRunner("tx");
8161
- const unloadRunner = TriggerRunner("unload");
8245
+ const txRunner = TriggerRunner("tx"); // Trigger registry for transaction completion. Avoids open docs.
8246
+ const unloadRunner = TriggerRunner("unload"); // Trigger registry for unload. Runs when a document is closed.
8162
8247
  function TriggerRunner(name) {
8163
8248
  let triggerExecPromise = null;
8164
8249
  let triggerScheduled = false;
@@ -8168,14 +8253,17 @@ function TriggerRunner(name) {
8168
8253
  for (const { db, parentId, triggers, parentTable, prop, } of registryCopy.values()) {
8169
8254
  const yDoc = DexieYProvider.getOrCreateDocument(db, parentTable, prop, parentId);
8170
8255
  try {
8171
- DexieYProvider.load(yDoc); // If doc is open, this would just be a ++refount
8172
- yield yDoc.whenLoaded; // If doc is loaded, this would resolve immediately
8256
+ const provider = DexieYProvider.load(yDoc); // If doc is open, this would just be a ++refount
8257
+ yield provider.whenLoaded; // If doc is loaded, this would resolve immediately
8173
8258
  for (const trigger of triggers) {
8174
8259
  yield trigger(yDoc, parentId);
8175
8260
  }
8176
8261
  }
8177
8262
  catch (error) {
8178
- console.error(`Error in YDocTrigger ${error}`);
8263
+ if ((error === null || error === void 0 ? void 0 : error.name) === 'AbortError') ;
8264
+ else {
8265
+ console.error(`Error in YDocTrigger ${error}`);
8266
+ }
8179
8267
  }
8180
8268
  finally {
8181
8269
  DexieYProvider.release(yDoc);
@@ -8187,16 +8275,20 @@ function TriggerRunner(name) {
8187
8275
  name,
8188
8276
  run() {
8189
8277
  return __awaiter(this, void 0, void 0, function* () {
8278
+ console.log(`Running trigger (${name})?`, triggerScheduled, registry.size, !!triggerExecPromise);
8190
8279
  if (!triggerScheduled && registry.size > 0) {
8191
8280
  triggerScheduled = true;
8192
8281
  if (triggerExecPromise)
8193
8282
  yield triggerExecPromise.catch(() => { });
8194
8283
  setTimeout(() => {
8195
8284
  // setTimeout() is to escape from Promise.PSD zones and never run within liveQueries or transaction scopes
8285
+ console.log("Running trigger really!", name);
8196
8286
  triggerScheduled = false;
8197
8287
  const registryCopy = registry;
8198
8288
  registry = new Map();
8199
- triggerExecPromise = execute(registryCopy).finally(() => (triggerExecPromise = null));
8289
+ triggerExecPromise = execute(registryCopy).finally(() => {
8290
+ triggerExecPromise = null;
8291
+ });
8200
8292
  }, 0);
8201
8293
  }
8202
8294
  });
@@ -8212,6 +8304,7 @@ function TriggerRunner(name) {
8212
8304
  prop,
8213
8305
  triggers: new Set(),
8214
8306
  };
8307
+ console.log(`Adding trigger ${key}`);
8215
8308
  registry.set(key, entry);
8216
8309
  }
8217
8310
  entry.triggers.add(trigger);
@@ -8351,7 +8444,7 @@ function dexieCloud(dexie) {
8351
8444
  const syncComplete = new Subject();
8352
8445
  dexie.cloud = {
8353
8446
  // @ts-ignore
8354
- version: "4.1.0-beta.43",
8447
+ version: "4.1.0-beta.45",
8355
8448
  options: Object.assign({}, DEFAULT_OPTIONS),
8356
8449
  schema: null,
8357
8450
  get currentUserId() {
@@ -8669,7 +8762,7 @@ function dexieCloud(dexie) {
8669
8762
  }
8670
8763
  }
8671
8764
  // @ts-ignore
8672
- dexieCloud.version = "4.1.0-beta.43";
8765
+ dexieCloud.version = "4.1.0-beta.45";
8673
8766
  Dexie.Cloud = dexieCloud;
8674
8767
 
8675
8768
  export { dexieCloud as default, defineYDocTrigger, dexieCloud, getTiedObjectId, getTiedRealmId, resolveText };