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

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.
@@ -0,0 +1,2 @@
1
+ import { type Table, type YjsDoc } from 'dexie';
2
+ export declare function defineYDocTrigger<T, TKey>(table: Table<T, TKey>, prop: keyof T & string, trigger: (ydoc: YjsDoc, parentId: TKey) => any): void;
@@ -1,3 +1,4 @@
1
1
  import dexieCloudAddon from './dexie-cloud-client';
2
2
  export * from './dexie-cloud-client';
3
+ export { defineYDocTrigger } from './define-ydoc-trigger';
3
4
  export default dexieCloudAddon;
@@ -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.12, Wed Oct 16 2024
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -4947,7 +4947,7 @@
4947
4947
  .toArray();
4948
4948
  }
4949
4949
 
4950
- function $Y(db) {
4950
+ function $Y$1(db) {
4951
4951
  const $Y = db.dx._options.Y;
4952
4952
  if (!$Y)
4953
4953
  throw new Error('Y library not supplied to Dexie constructor');
@@ -4977,7 +4977,7 @@
4977
4977
  for (const table of tablesToSync) {
4978
4978
  if (table.schema.yProps) {
4979
4979
  for (const yProp of table.schema.yProps) {
4980
- const Y = $Y(db); // This is how we retrieve the user-provided Y library
4980
+ const Y = $Y$1(db); // This is how we retrieve the user-provided Y library
4981
4981
  const yTable = db.table(yProp.updatesTable); // the updates-table for this combo of table+propName
4982
4982
  const syncState = (yield yTable.get(DEXIE_CLOUD_SYNCER_ID));
4983
4983
  // unsentFrom = the `i` value of updates that aren't yet sent to server (or at least not acked by the server yet)
@@ -5061,6 +5061,7 @@
5061
5061
  }
5062
5062
 
5063
5063
  function applyYServerMessages(yMessages, db) {
5064
+ var _a;
5064
5065
  return __awaiter(this, void 0, void 0, function* () {
5065
5066
  const result = {};
5066
5067
  for (const m of yMessages) {
@@ -5092,7 +5093,24 @@
5092
5093
  // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765
5093
5094
  console.debug(`Y update rejected. Deleting it.`);
5094
5095
  const utbl = getUpdatesTable(db, m.table, m.prop);
5095
- yield utbl.delete(m.i);
5096
+ // Delete the rejected update and all local updates since (avoid holes in the CRDT)
5097
+ // and destroy it's open document if there is one.
5098
+ const primaryKey = (_a = (yield utbl.get(m.i))) === null || _a === void 0 ? void 0 : _a.k;
5099
+ if (primaryKey != null) {
5100
+ yield db.transaction('rw', utbl, tx => {
5101
+ // @ts-ignore
5102
+ tx.idbtrans._rejecting_y_ypdate = true; // Inform ydoc triggers that we delete because of a rejection and not GC
5103
+ return utbl
5104
+ .where('i')
5105
+ .aboveOrEqual(m.i)
5106
+ .filter(u => Dexie.cmp(u.k, primaryKey) === 0 && ((u.f || 0) & 1) === 1)
5107
+ .delete();
5108
+ });
5109
+ // Destroy active doc
5110
+ const activeDoc = Dexie.DexieYProvider.getDocCache(db.dx).find(m.table, primaryKey, m.prop);
5111
+ if (activeDoc)
5112
+ activeDoc.destroy(); // Destroy the document so that editors don't continue to work on it
5113
+ }
5096
5114
  break;
5097
5115
  }
5098
5116
  case 'in-sync': {
@@ -7807,125 +7825,137 @@
7807
7825
  function createYHandler(db) {
7808
7826
  return (provider) => {
7809
7827
  var _a;
7810
- const awap = getAwarenessLibrary(db);
7811
7828
  const doc = provider.doc;
7812
- const { parentTable, parentId, parentProp, updatesTable } = doc.meta;
7829
+ const { parentTable } = doc.meta || {};
7813
7830
  if (!((_a = db.cloud.schema) === null || _a === void 0 ? void 0 : _a[parentTable].markedForSync)) {
7814
7831
  return; // The table that holds the doc is not marked for sync - leave it to dexie. No syncing, no awareness.
7815
7832
  }
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);
7833
+ let awareness;
7834
+ Object.defineProperty(provider, "awareness", {
7835
+ get() {
7836
+ if (awareness)
7837
+ return awareness;
7838
+ awarenessWeakMap.set(doc, awareness);
7839
+ awareness = createAwareness(db, doc, provider);
7840
+ return awareness;
7841
+ }
7842
+ });
7843
+ };
7844
+ }
7845
+ function createAwareness(db, doc, provider) {
7846
+ const { parentTable, parentId, parentProp, updatesTable } = doc.meta;
7847
+ const awap = getAwarenessLibrary(db);
7848
+ const awareness = new awap.Awareness(doc);
7849
+ awareness.on('update', ({ added, updated, removed }, origin) => {
7850
+ // Send the update
7851
+ const changedClients = added.concat(updated).concat(removed);
7852
+ const user = db.cloud.currentUser.value;
7853
+ if (origin !== 'server' && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7854
+ const update = awap.encodeAwarenessUpdate(awareness, changedClients);
7855
+ db.messageProducer.next({
7856
+ type: 'aware',
7857
+ table: parentTable,
7858
+ prop: parentProp,
7859
+ k: doc.meta.parentId,
7860
+ u: update,
7861
+ });
7862
+ if (provider.destroyed) {
7863
+ // We're called from awareness.on('destroy') that did
7864
+ // removeAwarenessStates.
7865
+ // It's time to also send the doc-close message that dexie-cloud understands
7866
+ // and uses to stop subscribing for updates and awareness updates and brings
7867
+ // down the cached information in memory on the WS connection for this.
7825
7868
  db.messageProducer.next({
7826
- type: 'aware',
7869
+ type: 'doc-close',
7827
7870
  table: parentTable,
7828
7871
  prop: parentProp,
7829
- k: doc.meta.parentId,
7830
- u: update,
7872
+ k: doc.meta.parentId
7831
7873
  });
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
7874
  }
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* () {
7875
+ }
7876
+ });
7877
+ awareness.on('destroy', () => {
7878
+ // Signal to server that this provider is destroyed (the update event will be triggered, which
7879
+ // in turn will trigger db.messageProducer that will send the message to the server if WS is connected)
7880
+ awap.removeAwarenessStates(awareness, [doc.clientID], 'provider destroyed');
7881
+ });
7882
+ // Open the document on the server
7883
+ (() => __awaiter(this, void 0, void 0, function* () {
7884
+ if (provider.destroyed)
7885
+ return;
7886
+ let connected = false;
7887
+ let currentFlowId = 1;
7888
+ const subscription = db.cloud.webSocketStatus.subscribe((wsStatus) => {
7854
7889
  if (provider.destroyed)
7855
7890
  return;
7856
- let connected = false;
7857
- let currentFlowId = 1;
7858
- const subscription = db.cloud.webSocketStatus.subscribe((wsStatus) => {
7859
- if (provider.destroyed)
7891
+ // Keep "connected" state in a variable so we can check it after async operations
7892
+ connected = wsStatus === 'connected';
7893
+ // We are or got connected. Open the document on the server.
7894
+ const user = db.cloud.currentUser.value;
7895
+ if (wsStatus === "connected" && user.isLoggedIn && !isEagerSyncDisabled(db)) {
7896
+ ++currentFlowId;
7897
+ openDocumentOnServer().catch(error => {
7898
+ console.warn(`Error catched in createYHandler.ts: ${error}`);
7899
+ });
7900
+ }
7901
+ });
7902
+ // Wait until WebSocket is connected
7903
+ provider.addCleanupHandler(subscription);
7904
+ /** Sends an 'doc-open' message to server whenever websocket becomes
7905
+ * connected, or if it is already connected.
7906
+ * The flow is aborted in case websocket is disconnected while querying
7907
+ * information required to compute the state vector. Flow is also
7908
+ * aborted in case document or provider has been destroyed during
7909
+ * the async parts of the task.
7910
+ *
7911
+ * The state vector is only computed from the updates that have occured
7912
+ * after the last full sync - which could very often be zero - in which
7913
+ * case no state vector is sent (then the server already knows us by
7914
+ * revision)
7915
+ *
7916
+ * When server gets the doc-open message, it will authorized us for
7917
+ * whether we are allowed to read / write to this document, and then
7918
+ * keep the cached information in memory on the WS connection for this
7919
+ * particular document, as well as subscribe to updates and awareness updates
7920
+ * from other clients on the document.
7921
+ */
7922
+ function openDocumentOnServer(wsStatus) {
7923
+ return __awaiter(this, void 0, void 0, function* () {
7924
+ const myFlow = currentFlowId; // So we can abort when a new flow is started
7925
+ const yTbl = db.table(updatesTable);
7926
+ const syncState = yield yTbl.get(DEXIE_CLOUD_SYNCER_ID);
7927
+ // After every await, check if we still should be working on this task.
7928
+ if (provider.destroyed || currentFlowId !== myFlow || !connected)
7860
7929
  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
- });
7930
+ const receivedUntil = (syncState === null || syncState === void 0 ? void 0 : syncState.receivedUntil) || 0;
7931
+ const docOpenMsg = {
7932
+ type: 'doc-open',
7933
+ table: parentTable,
7934
+ prop: parentProp,
7935
+ k: parentId,
7936
+ serverRev: syncState === null || syncState === void 0 ? void 0 : syncState.serverRev,
7937
+ };
7938
+ const serverUpdatesSinceLastSync = yield yTbl
7939
+ .where('i')
7940
+ .between(receivedUntil, Infinity, false)
7941
+ .filter((update) => Dexie.cmp(update.k, parentId) === 0 && // Only updates for this document
7942
+ ((update.f || 0) & 1) === 0 // Don't include local changes
7943
+ )
7944
+ .toArray();
7945
+ // After every await, check if we still should be working on this task.
7946
+ if (provider.destroyed || currentFlowId !== myFlow || !connected)
7947
+ return;
7948
+ if (serverUpdatesSinceLastSync.length > 0) {
7949
+ const Y = $Y$1(db); // Get the Yjs library from Dexie constructor options
7950
+ const mergedUpdate = Y.mergeUpdatesV2(serverUpdatesSinceLastSync.map((update) => update.u));
7951
+ const stateVector = Y.encodeStateVectorFromUpdateV2(mergedUpdate);
7952
+ docOpenMsg.sv = stateVector;
7870
7953
  }
7954
+ db.messageProducer.next(docOpenMsg);
7871
7955
  });
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
- };
7956
+ }
7957
+ }))();
7958
+ return awareness;
7929
7959
  }
7930
7960
 
7931
7961
  function getTiedRealmId(objectId) {
@@ -7975,7 +8005,7 @@
7975
8005
  const syncComplete = new rxjs.Subject();
7976
8006
  dexie.cloud = {
7977
8007
  // @ts-ignore
7978
- version: "4.1.0-alpha.10",
8008
+ version: "4.1.0-alpha.12",
7979
8009
  options: Object.assign({}, DEFAULT_OPTIONS),
7980
8010
  schema: null,
7981
8011
  get currentUserId() {
@@ -8277,10 +8307,163 @@
8277
8307
  }
8278
8308
  }
8279
8309
  // @ts-ignore
8280
- dexieCloud.version = "4.1.0-alpha.10";
8310
+ dexieCloud.version = "4.1.0-alpha.12";
8281
8311
  Dexie.Cloud = dexieCloud;
8282
8312
 
8313
+ const ydocTriggers = {};
8314
+ const docIsAlreadyHooked = new WeakSet();
8315
+ const middlewares = new WeakMap();
8316
+ const createMiddleware = (db) => ({
8317
+ stack: 'dbcore',
8318
+ level: 10,
8319
+ name: 'yTriggerMiddleware',
8320
+ create: (down) => {
8321
+ return Object.assign(Object.assign({}, down), { transaction: (stores, mode, options) => {
8322
+ const idbtrans = down.transaction(stores, mode, options);
8323
+ idbtrans.addEventListener('complete', onTransactionCommitted);
8324
+ return idbtrans;
8325
+ }, table: (tblName) => {
8326
+ const coreTable = down.table(tblName);
8327
+ const triggerSpec = ydocTriggers[tblName];
8328
+ if (!triggerSpec)
8329
+ return coreTable;
8330
+ const { trigger, parentTable, prop } = triggerSpec;
8331
+ return Object.assign(Object.assign({}, coreTable), { mutate(req) {
8332
+ switch (req.type) {
8333
+ case 'add': {
8334
+ for (const obj of req.values) {
8335
+ const primaryKey = coreTable.schema.primaryKey.extractKey(obj);
8336
+ const doc = Dexie.DexieYProvider.getDocCache(db).find(parentTable, primaryKey, prop);
8337
+ if (doc) {
8338
+ if (!docIsAlreadyHooked.has(doc)) {
8339
+ hookToDoc(doc, primaryKey, trigger);
8340
+ docIsAlreadyHooked.add(doc);
8341
+ }
8342
+ }
8343
+ else {
8344
+ enqueueTrigger(tblName, primaryKey, trigger);
8345
+ }
8346
+ }
8347
+ break;
8348
+ }
8349
+ case 'delete':
8350
+ // @ts-ignore
8351
+ if (req.trans._rejecting_y_ypdate) {
8352
+ // The deletion came from a rejection, not garbage collection.
8353
+ // When that happens, let the triggers run to compute new values
8354
+ // based on the deleted updates.
8355
+ coreTable
8356
+ .getMany({
8357
+ keys: req.keys,
8358
+ trans: req.trans,
8359
+ cache: 'immutable',
8360
+ })
8361
+ .then((updates) => {
8362
+ const keySet = new Dexie.RangeSet();
8363
+ for (const { k } of updates) {
8364
+ keySet.addKey(k);
8365
+ }
8366
+ for (const key of keySet) {
8367
+ enqueueTrigger(tblName, key, trigger);
8368
+ }
8369
+ });
8370
+ }
8371
+ break;
8372
+ }
8373
+ return coreTable.mutate(req);
8374
+ } });
8375
+ } });
8376
+ },
8377
+ });
8378
+ let triggerExecPromise = null;
8379
+ let triggerScheduled = false;
8380
+ let scheduledTriggers = [];
8381
+ function $Y(db) {
8382
+ const $Y = db._options.Y;
8383
+ if (!$Y)
8384
+ throw new Error('Y library not supplied to Dexie constructor');
8385
+ return $Y;
8386
+ }
8387
+ function executeTriggers(triggersToRun) {
8388
+ return __awaiter(this, void 0, void 0, function* () {
8389
+ for (const { db, parentId, trigger, updatesTable } of triggersToRun) {
8390
+ // Load entire document into an Y.Doc instance:
8391
+ const updates = yield db
8392
+ .table(updatesTable)
8393
+ .where({ k: parentId })
8394
+ .toArray();
8395
+ const Y = $Y(db);
8396
+ const yDoc = new Y.Doc();
8397
+ for (const update of updates) {
8398
+ Y.applyUpdateV2(yDoc, update);
8399
+ }
8400
+ try {
8401
+ yield trigger(yDoc, parentId);
8402
+ }
8403
+ catch (error) {
8404
+ console.error(`Error in YDocTrigger ${error}`);
8405
+ }
8406
+ }
8407
+ });
8408
+ }
8409
+ function enqueueTrigger(updatesTable, parentId, trigger) {
8410
+ var _a;
8411
+ ((_a = scheduledTriggers[updatesTable]) !== null && _a !== void 0 ? _a : (scheduledTriggers[updatesTable] = [])).push({
8412
+ parentId,
8413
+ trigger,
8414
+ });
8415
+ }
8416
+ function onTransactionCommitted() {
8417
+ return __awaiter(this, void 0, void 0, function* () {
8418
+ if (!triggerScheduled && scheduledTriggers.length > 0) {
8419
+ triggerScheduled = true;
8420
+ if (triggerExecPromise)
8421
+ yield triggerExecPromise.catch(() => { });
8422
+ setTimeout(() => {
8423
+ // setTimeout() is to escape from Promise.PSD zones and never run within liveQueries or transaction scopes
8424
+ triggerScheduled = false;
8425
+ const triggersToRun = scheduledTriggers;
8426
+ scheduledTriggers = [];
8427
+ triggerExecPromise = executeTriggers(triggersToRun).finally(() => (triggerExecPromise = null));
8428
+ }, 0);
8429
+ }
8430
+ });
8431
+ }
8432
+ function hookToDoc(doc, parentId, trigger) {
8433
+ // From now on, keep listening to doc updates and execute the trigger when it happens there instead
8434
+ doc.on('updateV2', (update, origin) => {
8435
+ //Dexie.ignoreTransaction(()=>{
8436
+ trigger(doc, parentId);
8437
+ //});
8438
+ });
8439
+ /*
8440
+ NOT NEEDED because DexieYProvider's docCache will also listen to destroy and remove it from its cache:
8441
+ doc.on('destroy', ()=>{
8442
+ docIsAlreadyHooked.delete(doc);
8443
+ })
8444
+ */
8445
+ }
8446
+ function defineYDocTrigger(table, prop, trigger) {
8447
+ var _a, _b;
8448
+ const updatesTable = (_b = (_a = table.schema.yProps) === null || _a === void 0 ? void 0 : _a.find((p) => p.prop === prop)) === null || _b === void 0 ? void 0 : _b.updatesTable;
8449
+ if (!updatesTable)
8450
+ throw new Error(`Table ${table.name} does not have a Yjs property named ${prop}`);
8451
+ ydocTriggers[updatesTable] = {
8452
+ trigger,
8453
+ parentTable: table.name,
8454
+ prop,
8455
+ };
8456
+ const db = table.db._novip;
8457
+ let mw = middlewares.get(db);
8458
+ if (!mw) {
8459
+ mw = createMiddleware(db);
8460
+ middlewares.set(db, mw);
8461
+ }
8462
+ db.use(mw);
8463
+ }
8464
+
8283
8465
  exports.default = dexieCloud;
8466
+ exports.defineYDocTrigger = defineYDocTrigger;
8284
8467
  exports.dexieCloud = dexieCloud;
8285
8468
  exports.getTiedObjectId = getTiedObjectId;
8286
8469
  exports.getTiedRealmId = getTiedRealmId;