dexie-cloud-addon 4.1.0-alpha.10 → 4.1.0-alpha.13

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-alpha.10, Wed Oct 16 2024
11
+ * Version 4.1.0-alpha.13, Thu Oct 17 2024
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -4755,6 +4755,7 @@
4755
4755
  }
4756
4756
 
4757
4757
  function applyYServerMessages(yMessages, db) {
4758
+ var _a;
4758
4759
  return __awaiter(this, void 0, void 0, function* () {
4759
4760
  const result = {};
4760
4761
  for (const m of yMessages) {
@@ -4786,7 +4787,24 @@
4786
4787
  // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
4787
4788
  console.debug(`Y update rejected. Deleting it.`);
4788
4789
  const utbl = getUpdatesTable(db, m.table, m.prop);
4789
- yield utbl.delete(m.i);
4790
+ // Delete the rejected update and all local updates since (avoid holes in the CRDT)
4791
+ // and destroy it's open document if there is one.
4792
+ const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
4793
+ if (primaryKey != null) {
4794
+ yield db.transaction('rw', utbl, tx => {
4795
+ // @ts-ignore
4796
+ tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
4797
+ return utbl
4798
+ .where('i')
4799
+ .aboveOrEqual(m.i)
4800
+ .filter(u => Dexie.cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
4801
+ .delete();
4802
+ });
4803
+ // Destroy active doc
4804
+ const activeDoc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
4805
+ if (activeDoc)
4806
+ activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
4807
+ }
4790
4808
  break;
4791
4809
  }
4792
4810
  case 'in-sync': {
@@ -6306,8 +6324,7 @@
6306
6324
  outstandingTransactions.next(outstandingTransactions.value);
6307
6325
  };
6308
6326
  const txComplete = () => {
6309
- if (tx.mutationsAdded &&
6310
- !isEagerSyncDisabled(db)) {
6327
+ if (tx.mutationsAdded && !isEagerSyncDisabled(db)) {
6311
6328
  triggerSync(db, 'push');
6312
6329
  }
6313
6330
  removeTransaction();
@@ -6389,26 +6406,107 @@
6389
6406
  : mutateAndLog(req);
6390
6407
  } }));
6391
6408
  function mutateAndLog(req) {
6409
+ var _a, _b;
6392
6410
  const trans = req.trans;
6393
- trans.mutationsAdded = true;
6411
+ const unsyncedProps = (_b = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.unsyncedProperties) === null || _b === void 0 ? void 0 : _b[tableName];
6394
6412
  const { txid, currentUser: { userId }, } = trans;
6395
6413
  const { type } = req;
6396
6414
  const opNo = ++trans.opCount;
6415
+ function stripChangeSpec(changeSpec) {
6416
+ if (!unsyncedProps)
6417
+ return changeSpec;
6418
+ let rv = changeSpec;
6419
+ for (const keyPath of Object.keys(changeSpec)) {
6420
+ if (unsyncedProps.some((p) => keyPath === p || keyPath.startsWith(p + '.'))) {
6421
+ if (rv === changeSpec)
6422
+ rv = Object.assign({}, changeSpec); // clone on demand
6423
+ delete rv[keyPath];
6424
+ }
6425
+ }
6426
+ return rv;
6427
+ }
6397
6428
  return table.mutate(req).then((res) => {
6429
+ var _a;
6398
6430
  const { numFailures: hasFailures, failures } = res;
6399
6431
  let keys = type === 'delete' ? req.keys : res.results;
6400
6432
  let values = 'values' in req ? req.values : [];
6401
- let updates = 'updates' in req && req.updates;
6433
+ let changeSpec = 'changeSpec' in req ? req.changeSpec : undefined;
6434
+ let updates = 'updates' in req ? req.updates : undefined;
6402
6435
  if (hasFailures) {
6403
6436
  keys = keys.filter((_, idx) => !failures[idx]);
6404
6437
  values = values.filter((_, idx) => !failures[idx]);
6405
6438
  }
6439
+ if (unsyncedProps) {
6440
+ // Filter out unsynced properties
6441
+ values = values.map((value) => {
6442
+ const newValue = Object.assign({}, value);
6443
+ for (const prop of unsyncedProps) {
6444
+ delete newValue[prop];
6445
+ }
6446
+ return newValue;
6447
+ });
6448
+ if (changeSpec) {
6449
+ // modify operation with criteria and changeSpec.
6450
+ // We must strip out unsynced properties from changeSpec.
6451
+ // We deal with criteria later.
6452
+ changeSpec = stripChangeSpec(changeSpec);
6453
+ if (Object.keys(changeSpec).length === 0) {
6454
+ // Nothing to change on server
6455
+ return res;
6456
+ }
6457
+ }
6458
+ if (updates) {
6459
+ let strippedChangeSpecs = updates.changeSpecs.map(stripChangeSpec);
6460
+ let newUpdates = {
6461
+ keys: [],
6462
+ changeSpecs: [],
6463
+ };
6464
+ const validKeys = new Dexie.RangeSet();
6465
+ let anyChangeSpecBecameEmpty = false;
6466
+ for (let i = 0, l = strippedChangeSpecs.length; i < l; ++i) {
6467
+ if (Object.keys(strippedChangeSpecs[i]).length > 0) {
6468
+ newUpdates.keys.push(updates.keys[i]);
6469
+ newUpdates.changeSpecs.push(strippedChangeSpecs[i]);
6470
+ validKeys.addKey(updates.keys[i]);
6471
+ }
6472
+ else {
6473
+ anyChangeSpecBecameEmpty = true;
6474
+ }
6475
+ }
6476
+ updates = newUpdates;
6477
+ if (anyChangeSpecBecameEmpty) {
6478
+ // Some keys were stripped. We must also strip them from keys and values
6479
+ let newKeys = [];
6480
+ let newValues = [];
6481
+ for (let i = 0, l = keys.length; i < l; ++i) {
6482
+ if (validKeys.hasKey(keys[i])) {
6483
+ newKeys.push(keys[i]);
6484
+ newValues.push(values[i]);
6485
+ }
6486
+ }
6487
+ keys = newKeys;
6488
+ values = newValues;
6489
+ }
6490
+ }
6491
+ }
6406
6492
  const ts = Date.now();
6407
6493
  // Canonicalize req.criteria.index to null if it's on the primary key.
6408
- const criteria = 'criteria' in req && req.criteria
6494
+ let criteria = 'criteria' in req && req.criteria
6409
6495
  ? Object.assign(Object.assign({}, req.criteria), { index: req.criteria.index === schema.primaryKey.keyPath // Use null to inform server that criteria is on primary key
6410
6496
  ? null // This will disable the server from trying to log consistent operations where it shouldnt.
6411
6497
  : req.criteria.index }) : undefined;
6498
+ if (unsyncedProps && (criteria === null || criteria === void 0 ? void 0 : criteria.index)) {
6499
+ const keyPaths = (_a = schema.indexes.find((idx) => idx.name === criteria.index)) === null || _a === void 0 ? void 0 : _a.keyPath;
6500
+ const involvedProps = keyPaths
6501
+ ? typeof keyPaths === 'string'
6502
+ ? [keyPaths]
6503
+ : keyPaths
6504
+ : [];
6505
+ if (involvedProps.some((p) => unsyncedProps === null || unsyncedProps === void 0 ? void 0 : unsyncedProps.includes(p))) {
6506
+ // Don't log criteria on unsynced properties as the server could not test them.
6507
+ criteria = undefined;
6508
+ }
6509
+ }
6412
6510
  const mut = req.type === 'delete'
6413
6511
  ? {
6414
6512
  type: 'delete',
@@ -6429,7 +6527,7 @@
6429
6527
  userId,
6430
6528
  values,
6431
6529
  }
6432
- : criteria && req.changeSpec
6530
+ : criteria && changeSpec
6433
6531
  ? {
6434
6532
  // Common changeSpec for all keys
6435
6533
  type: 'modify',
@@ -6437,30 +6535,41 @@
6437
6535
  opNo,
6438
6536
  keys,
6439
6537
  criteria,
6440
- changeSpec: req.changeSpec,
6538
+ changeSpec,
6441
6539
  txid,
6442
6540
  userId,
6443
6541
  }
6444
- : updates
6542
+ : changeSpec
6445
6543
  ? {
6446
- // One changeSpec per key
6544
+ // In case criteria involved an unsynced property, we go for keys instead.
6447
6545
  type: 'update',
6448
6546
  ts,
6449
6547
  opNo,
6450
- keys: updates.keys,
6451
- changeSpecs: updates.changeSpecs,
6452
- txid,
6453
- userId,
6454
- }
6455
- : {
6456
- type: 'upsert',
6457
- ts,
6458
- opNo,
6459
6548
  keys,
6460
- values,
6549
+ changeSpecs: keys.map(() => changeSpec),
6461
6550
  txid,
6462
6551
  userId,
6463
- };
6552
+ }
6553
+ : updates
6554
+ ? {
6555
+ // One changeSpec per key
6556
+ type: 'update',
6557
+ ts,
6558
+ opNo,
6559
+ keys: updates.keys,
6560
+ changeSpecs: updates.changeSpecs,
6561
+ txid,
6562
+ userId,
6563
+ }
6564
+ : {
6565
+ type: 'upsert',
6566
+ ts,
6567
+ opNo,
6568
+ keys,
6569
+ values,
6570
+ txid,
6571
+ userId,
6572
+ };
6464
6573
  if ('isAdditionalChunk' in req && req.isAdditionalChunk) {
6465
6574
  mut.isAdditionalChunk = true;
6466
6575
  }
@@ -7807,125 +7916,137 @@
7807
7916
  function createYHandler(db) {
7808
7917
  return (provider) => {
7809
7918
  var _a;
7810
- const awap = getAwarenessLibrary(db);
7811
7919
  const doc = provider.doc;
7812
- const { parentTable, parentId, parentProp, updatesTable } = doc.meta;
7920
+ const { parentTable } = doc.meta || {};
7813
7921
  if (!((_a = db.cloud.schema) === null || _a === void 0 ? void 0 : _a[parentTable].markedForSync)) {
7814
7922
  return; // The table that holds the doc is not marked for sync - leave it to dexie. No syncing, no awareness.
7815
7923
  }
7816
- let awareness = new awap.Awareness(doc);
7817
- awarenessWeakMap.set(doc, awareness);
7818
- provider.awareness = awareness;
7819
- awareness.on('update', ({ added, updated, removed }, origin) => {
7820
- // Send the update
7821
- const changedClients = added.concat(updated).concat(removed);
7822
- const user = db.cloud.currentUser.value;
7823
- if (origin !== 'server' && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7824
- const update = awap.encodeAwarenessUpdate(awareness, changedClients);
7924
+ let awareness;
7925
+ Object.defineProperty(provider, "awareness", {
7926
+ get() {
7927
+ if (awareness)
7928
+ return awareness;
7929
+ awarenessWeakMap.set(doc, awareness);
7930
+ awareness = createAwareness(db, doc, provider);
7931
+ return awareness;
7932
+ }
7933
+ });
7934
+ };
7935
+ }
7936
+ function createAwareness(db, doc, provider) {
7937
+ const { parentTable, parentId, parentProp, updatesTable } = doc.meta;
7938
+ const awap = getAwarenessLibrary(db);
7939
+ const awareness = new awap.Awareness(doc);
7940
+ awareness.on('update', ({ added, updated, removed }, origin) => {
7941
+ // Send the update
7942
+ const changedClients = added.concat(updated).concat(removed);
7943
+ const user = db.cloud.currentUser.value;
7944
+ if (origin !== 'server' && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7945
+ const update = awap.encodeAwarenessUpdate(awareness, changedClients);
7946
+ db.messageProducer.next({
7947
+ type: 'aware',
7948
+ table: parentTable,
7949
+ prop: parentProp,
7950
+ k: doc.meta.parentId,
7951
+ u: update,
7952
+ });
7953
+ if (provider.destroyed) {
7954
+ // We're called from awareness.on('destroy') that did
7955
+ // removeAwarenessStates.
7956
+ // It's time to also send the doc-close message that dexie-cloud understands
7957
+ // and uses to stop subscribing for updates and awareness updates and brings
7958
+ // down the cached information in memory on the WS connection for this.
7825
7959
  db.messageProducer.next({
7826
- type: 'aware',
7960
+ type: 'doc-close',
7827
7961
  table: parentTable,
7828
7962
  prop: parentProp,
7829
- k: doc.meta.parentId,
7830
- u: update,
7963
+ k: doc.meta.parentId
7831
7964
  });
7832
- if (provider.destroyed) {
7833
- // We're called from awareness.on('destroy') that did
7834
- // removeAwarenessStates.
7835
- // It's time to also send the doc-close message that dexie-cloud understands
7836
- // and uses to stop subscribing for updates and awareness updates and brings
7837
- // down the cached information in memory on the WS connection for this.
7838
- db.messageProducer.next({
7839
- type: 'doc-close',
7840
- table: parentTable,
7841
- prop: parentProp,
7842
- k: doc.meta.parentId
7843
- });
7844
- }
7845
7965
  }
7846
- });
7847
- awareness.on('destroy', () => {
7848
- // Signal to server that this provider is destroyed (the update event will be triggered, which
7849
- // in turn will trigger db.messageProducer that will send the message to the server if WS is connected)
7850
- awap.removeAwarenessStates(awareness, [doc.clientID], 'provider destroyed');
7851
- });
7852
- // Now wait til document is loaded and then open the document on the server
7853
- provider.on('load', () => __awaiter(this, void 0, void 0, function* () {
7966
+ }
7967
+ });
7968
+ awareness.on('destroy', () => {
7969
+ // Signal to server that this provider is destroyed (the update event will be triggered, which
7970
+ // in turn will trigger db.messageProducer that will send the message to the server if WS is connected)
7971
+ awap.removeAwarenessStates(awareness, [doc.clientID], 'provider destroyed');
7972
+ });
7973
+ // Open the document on the server
7974
+ (() => __awaiter(this, void 0, void 0, function* () {
7975
+ if (provider.destroyed)
7976
+ return;
7977
+ let connected = false;
7978
+ let currentFlowId = 1;
7979
+ const subscription = db.cloud.webSocketStatus.subscribe((wsStatus) => {
7854
7980
  if (provider.destroyed)
7855
7981
  return;
7856
- let connected = false;
7857
- let currentFlowId = 1;
7858
- const subscription = db.cloud.webSocketStatus.subscribe((wsStatus) => {
7859
- if (provider.destroyed)
7982
+ // Keep "connected" state in a variable so we can check it after async operations
7983
+ connected = wsStatus === 'connected';
7984
+ // We are or got connected. Open the document on the server.
7985
+ const user = db.cloud.currentUser.value;
7986
+ if (wsStatus === "connected" && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7987
+ ++currentFlowId;
7988
+ openDocumentOnServer().catch(error => {
7989
+ console.warn(`Error catched in createYHandler.ts: ${error}`);
7990
+ });
7991
+ }
7992
+ });
7993
+ // Wait until WebSocket is connected
7994
+ provider.addCleanupHandler(subscription);
7995
+ /** Sends an 'doc-open' message to server whenever websocket becomes
7996
+ * connected, or if it is already connected.
7997
+ * The flow is aborted in case websocket is disconnected while querying
7998
+ * information required to compute the state vector. Flow is also
7999
+ * aborted in case document or provider has been destroyed during
8000
+ * the async parts of the task.
8001
+ *
8002
+ * The state vector is only computed from the updates that have occured
8003
+ * after the last full sync - which could very often be zero - in which
8004
+ * case no state vector is sent (then the server already knows us by
8005
+ * revision)
8006
+ *
8007
+ * When server gets the doc-open message, it will authorized us for
8008
+ * whether we are allowed to read / write to this document, and then
8009
+ * keep the cached information in memory on the WS connection for this
8010
+ * particular document, as well as subscribe to updates and awareness updates
8011
+ * from other clients on the document.
8012
+ */
8013
+ function openDocumentOnServer(wsStatus) {
8014
+ return __awaiter(this, void 0, void 0, function* () {
8015
+ const myFlow = currentFlowId; // So we can abort when a new flow is started
8016
+ const yTbl = db.table(updatesTable);
8017
+ const syncState = yield yTbl.get(DEXIE_CLOUD_SYNCER_ID);
8018
+ // After every await, check if we still should be working on this task.
8019
+ if (provider.destroyed || currentFlowId !== myFlow || !connected)
7860
8020
  return;
7861
- // Keep "connected" state in a variable so we can check it after async operations
7862
- connected = wsStatus === 'connected';
7863
- // We are or got connected. Open the document on the server.
7864
- const user = db.cloud.currentUser.value;
7865
- if (wsStatus === "connected" && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7866
- ++currentFlowId;
7867
- openDocumentOnServer().catch(error => {
7868
- console.warn(`Error catched in createYHandler.ts: ${error}`);
7869
- });
8021
+ const receivedUntil = (syncState === null || syncState === void 0 ? void 0 : syncState.receivedUntil) || 0;
8022
+ const docOpenMsg = {
8023
+ type: 'doc-open',
8024
+ table: parentTable,
8025
+ prop: parentProp,
8026
+ k: parentId,
8027
+ serverRev: syncState === null || syncState === void 0 ? void 0 : syncState.serverRev,
8028
+ };
8029
+ const serverUpdatesSinceLastSync = yield yTbl
8030
+ .where('i')
8031
+ .between(receivedUntil, Infinity, false)
8032
+ .filter((update) => Dexie.cmp(update.k, parentId) === 0 && // Only updates for this document
8033
+ ((update.f || 0) & 1) === 0 // Don't include local changes
8034
+ )
8035
+ .toArray();
8036
+ // After every await, check if we still should be working on this task.
8037
+ if (provider.destroyed || currentFlowId !== myFlow || !connected)
8038
+ return;
8039
+ if (serverUpdatesSinceLastSync.length > 0) {
8040
+ const Y = $Y(db); // Get the Yjs library from Dexie constructor options
8041
+ const mergedUpdate = Y.mergeUpdatesV2(serverUpdatesSinceLastSync.map((update) => update.u));
8042
+ const stateVector = Y.encodeStateVectorFromUpdateV2(mergedUpdate);
8043
+ docOpenMsg.sv = stateVector;
7870
8044
  }
8045
+ db.messageProducer.next(docOpenMsg);
7871
8046
  });
7872
- // Wait until WebSocket is connected
7873
- provider.addCleanupHandler(subscription);
7874
- /** Sends an 'doc-open' message to server whenever websocket becomes
7875
- * connected, or if it is already connected.
7876
- * The flow is aborted in case websocket is disconnected while querying
7877
- * information required to compute the state vector. Flow is also
7878
- * aborted in case document or provider has been destroyed during
7879
- * the async parts of the task.
7880
- *
7881
- * The state vector is only computed from the updates that have occured
7882
- * after the last full sync - which could very often be zero - in which
7883
- * case no state vector is sent (then the server already knows us by
7884
- * revision)
7885
- *
7886
- * When server gets the doc-open message, it will authorized us for
7887
- * whether we are allowed to read / write to this document, and then
7888
- * keep the cached information in memory on the WS connection for this
7889
- * particular document, as well as subscribe to updates and awareness updates
7890
- * from other clients on the document.
7891
- */
7892
- function openDocumentOnServer(wsStatus) {
7893
- return __awaiter(this, void 0, void 0, function* () {
7894
- const myFlow = currentFlowId; // So we can abort when a new flow is started
7895
- const yTbl = db.table(updatesTable);
7896
- const syncState = yield yTbl.get(DEXIE_CLOUD_SYNCER_ID);
7897
- // After every await, check if we still should be working on this task.
7898
- if (provider.destroyed || currentFlowId !== myFlow || !connected)
7899
- return;
7900
- const receivedUntil = (syncState === null || syncState === void 0 ? void 0 : syncState.receivedUntil) || 0;
7901
- const docOpenMsg = {
7902
- type: 'doc-open',
7903
- table: parentTable,
7904
- prop: parentProp,
7905
- k: parentId,
7906
- serverRev: syncState === null || syncState === void 0 ? void 0 : syncState.serverRev,
7907
- };
7908
- const serverUpdatesSinceLastSync = yield yTbl
7909
- .where('i')
7910
- .between(receivedUntil, Infinity, false)
7911
- .filter((update) => Dexie.cmp(update.k, parentId) === 0 && // Only updates for this document
7912
- ((update.f || 0) & 1) === 0 // Don't include local changes
7913
- )
7914
- .toArray();
7915
- // After every await, check if we still should be working on this task.
7916
- if (provider.destroyed || currentFlowId !== myFlow || !connected)
7917
- return;
7918
- if (serverUpdatesSinceLastSync.length > 0) {
7919
- const Y = $Y(db); // Get the Yjs library from Dexie constructor options
7920
- const mergedUpdate = Y.mergeUpdatesV2(serverUpdatesSinceLastSync.map((update) => update.u));
7921
- const stateVector = Y.encodeStateVectorFromUpdateV2(mergedUpdate);
7922
- docOpenMsg.sv = stateVector;
7923
- }
7924
- db.messageProducer.next(docOpenMsg);
7925
- });
7926
- }
7927
- }));
7928
- };
8047
+ }
8048
+ }))();
8049
+ return awareness;
7929
8050
  }
7930
8051
 
7931
8052
  const DEFAULT_OPTIONS = {
@@ -7968,7 +8089,7 @@
7968
8089
  const syncComplete = new rxjs.Subject();
7969
8090
  dexie.cloud = {
7970
8091
  // @ts-ignore
7971
- version: "4.1.0-alpha.10",
8092
+ version: "4.1.0-alpha.13",
7972
8093
  options: Object.assign({}, DEFAULT_OPTIONS),
7973
8094
  schema: null,
7974
8095
  get currentUserId() {
@@ -8270,7 +8391,7 @@
8270
8391
  }
8271
8392
  }
8272
8393
  // @ts-ignore
8273
- dexieCloud.version = "4.1.0-alpha.10";
8394
+ dexieCloud.version = "4.1.0-alpha.13";
8274
8395
  Dexie.Cloud = dexieCloud;
8275
8396
 
8276
8397
  // In case the SW lives for a while, let it reuse already opened connections: