@dittolive/ditto 4.1.0 → 4.2.0

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.
package/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  *Ditto is a cross platform SDK that allows mobile, web, and IoT apps to sync
4
4
  with and even without connectivity.*
5
5
 
6
- Version: **4.1.0**
6
+ Version: **4.2.0**
7
7
 
8
8
  Please visit [ditto.live](https://ditto.live) for more info as well as the
9
- [API Reference](https://software.ditto.live/js/Ditto/4.1.0/api-reference/) for this particular version.
9
+ [API Reference](https://software.ditto.live/js/Ditto/4.2.0/api-reference/) for this particular version.
10
10
 
11
11
  --------------------------------------------------------------------------------
12
12
 
package/node/ditto.cjs.js CHANGED
@@ -289,9 +289,7 @@ function ditto_add_internal_ble_server_transport(...args) { return ditto.ditto_a
289
289
  function ditto_add_internal_mdns_client_transport(...args) { return ditto.ditto_add_internal_mdns_client_transport(...args) }
290
290
  function ditto_add_internal_mdns_server_transport(...args) { return ditto.ditto_add_internal_mdns_server_transport(...args) }
291
291
  function ditto_add_multicast_transport(...args) { return ditto.ditto_add_multicast_transport(...args) }
292
- function ditto_add_static_tcp_client(...args) { return ditto.ditto_add_static_tcp_client(...args) }
293
292
  function ditto_add_subscription(...args) { return ditto.ditto_add_subscription(...args) }
294
- function ditto_add_websocket_client(...args) { return ditto.ditto_add_websocket_client(...args) }
295
293
  function ditto_auth_client_get_site_id(...args) { return ditto.ditto_auth_client_get_site_id(...args) }
296
294
  function ditto_auth_client_is_web_valid(...args) { return ditto.ditto_auth_client_is_web_valid(...args) }
297
295
  function ditto_auth_client_is_x509_valid(...args) { return ditto.ditto_auth_client_is_x509_valid(...args) }
@@ -326,7 +324,9 @@ function ditto_document_set_cbor_with_timestamp(...args) { return ditto.ditto_do
326
324
  function ditto_documents_hash(...args) { return ditto.ditto_documents_hash(...args) }
327
325
  function ditto_documents_hash_mnemonic(...args) { return ditto.ditto_documents_hash_mnemonic(...args) }
328
326
  function ditto_error_message(...args) { return ditto.ditto_error_message(...args) }
327
+ function ditto_experimental_add_dql_subscription(...args) { return ditto.ditto_experimental_add_dql_subscription(...args) }
329
328
  function ditto_experimental_exec_query_str(...args) { return ditto.ditto_experimental_exec_query_str(...args) }
329
+ function ditto_experimental_remove_dql_subscription(...args) { return ditto.ditto_experimental_remove_dql_subscription(...args) }
330
330
  function ditto_free(...args) { return ditto.ditto_free(...args) }
331
331
  function ditto_free_attachment_handle(...args) { return ditto.ditto_free_attachment_handle(...args) }
332
332
  function ditto_get_collection_names(...args) { return ditto.ditto_get_collection_names(...args) }
@@ -369,6 +369,8 @@ function ditto_resolve_attachment(...args) { return ditto.ditto_resolve_attachme
369
369
  function ditto_run_garbage_collection(...args) { return ditto.ditto_run_garbage_collection(...args) }
370
370
  function ditto_set_connect_retry_interval(...args) { return ditto.ditto_set_connect_retry_interval(...args) }
371
371
  function ditto_set_device_name(...args) { return ditto.ditto_set_device_name(...args) }
372
+ function ditto_set_static_tcp_clients(...args) { return ditto.ditto_set_static_tcp_clients(...args) }
373
+ function ditto_set_static_websocket_clients(...args) { return ditto.ditto_set_static_websocket_clients(...args) }
372
374
  function ditto_set_sync_group(...args) { return ditto.ditto_set_sync_group(...args) }
373
375
  function ditto_shutdown(...args) { return ditto.ditto_shutdown(...args) }
374
376
  function ditto_start_http_server(...args) { return ditto.ditto_start_http_server(...args) }
@@ -379,10 +381,12 @@ function ditto_validate_document_id(...args) { return ditto.ditto_validate_docum
379
381
  function ditto_write_transaction(...args) { return ditto.ditto_write_transaction(...args) }
380
382
  function ditto_write_transaction_commit(...args) { return ditto.ditto_write_transaction_commit(...args) }
381
383
  function ditto_write_transaction_rollback(...args) { return ditto.ditto_write_transaction_rollback(...args) }
384
+ function getDeadlockTimeout$1(...args) { return ditto.getDeadlockTimeout(...args) }
382
385
  function jsDocsToCDocs(...args) { return ditto.jsDocsToCDocs(...args) }
383
386
  function mdns_client_free_handle(...args) { return ditto.mdns_client_free_handle(...args) }
384
387
  function mdns_server_free_handle(...args) { return ditto.mdns_server_free_handle(...args) }
385
388
  function refCStringToString(...args) { return ditto.refCStringToString(...args) }
389
+ function setDeadlockTimeout$1(...args) { return ditto.setDeadlockTimeout(...args) }
386
390
  function static_tcp_client_free_handle(...args) { return ditto.static_tcp_client_free_handle(...args) }
387
391
  function uninitialized_ditto_make(...args) { return ditto.uninitialized_ditto_make(...args) }
388
392
  function verify_license(...args) { return ditto.verify_license(...args) }
@@ -404,9 +408,9 @@ var DittoCRDTType;
404
408
  DittoCRDTType[DittoCRDTType["rga"] = 3] = "rga";
405
409
  DittoCRDTType[DittoCRDTType["rwMap"] = 4] = "rwMap";
406
410
  })(DittoCRDTType || (DittoCRDTType = {}));
407
- // ------------------------------------------------------- Linux BLE Hack ------
411
+ // -------------------------------------- Linux & Windows Transports Hack ------
408
412
  // HACK: quick and dirty, wrap the internal BLE functions which
409
- // are currently only used by the Node Linux build. See comment
413
+ // are currently only used by the Node Linux & Windows build. See comment
410
414
  // in `sync.ts` -> `Ditto.applyPeerToPeerBluetoothLE()` for details.
411
415
  function dittoAddInternalBLEClientTransport(ditto) {
412
416
  {
@@ -641,6 +645,22 @@ function removeSubscription(ditto, collectionName, query, queryArgsCBOR, orderBy
641
645
  const queryX = bytesFromString(query);
642
646
  return ditto_remove_subscription(ditto, collectionNameX, queryX, queryArgsCBOR, orderBy, limit, offset);
643
647
  }
648
+ /** @internal */
649
+ function experimentalAddDqlSubscription(ditto, query, queryArgsCBOR) {
650
+ ensureInitialized();
651
+ const queryBuffer = bytesFromString(query);
652
+ const statusCode = ditto_experimental_add_dql_subscription(ditto, queryBuffer, queryArgsCBOR);
653
+ if (statusCode !== 0)
654
+ throw new Error(errorMessage() || `ditto_experimental_add_dql_subscription() failed with error code: ${statusCode}`);
655
+ }
656
+ /** @internal */
657
+ function experimentalRemoveDqlSubscription(ditto, query, queryArgsCBOR) {
658
+ ensureInitialized();
659
+ const queryBuffer = bytesFromString(query);
660
+ const statusCode = ditto_experimental_remove_dql_subscription(ditto, queryBuffer, queryArgsCBOR);
661
+ if (statusCode !== 0)
662
+ throw new Error(errorMessage() || `ditto_experimental_remove_dql_subscription() failed with error code: ${statusCode}`);
663
+ }
644
664
  // ------------------------------------------------------------ LiveQuery ------
645
665
  /** @internal */
646
666
  function liveQueryRegister(ditto, collectionName, query, queryArgsCBOR, orderBy, limit, offset, eventHandler,
@@ -650,9 +670,10 @@ onError) {
650
670
  ensureInitialized();
651
671
  const collectionNameBuffer = bytesFromString(collectionName);
652
672
  const queryBuffer = bytesFromString(query);
653
- // Note(Daniel): the callback is now registered to be called in a detached manner:
654
- // if the FFI / Rust does `cb()`, then when that call returns, the js callback itself may not
655
- // have completed. This is fine, since `signalNext()` shall be the proper way to achieve this.
673
+ // Note(Daniel): the callback is now registered to be called in a detached
674
+ // manner: if the FFI / Rust does `cb()`, then when that call returns, the js
675
+ // callback itself may not have completed. This is fine, since `signalNext()`
676
+ // shall be the proper way to achieve this.
656
677
  const { status_code: errorCode, i64: id } = ditto_live_query_register_str_detached(ditto, collectionNameBuffer, queryBuffer, queryArgsCBOR, orderBy, limit, offset, wrapBackgroundCbForFFI(onError, eventHandler));
657
678
  if (errorCode !== 0)
658
679
  throw new Error(errorMessage() || `\`ditto_live_query_register_str()\` failed with error code: ${errorCode}`);
@@ -726,24 +747,10 @@ async function writeTransactionRollback(ditto, transaction) {
726
747
  if (errorCode !== 0)
727
748
  throw new Error(errorMessage() || `ditto_write_transaction_rollback() failed with error code: ${errorCode}`);
728
749
  }
729
- // ------------------------------------------------------ StaticTCPClient ------
730
- /** @internal */
731
- function addStaticTCPClient(ditto, address) {
732
- ensureInitialized();
733
- const addressBuffer = bytesFromString(address);
734
- return ditto_add_static_tcp_client(ditto, addressBuffer);
735
- }
736
750
  /** @internal */
737
751
  function staticTCPClientFreeHandle(self) {
738
752
  static_tcp_client_free_handle(self);
739
753
  }
740
- // ------------------------------------------------------ WebsocketClient ------
741
- /** @internal */
742
- function addWebsocketClient(ditto, address, routingHint) {
743
- ensureInitialized();
744
- const addressBuffer = bytesFromString(address);
745
- return ditto_add_websocket_client(ditto, addressBuffer, routingHint);
746
- }
747
754
  /** @internal */
748
755
  function websocketClientFreeHandle(self) {
749
756
  ensureInitialized();
@@ -948,6 +955,16 @@ function dittoFree(self) {
948
955
  return ditto_free(self);
949
956
  }
950
957
  /** @internal */
958
+ function getDeadlockTimeout() {
959
+ ensureInitialized();
960
+ return getDeadlockTimeout$1();
961
+ }
962
+ /** @internal */
963
+ function setDeadlockTimeout(duration) {
964
+ ensureInitialized();
965
+ setDeadlockTimeout$1(duration);
966
+ }
967
+ /** @internal */
951
968
  async function dittoRegisterPresenceV1Callback(self, cb) {
952
969
  ensureInitialized();
953
970
  ditto_register_presence_v1_callback(self, wrapBackgroundCbForFFI((err) => console.error(`The registered presence callback v1 errored with ${err}`), (cJsonStr) => {
@@ -1127,6 +1144,20 @@ async function dittoDisableSyncWithV3(dittoPointer) {
1127
1144
  if (errorCode !== 0)
1128
1145
  throw new Error(errorMessage() || `ditto_disable_sync_with_v3() failed with error code: ${errorCode}`);
1129
1146
  }
1147
+ function dittoSetStaticTCPClients(ditto, listOfServers) {
1148
+ ensureInitialized();
1149
+ // TODO(Vincent): add error handling.
1150
+ {
1151
+ const listOfServersBytes = listOfServers.map((server) => bytesFromString(server));
1152
+ ditto_set_static_tcp_clients(ditto, listOfServersBytes);
1153
+ }
1154
+ }
1155
+ function dittoSetStaticWebsocketClients(ditto, listOfServers, routingHint) {
1156
+ ensureInitialized();
1157
+ // TODO(Vincent): add error handling.
1158
+ const listOfServersBytes = listOfServers.map((server) => bytesFromString(server));
1159
+ ditto_set_static_websocket_clients(ditto, listOfServersBytes, routingHint);
1160
+ }
1130
1161
  // ------------------------------------------------------ Hash & Mnemonic ------
1131
1162
  /** @internal */
1132
1163
  function documentsHash(documents) {
@@ -1439,7 +1470,7 @@ function awdlDestroy(awdl) {
1439
1470
 
1440
1471
  // NOTE: this is patched up with the actual build version by Jake task
1441
1472
  // build:package and has to be a valid semantic version as defined here: https://semver.org.
1442
- const fullBuildVersionString = '4.1.0';
1473
+ const fullBuildVersionString = '4.2.0';
1443
1474
 
1444
1475
  //
1445
1476
  /**
@@ -2749,6 +2780,11 @@ function augmentJSONValue(json, mutDoc, workingPath) {
2749
2780
  return json;
2750
2781
  }
2751
2782
  }
2783
+ /**
2784
+ * Converts objects that may contain instances of classes of this SDK, i.e.
2785
+ * `DocumentID`, `Counter`, `Register` and `Attachment`, into plain JS objects
2786
+ * that can be passed to the FFI layer.
2787
+ */
2752
2788
  function desugarJSObject(jsObj, atRoot = false) {
2753
2789
  if (jsObj && typeof jsObj === 'object') {
2754
2790
  if (Array.isArray(jsObj)) {
@@ -2786,8 +2822,25 @@ function desugarJSObject(jsObj, atRoot = false) {
2786
2822
  }
2787
2823
  }
2788
2824
  else {
2825
+ checkForUnsupportedValues(jsObj);
2789
2826
  return jsObj;
2790
2827
  }
2828
+ }
2829
+ /**
2830
+ * Throws an error if input is a non-finite float value.
2831
+ *
2832
+ * Workaround while we don't have a reliable way of receiving error messages
2833
+ * from `dittoCore.ditto_collection_insert_value()`.
2834
+ *
2835
+ * See https://github.com/getditto/ditto/issues/8657 for details.
2836
+ *
2837
+ * @param jsObj The object to check.
2838
+ * @throws {Error} If `jsObj` is a non-finite float value.
2839
+ */
2840
+ function checkForUnsupportedValues(jsObj) {
2841
+ if (Number.isNaN(jsObj) || jsObj === Infinity || jsObj === -Infinity) {
2842
+ throw new Error('Non-finite float values are not supported');
2843
+ }
2791
2844
  }
2792
2845
 
2793
2846
  //
@@ -5186,6 +5239,125 @@ function collectionsFromDocuments(documents, store) {
5186
5239
  return collections;
5187
5240
  }
5188
5241
 
5242
+ //
5243
+ /**
5244
+ * Manages `ExperimentalSubscription` instances and removes them when Ditto is
5245
+ * closed.
5246
+ *
5247
+ * @internal
5248
+ */
5249
+ class ExperimentalSubscriptionManager {
5250
+ constructor(ditto) {
5251
+ this.subscriptions = {};
5252
+ this.finalizationRegistry = new FinalizationRegistry(this.removeWithContextInfo.bind(this));
5253
+ this.ditto = ditto;
5254
+ }
5255
+ /**
5256
+ * Begin tracking a subscription instance and start it.
5257
+ *
5258
+ * @internal
5259
+ */
5260
+ addSubscription(subscription) {
5261
+ if (this.subscriptions[subscription.contextInfo.id] != null) {
5262
+ throw new Error(`Internal inconsistency, tried to add a subscription that is already tracked: ${subscription.contextInfo.id}`);
5263
+ }
5264
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto);
5265
+ this.ditto.deferClose(() => {
5266
+ experimentalAddDqlSubscription(dittoHandle.deref(), subscription.query, subscription.queryArgsCBOR);
5267
+ });
5268
+ // Only track the subscription after it has been successfully added to Ditto
5269
+ // to avoid tracking a subscription that failed to start.
5270
+ this.subscriptions[subscription.contextInfo.id] = new WeakRef(subscription);
5271
+ this.finalizationRegistry.register(subscription, subscription.contextInfo, subscription);
5272
+ }
5273
+ /**
5274
+ * Stop tracking a subscription instance and cancel it.
5275
+ *
5276
+ * @internal
5277
+ */
5278
+ removeSubscription(subscription) {
5279
+ if (this.subscriptions[subscription.contextInfo.id] == null) {
5280
+ throw new Error(`Internal inconsistency, tried to remove a subscription that is not tracked: ${subscription.contextInfo.id}`);
5281
+ }
5282
+ this.finalizationRegistry.unregister(subscription);
5283
+ delete this.subscriptions[subscription.contextInfo.id];
5284
+ this.removeWithContextInfo(subscription.contextInfo);
5285
+ }
5286
+ /**
5287
+ * Stop tracking all subscriptions and cancel them.
5288
+ *
5289
+ * @internal
5290
+ */
5291
+ close() {
5292
+ Logger.debug(`ExperimentalSubscriptionManager.close() with ${this.subscriptions.length} queries`);
5293
+ this.ditto.deferClose(() => {
5294
+ var _a;
5295
+ for (const subscriptionWeakRef of Object.values(this.subscriptions)) {
5296
+ // If this deref fails, the garbage collector has already removed the
5297
+ // subscription and will issue a finalization callback to
5298
+ // `removeWithContextInfo()`.
5299
+ (_a = subscriptionWeakRef.deref()) === null || _a === void 0 ? void 0 : _a.cancel();
5300
+ }
5301
+ });
5302
+ }
5303
+ /**
5304
+ * Remove tracked subscription without unregistering from finalization
5305
+ * registry.
5306
+ *
5307
+ * @internal
5308
+ */
5309
+ removeWithContextInfo(contextInfo) {
5310
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto);
5311
+ this.ditto.deferClose(() => {
5312
+ experimentalRemoveDqlSubscription(dittoHandle.deref(), contextInfo.query, contextInfo.queryArgsCBOR);
5313
+ delete this.subscriptions[contextInfo.id];
5314
+ });
5315
+ }
5316
+ }
5317
+
5318
+ //
5319
+ /**
5320
+ * Create a subscription to receive updates from remote peers about documents
5321
+ * matching the subscription's query.
5322
+ *
5323
+ * The subscription will remain active until {@link cancel | cancel()} is called
5324
+ * or the subscription object goes out of scope and is garbage collected.
5325
+ *
5326
+ * @internal
5327
+ */
5328
+ class ExperimentalSubscription {
5329
+ /**
5330
+ * Cancels a subscription and releases all associated resources.
5331
+ */
5332
+ cancel() {
5333
+ if (!this.isCancelled) {
5334
+ this['isCancelled'] = true;
5335
+ this.manager.removeSubscription(this);
5336
+ }
5337
+ }
5338
+ // --------------------------- Internal --------------------------------------
5339
+ /** @internal */
5340
+ constructor(manager, query, queryArgsCBOR) {
5341
+ /**
5342
+ * `true` when {@link cancel | cancel()} has been called or the Ditto instance
5343
+ * managing this subscription has been closed.
5344
+ */
5345
+ // Recording this information on the subscription instance itself allows
5346
+ // working with it even after Ditto has been closed and the subscription
5347
+ // manager can not be accessed anymore.
5348
+ this.isCancelled = false;
5349
+ this.query = query;
5350
+ this.queryArgsCBOR = queryArgsCBOR;
5351
+ this.contextInfo = {
5352
+ id: generateEphemeralToken(),
5353
+ query,
5354
+ queryArgsCBOR,
5355
+ };
5356
+ this.manager = manager;
5357
+ manager.addSubscription(this);
5358
+ }
5359
+ }
5360
+
5189
5361
  //
5190
5362
  /**
5191
5363
  * An addon for the Ditto store that contains experimental features. Accessible
@@ -5197,21 +5369,28 @@ class ExperimentalStore {
5197
5369
  /** @internal */
5198
5370
  constructor(ditto) {
5199
5371
  this.ditto = ditto;
5372
+ this.subscriptionManager = new ExperimentalSubscriptionManager(this.ditto);
5373
+ }
5374
+ /** @internal */
5375
+ close() {
5376
+ this.subscriptionManager.close();
5200
5377
  }
5201
5378
  /**
5202
5379
  * Executes the given query in the local store and returns the result.
5203
5380
  *
5204
5381
  * Limitations:
5205
5382
  *
5206
- * - Only supports `SELECT * FROM <collection name>`
5207
- * - No query parameters
5383
+ * - Supports `SELECT * FROM <collection name>` with optional `WHERE
5384
+ * <expression>`
5385
+ * - No query parameters as function arguments yet
5208
5386
  * - No transactions
5209
5387
  *
5210
5388
  * @internal
5211
5389
  * @param query a Ditto Query Language query string
5212
5390
  * @returns a promise for an array of Ditto documents matching the query
5213
- **/
5391
+ */
5214
5392
  async execute(query) {
5393
+ Logger.debug(`ExperimentalStore.execute(query = ${query})`);
5215
5394
  const ditto = this.ditto;
5216
5395
  const dittoHandle = Bridge.ditto.handleFor(ditto);
5217
5396
  return ditto.deferCloseAsync(async () => {
@@ -5226,9 +5405,21 @@ class ExperimentalStore {
5226
5405
  return documentPointers.map((documentPointer) => Bridge.document.bridge(documentPointer));
5227
5406
  });
5228
5407
  }
5229
- /** @internal */
5230
- close() {
5231
- // Nothing to do yet.
5408
+ /**
5409
+ * Subscribes the device to updates specified by a DQL query to collections
5410
+ * that other devices know about.
5411
+ *
5412
+ * The returned {@link ExperimentalSubscription} object must be kept in scope
5413
+ * for as long as you want to keep receiving updates.
5414
+ *
5415
+ * @internal
5416
+ * @param query a Ditto Query Language query string of the form SELECT * FROM
5417
+ * collection WHERE expression
5418
+ * @returns {@link ExperimentalSubscription} object
5419
+ */
5420
+ subscribe(query) {
5421
+ Logger.debug(`ExperimentalStore.subscribe(query = ${query})`);
5422
+ return new ExperimentalSubscription(this.subscriptionManager, query, null);
5232
5423
  }
5233
5424
  }
5234
5425
 
@@ -5996,8 +6187,6 @@ class Sync {
5996
6187
  this.ditto = ditto;
5997
6188
  this.parameters = parameters;
5998
6189
  this.state = stateFrom(parameters);
5999
- this.staticTCPClientsByAddress = {};
6000
- this.websocketClientsByURL = {};
6001
6190
  }
6002
6191
  update(parameters) {
6003
6192
  this['parameters'] = { ...parameters };
@@ -6181,70 +6370,17 @@ class Sync {
6181
6370
  const ditto = this.ditto;
6182
6371
  const dittoHandle = Bridge.ditto.handleFor(ditto);
6183
6372
  ditto.deferClose(() => {
6184
- // REFACTOR: same "algorithm" as for Websockets below, DRY up.
6185
- const currentTCPServers = Object.getOwnPropertyNames(this.staticTCPClientsByAddress);
6186
- const desiredTCPServers = stateNew.effectiveTransportConfig.connect.tcpServers;
6187
- const tcpServersToConnectToSet = new Set(desiredTCPServers);
6188
- for (const tcpServer of currentTCPServers)
6189
- tcpServersToConnectToSet.delete(tcpServer);
6190
- const tcpServersToDisconnectFromSet = new Set(currentTCPServers);
6191
- for (const tcpServer of desiredTCPServers)
6192
- tcpServersToDisconnectFromSet.delete(tcpServer);
6193
- const tcpServersToConnectTo = tcpServersToConnectToSet.values();
6194
- const tcpServersToDisconnectFrom = tcpServersToDisconnectFromSet.values();
6195
- for (const tcpServer of tcpServersToConnectTo) {
6196
- const staticTCPClientPointer = addStaticTCPClient(dittoHandle.deref(), tcpServer);
6197
- const staticTCPClient = Bridge.staticTCPClient.bridge(staticTCPClientPointer);
6198
- this.staticTCPClientsByAddress[tcpServer] = staticTCPClient;
6199
- }
6200
- for (const tcpServer of tcpServersToDisconnectFrom) {
6201
- // REFACTOR: we have to make sure freeing the tcpServer handle is done
6202
- // BEFORE the Ditto instance itself is freed.
6203
- const staticTCPClient = this.staticTCPClientsByAddress[tcpServer];
6204
- if (!staticTCPClient)
6205
- throw new Error(`Internal inconsistency, can't disconnect from TCP address '${tcpServer}', no staticTCPClient found.`);
6206
- const staticTCPClientPointer = Bridge.staticTCPClient.handleFor(staticTCPClient).deref();
6207
- // Unregister to avoid double-free for this pointer, then free manually:
6208
- Bridge.staticTCPClient.unregister(staticTCPClient);
6209
- staticTCPClientFreeHandle(staticTCPClientPointer);
6210
- delete this.staticTCPClientsByAddress[tcpServer];
6211
- }
6373
+ const tcpServers = stateNew.effectiveTransportConfig.connect.tcpServers;
6374
+ dittoSetStaticTCPClients(dittoHandle.deref(), tcpServers);
6212
6375
  });
6213
6376
  }
6214
6377
  updateConnectWebsocketURLs(stateOld, stateNew) {
6215
6378
  const ditto = this.ditto;
6216
6379
  const dittoHandle = Bridge.ditto.handleFor(ditto);
6217
6380
  ditto.deferClose(() => {
6218
- // REFACTOR: same "algorithm" as for TCP servers above, DRY up.
6219
- // IDEA: normalize URLs so that we don't connect to the same URL twice?
6220
- const currentWebsocketURLs = Object.getOwnPropertyNames(this.websocketClientsByURL);
6221
- const desiredWebsocketURLs = stateNew.effectiveTransportConfig.connect.websocketURLs.slice();
6222
- const websocketURLsToConnectToSet = new Set(desiredWebsocketURLs);
6223
- for (const websocketURL of currentWebsocketURLs)
6224
- websocketURLsToConnectToSet.delete(websocketURL);
6225
- const websocketURLsToDisconnectFromSet = new Set(currentWebsocketURLs);
6226
- for (const websocketURL of desiredWebsocketURLs)
6227
- websocketURLsToDisconnectFromSet.delete(websocketURL);
6228
- const websocketURLsToConnectTo = websocketURLsToConnectToSet.values();
6229
- const websocketURLsToDisconnectFrom = websocketURLsToDisconnectFromSet.values();
6381
+ const websocketURLs = stateNew.effectiveTransportConfig.connect.websocketURLs;
6230
6382
  const routingHint = stateNew.effectiveTransportConfig.global.routingHint;
6231
- for (const websocketURL of websocketURLsToConnectTo) {
6232
- const websocketClientPointer = addWebsocketClient(dittoHandle.deref(), websocketURL, routingHint);
6233
- const websocketClient = Bridge.websocketClient.bridge(websocketClientPointer);
6234
- this.websocketClientsByURL[websocketURL] = websocketClient;
6235
- }
6236
- for (const websocketURL of websocketURLsToDisconnectFrom) {
6237
- // REFACTOR: we have to make sure freeing the websocket handle is done
6238
- // BEFORE the Ditto instance itself is freed.
6239
- const websocketClient = this.websocketClientsByURL[websocketURL];
6240
- if (!websocketClient)
6241
- throw new Error(`Internal inconsistency, can't disconnect from websocket URL '${websocketURL}', no websocketClient found.`);
6242
- const websocketClientPointer = Bridge.websocketClient.handleFor(websocketClient).deref();
6243
- // Unregister to avoid double-free for this pointer, then free manually:
6244
- Bridge.websocketClient.unregister(websocketClient);
6245
- websocketClientFreeHandle(websocketClientPointer);
6246
- delete this.websocketClientsByURL[websocketURL];
6247
- }
6383
+ dittoSetStaticWebsocketClients(dittoHandle.deref(), websocketURLs, routingHint);
6248
6384
  });
6249
6385
  }
6250
6386
  updateGlobal(stateOld, stateNew) {
@@ -6346,7 +6482,7 @@ class SubscriptionManager {
6346
6482
  constructor(ditto) {
6347
6483
  this.ditto = ditto;
6348
6484
  this.subscriptions = {};
6349
- this.finalizationRegistry = new FinalizationRegistry(this.removeWithContextinfo.bind(this));
6485
+ this.finalizationRegistry = new FinalizationRegistry(this.removeWithContextInfo.bind(this));
6350
6486
  }
6351
6487
  /**
6352
6488
  * Begin tracking a subscription instance and start it.
@@ -6371,7 +6507,7 @@ class SubscriptionManager {
6371
6507
  throw new Error(`Internal inconsistency, tried to remove a subscription that is not tracked: ${subscription.contextInfo.id}`);
6372
6508
  }
6373
6509
  this.finalizationRegistry.unregister(subscription);
6374
- this.removeWithContextinfo(subscription.contextInfo);
6510
+ this.removeWithContextInfo(subscription.contextInfo);
6375
6511
  }
6376
6512
  /**
6377
6513
  * Stop tracking all subscriptions and cancel them.
@@ -6394,7 +6530,7 @@ class SubscriptionManager {
6394
6530
  * registry.
6395
6531
  *
6396
6532
  * @internal */
6397
- removeWithContextinfo(contextInfo) {
6533
+ removeWithContextInfo(contextInfo) {
6398
6534
  const ditto = this.ditto;
6399
6535
  const dittoHandle = Bridge.ditto.handleFor(ditto);
6400
6536
  ditto.deferClose(() => {
@@ -6697,8 +6833,13 @@ class Ditto {
6697
6833
  })();
6698
6834
  const dittoPointer = dittoMake(uninitializedDittoX, identityConfig);
6699
6835
  dittoAuthClientSetValidityListener(dittoPointer, function (...args) {
6700
- var _a;
6701
- (_a = weakThis.deref()) === null || _a === void 0 ? void 0 : _a.authClientValidityChanged(...args);
6836
+ const ditto = weakThis.deref();
6837
+ if (ditto == null || ditto.isClosed) {
6838
+ Logger.info('Ditto is closed, ignoring auth client validity change');
6839
+ }
6840
+ else {
6841
+ ditto.authClientValidityChanged(...args);
6842
+ }
6702
6843
  });
6703
6844
  const isWebValid = dittoAuthClientIsWebValid(dittoPointer);
6704
6845
  const isX509Valid = dittoAuthClientIsX509Valid(dittoPointer);
@@ -6762,12 +6903,59 @@ class Ditto {
6762
6903
  this.attachmentFetcherManager = new AttachmentFetcherManager(this);
6763
6904
  this.transportConditionsManager = new TransportConditionsManager(this);
6764
6905
  this.subscriptionManager = new SubscriptionManager(this);
6906
+ disableDeadlockTimeoutWhenDebugging();
6765
6907
  // WORKAROUND: see description above where the
6766
6908
  // secondsRemainingUntilAuthenticationExpires variable is declared.
6767
6909
  if (secondsRemainingUntilAuthenticationExpires !== null) {
6768
6910
  this.auth['@ditto.authenticationExpiring'](secondsRemainingUntilAuthenticationExpires);
6769
6911
  }
6770
6912
  }
6913
+ /**
6914
+ * Don't terminate the process when callbacks are pending for a long time.
6915
+ *
6916
+ * Some methods in the Ditto library accept asynchronous functions as callback
6917
+ * parameters. If these asynchronous functions do not resolve within a certain
6918
+ * period of time after having been invoked by Ditto, deadlock detection gets
6919
+ * triggered, resulting in the termination of the process.
6920
+ *
6921
+ * When Ditto is executed in a Node.js environment with an interactive
6922
+ * debugger attached, this deadlock detection might get activated upon
6923
+ * encountering a breakpoint. Calling `Ditto.disableDeadlockDetection()`
6924
+ * disables this behavior, thus allowing the use of an interactive debugger
6925
+ * without triggering the deadlock detection.
6926
+ *
6927
+ * This feature is not available in the browser.
6928
+ */
6929
+ static disableDeadlockDetection() {
6930
+ {
6931
+ const current = getDeadlockTimeout();
6932
+ if (current !== 0) {
6933
+ try {
6934
+ setDeadlockTimeout(0);
6935
+ }
6936
+ catch (e) {
6937
+ throw new Error(`Failed to disable deadlock detection: ${e === null || e === void 0 ? void 0 : e.message}`);
6938
+ }
6939
+ }
6940
+ }
6941
+ }
6942
+ /**
6943
+ * Returns `true` if deadlock detection is enabled.
6944
+ *
6945
+ * See
6946
+ * {@link Ditto.disableDeadlockDetection | Ditto.disableDeadlockDetection()}
6947
+ * for more information.
6948
+ *
6949
+ * This method always returns `false` in the browser where deadlock detection
6950
+ * is not available.
6951
+ *
6952
+ * @returns `true` if deadlock detection is enabled
6953
+ */
6954
+ static hasDeadlockDetection() {
6955
+ {
6956
+ return getDeadlockTimeout() !== 0;
6957
+ }
6958
+ }
6771
6959
  /**
6772
6960
  * Check if the current environment supports running Ditto.
6773
6961
  *
@@ -6779,7 +6967,7 @@ class Ditto {
6779
6967
  *
6780
6968
  * Internet Explorer is not supported.
6781
6969
  *
6782
- * @returns true if the environment is supported
6970
+ * @returns `true` if the environment is supported
6783
6971
  */
6784
6972
  static isEnvironmentSupported() {
6785
6973
  // From https://stackoverflow.com/questions/21825157/internet-explorer-11-detection
@@ -7130,6 +7318,31 @@ const checkAPIs = (_globalObject) => {
7130
7318
  const requiredBrowserAPIs = ['BigInt', 'WeakRef', 'FinalizationRegistry'];
7131
7319
  const globalObject = _globalObject || globalThis || global || window;
7132
7320
  return requiredBrowserAPIs.every((apiName) => !!globalObject[apiName]);
7321
+ };
7322
+ /**
7323
+ * Disable deadlock timeout when Node.js is running with `--inspect` parameter.
7324
+ *
7325
+ * This heuristic is not failsafe as debugging mode can also be enabled by
7326
+ * sending a `SIGUSR1` signal to the process.
7327
+ *
7328
+ * @internal
7329
+ */
7330
+ const disableDeadlockTimeoutWhenDebugging = () => {
7331
+ var _a, _b;
7332
+ {
7333
+ const hasInspector = process && ((_a = process === null || process === void 0 ? void 0 : process.execArgv) === null || _a === void 0 ? void 0 : _a.some((arg) => arg.includes('--inspect') || arg.includes('--debug')));
7334
+ // @ts-ignore - v8debug may be undefined
7335
+ const hasLegacyDebugMode = (process && ((_b = process === null || process === void 0 ? void 0 : process.execArgv) === null || _b === void 0 ? void 0 : _b.some((arg) => arg.includes('--debug')))) || typeof v8debug === 'object';
7336
+ if (hasInspector || hasLegacyDebugMode) {
7337
+ try {
7338
+ setDeadlockTimeout(0);
7339
+ Logger.warning('Detected Node running with inspector enabled, disabling deadlock timeout.');
7340
+ }
7341
+ catch (e) {
7342
+ Logger.error(`Detected Node running with inspector enabled but failed to disable deadlock timeout: ${e === null || e === void 0 ? void 0 : e.message}`);
7343
+ }
7344
+ }
7345
+ }
7133
7346
  };
7134
7347
 
7135
7348
  /**
@@ -7137,7 +7350,8 @@ const checkAPIs = (_globalObject) => {
7137
7350
  *
7138
7351
  * Use this in testing to ensure that all objects are properly garbage collected at the end of tests.
7139
7352
  *
7140
- * @returns an object with a key per bridge type, containing the number of registered objects.
7353
+ * @returns an object with a key per bridge type, set to the count of registered
7354
+ * objects.
7141
7355
  */
7142
7356
  function getBridgeLoad() {
7143
7357
  const countsByType = {};
@@ -7193,6 +7407,8 @@ exports.Document = Document;
7193
7407
  exports.DocumentID = DocumentID;
7194
7408
  exports.DocumentPath = DocumentPath;
7195
7409
  exports.ExperimentalStore = ExperimentalStore;
7410
+ exports.ExperimentalSubscription = ExperimentalSubscription;
7411
+ exports.ExperimentalSubscriptionManager = ExperimentalSubscriptionManager;
7196
7412
  exports.IdentityTypesRequiringOfflineLicenseToken = IdentityTypesRequiringOfflineLicenseToken;
7197
7413
  exports.KeepAlive = KeepAlive;
7198
7414
  exports.LiveQuery = LiveQuery;
@@ -7224,6 +7440,7 @@ exports.WriteTransactionPendingCursorOperation = WriteTransactionPendingCursorOp
7224
7440
  exports.WriteTransactionPendingIDSpecificOperation = WriteTransactionPendingIDSpecificOperation;
7225
7441
  exports.addressToString = addressToString;
7226
7442
  exports.checkAPIs = checkAPIs;
7443
+ exports.disableDeadlockTimeoutWhenDebugging = disableDeadlockTimeoutWhenDebugging;
7227
7444
  exports.getBridgeLoad = getBridgeLoad;
7228
7445
  exports.init = init;
7229
7446
  exports.validateDocumentIDCBOR = validateDocumentIDCBOR;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dittolive/ditto",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Ditto is a cross-platform embeddable NoSQL database that can sync with or without an internet connection.",
5
5
  "homepage": "https://ditto.live",
6
6
  "license": "SEE LICENSE IN LICENSE.md",