@dittolive/ditto 4.0.3-alpha.linux-ble-fixes-2 → 4.1.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/node/ditto.cjs.js CHANGED
@@ -84,7 +84,7 @@ class Observer {
84
84
  * method. Otherwise returns `false`.
85
85
  */
86
86
  get isStopped() {
87
- return typeof this.token === 'undefined';
87
+ return this.observerManager.hasObserver(this.token);
88
88
  }
89
89
  /**
90
90
  * Stops the observation. Calling this method multiple times has no effect.
@@ -292,21 +292,16 @@ function ditto_add_multicast_transport(...args) { return ditto.ditto_add_multica
292
292
  function ditto_add_static_tcp_client(...args) { return ditto.ditto_add_static_tcp_client(...args) }
293
293
  function ditto_add_subscription(...args) { return ditto.ditto_add_subscription(...args) }
294
294
  function ditto_add_websocket_client(...args) { return ditto.ditto_add_websocket_client(...args) }
295
- function ditto_auth_client_free(...args) { return ditto.ditto_auth_client_free(...args) }
296
295
  function ditto_auth_client_get_site_id(...args) { return ditto.ditto_auth_client_get_site_id(...args) }
297
296
  function ditto_auth_client_is_web_valid(...args) { return ditto.ditto_auth_client_is_web_valid(...args) }
298
297
  function ditto_auth_client_is_x509_valid(...args) { return ditto.ditto_auth_client_is_x509_valid(...args) }
299
298
  function ditto_auth_client_login_with_credentials(...args) { return ditto.ditto_auth_client_login_with_credentials(...args) }
300
299
  function ditto_auth_client_login_with_token(...args) { return ditto.ditto_auth_client_login_with_token(...args) }
301
300
  function ditto_auth_client_logout(...args) { return ditto.ditto_auth_client_logout(...args) }
302
- function ditto_auth_client_make_anonymous_client(...args) { return ditto.ditto_auth_client_make_anonymous_client(...args) }
303
- function ditto_auth_client_make_for_development(...args) { return ditto.ditto_auth_client_make_for_development(...args) }
304
301
  function ditto_auth_client_make_login_provider(...args) { return ditto.ditto_auth_client_make_login_provider(...args) }
305
- function ditto_auth_client_make_with_shared_key(...args) { return ditto.ditto_auth_client_make_with_shared_key(...args) }
306
- function ditto_auth_client_make_with_static_x509(...args) { return ditto.ditto_auth_client_make_with_static_x509(...args) }
307
- function ditto_auth_client_make_with_web(...args) { return ditto.ditto_auth_client_make_with_web(...args) }
308
302
  function ditto_auth_client_set_validity_listener(...args) { return ditto.ditto_auth_client_set_validity_listener(...args) }
309
303
  function ditto_auth_client_user_id(...args) { return ditto.ditto_auth_client_user_id(...args) }
304
+ function ditto_auth_set_login_provider(...args) { return ditto.ditto_auth_set_login_provider(...args) }
310
305
  function ditto_cancel_resolve_attachment(...args) { return ditto.ditto_cancel_resolve_attachment(...args) }
311
306
  function ditto_clear_presence_callback(...args) { return ditto.ditto_clear_presence_callback(...args) }
312
307
  function ditto_clear_presence_v3_callback(...args) { return ditto.ditto_clear_presence_v3_callback(...args) }
@@ -337,6 +332,11 @@ function ditto_free_attachment_handle(...args) { return ditto.ditto_free_attachm
337
332
  function ditto_get_collection_names(...args) { return ditto.ditto_get_collection_names(...args) }
338
333
  function ditto_get_complete_attachment_path(...args) { return ditto.ditto_get_complete_attachment_path(...args) }
339
334
  function ditto_get_sdk_version(...args) { return ditto.ditto_get_sdk_version(...args) }
335
+ function ditto_identity_config_make_manual_v0(...args) { return ditto.ditto_identity_config_make_manual_v0(...args) }
336
+ function ditto_identity_config_make_offline_playground(...args) { return ditto.ditto_identity_config_make_offline_playground(...args) }
337
+ function ditto_identity_config_make_online_playground(...args) { return ditto.ditto_identity_config_make_online_playground(...args) }
338
+ function ditto_identity_config_make_online_with_authentication(...args) { return ditto.ditto_identity_config_make_online_with_authentication(...args) }
339
+ function ditto_identity_config_make_shared_key(...args) { return ditto.ditto_identity_config_make_shared_key(...args) }
340
340
  function ditto_init_sdk_version(...args) { return ditto.ditto_init_sdk_version(...args) }
341
341
  function ditto_live_query_register_str_detached(...args) { return ditto.ditto_live_query_register_str_detached(...args) }
342
342
  function ditto_live_query_signal_available_next(...args) { return ditto.ditto_live_query_signal_available_next(...args) }
@@ -370,6 +370,7 @@ function ditto_run_garbage_collection(...args) { return ditto.ditto_run_garbage_
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
372
  function ditto_set_sync_group(...args) { return ditto.ditto_set_sync_group(...args) }
373
+ function ditto_shutdown(...args) { return ditto.ditto_shutdown(...args) }
373
374
  function ditto_start_http_server(...args) { return ditto.ditto_start_http_server(...args) }
374
375
  function ditto_start_tcp_server(...args) { return ditto.ditto_start_tcp_server(...args) }
375
376
  function ditto_stop_http_server(...args) { return ditto.ditto_stop_http_server(...args) }
@@ -537,7 +538,7 @@ async function collectionGet(ditto, collectionName, documentID, readTransaction)
537
538
  return document;
538
539
  }
539
540
  /** @internal */
540
- async function collectionInsertValue(ditto, collectionName, doc_cbor, doc_id, writeStrategy, writeTransaction) {
541
+ async function collectionInsertValue(ditto, collectionName, doc_cbor, writeStrategy, writeTransaction) {
541
542
  ensureInitialized();
542
543
  // REFACTOR: add proper error handling.
543
544
  const collectionNameX = bytesFromString(collectionName);
@@ -555,7 +556,7 @@ async function collectionInsertValue(ditto, collectionName, doc_cbor, doc_id, wr
555
556
  default:
556
557
  throw new Error('Invalid write strategy provided');
557
558
  }
558
- const { status_code: errorCode, id } = await ditto_collection_insert_value(ditto, collectionNameX, doc_cbor, doc_id, strategy, null, writeTransaction !== null && writeTransaction !== void 0 ? writeTransaction : null);
559
+ const { status_code: errorCode, id } = await ditto_collection_insert_value(ditto, collectionNameX, doc_cbor, strategy, null, writeTransaction !== null && writeTransaction !== void 0 ? writeTransaction : null);
559
560
  if (errorCode !== 0)
560
561
  throw new Error(errorMessage() || `ditto_collection_insert_value() failed with error code: ${errorCode}`);
561
562
  return boxCBytesIntoBuffer(id);
@@ -823,103 +824,94 @@ function log(level, message) {
823
824
  }
824
825
  // ----------------------------------------------------------- AuthClient ------
825
826
  /** @internal */
826
- function dittoAuthClientMakeAnonymousClient(path, appID, sharedToken, baseURL) {
827
+ function dittoIdentityConfigMakeOnlinePlayground(appID, sharedToken, baseURL) {
827
828
  ensureInitialized();
828
- const pathX = bytesFromString(path);
829
829
  const appIDX = bytesFromString(appID);
830
830
  const sharedTokenX = bytesFromString(sharedToken);
831
831
  const baseURLX = bytesFromString(baseURL);
832
- const { status_code: errorCode, auth_client: authClient } = ditto_auth_client_make_anonymous_client(pathX, appIDX, sharedTokenX, baseURLX);
832
+ const { status_code: errorCode, identity_config: identityConfig } = ditto_identity_config_make_online_playground(appIDX, sharedTokenX, baseURLX);
833
833
  if (errorCode !== 0)
834
- throw new Error(errorMessage() || `ditto_auth_client_make_anonymous_client() failed with error code: ${errorCode}`);
835
- return authClient;
834
+ throw new Error(errorMessage() || `ditto_identity_config_make_online_playground() failed with error code: ${errorCode}`);
835
+ return identityConfig;
836
836
  }
837
837
  /** @internal */
838
- function dittoAuthClientMakeWithWeb(path, appID, baseURL, loginProvider) {
838
+ function dittoIdentityConfigMakeOnlineWithAuthentication(appID, baseURL) {
839
839
  ensureInitialized();
840
- const pathX = bytesFromString(path);
841
840
  const appIDX = bytesFromString(appID);
842
841
  const baseURLX = bytesFromString(baseURL);
843
- const { status_code: errorCode, auth_client: authClient } = ditto_auth_client_make_with_web(pathX, appIDX, baseURLX, loginProvider);
842
+ const { status_code: errorCode, identity_config: identityConfig } = ditto_identity_config_make_online_with_authentication(appIDX, baseURLX);
844
843
  if (errorCode !== 0)
845
- throw new Error(errorMessage() || `ditto_auth_client_make_with_web() failed with error code: ${errorCode}`);
846
- return authClient;
844
+ throw new Error(errorMessage() || `ditto_identity_config_make_online_with_authentication() failed with error code: ${errorCode}`);
845
+ return identityConfig;
847
846
  }
848
847
  /** @internal */
849
- function dittoAuthClientMakeForDevelopment(path, appId, siteID) {
848
+ function dittoIdentityConfigMakeOfflinePlayground(appId, siteID) {
850
849
  ensureInitialized();
851
- const pathX = bytesFromString(path);
852
850
  const appIdX = bytesFromString(appId);
853
851
  const siteIDX = Number(siteID);
854
- const { status_code: errorCode, auth_client: authClient } = ditto_auth_client_make_for_development(pathX, appIdX, siteIDX);
852
+ const { status_code: errorCode, identity_config: identityConfig } = ditto_identity_config_make_offline_playground(appIdX, siteIDX);
855
853
  if (errorCode !== 0)
856
- throw new Error(errorMessage() || `ditto_auth_client_make_for_development() failed with error code: ${errorCode}`);
857
- return authClient;
854
+ throw new Error(errorMessage() || `ditto_identity_config_make_offline_playground() failed with error code: ${errorCode}`);
855
+ return identityConfig;
858
856
  }
859
857
  /** @internal */
860
- function dittoAuthClientMakeWithSharedKey(path, appId, sharedKey, siteID) {
858
+ function dittoIdentityConfigMakeSharedKey(appId, sharedKey, siteID) {
861
859
  ensureInitialized();
862
- const pathX = bytesFromString(path);
863
860
  const appIdX = bytesFromString(appId);
864
861
  const sharedKeyX = bytesFromString(sharedKey);
865
862
  const siteIDX = Number(siteID);
866
- const { status_code: errorCode, auth_client: authClient } = ditto_auth_client_make_with_shared_key(pathX, appIdX, sharedKeyX, siteIDX);
863
+ const { status_code: errorCode, identity_config: identityConfig } = ditto_identity_config_make_shared_key(appIdX, sharedKeyX, siteIDX);
867
864
  if (errorCode !== 0)
868
- throw new Error(errorMessage() || `ditto_auth_client_make_with_shared_key() failed with error code: ${errorCode}`);
869
- return authClient;
865
+ throw new Error(errorMessage() || `ditto_identity_config_make_shared_key() failed with error code: ${errorCode}`);
866
+ return identityConfig;
870
867
  }
871
868
  /** @internal */
872
- function dittoAuthClientMakeWithStaticX509(configCBORBase64) {
869
+ function dittoIdentityConfigMakeManual(configCBORBase64) {
873
870
  ensureInitialized();
874
871
  const configCBORBase64X = bytesFromString(configCBORBase64);
875
- const { status_code: errorCode, auth_client: authClient } = ditto_auth_client_make_with_static_x509(configCBORBase64X);
872
+ const { status_code: errorCode, identity_config: identityConfig } = ditto_identity_config_make_manual_v0(configCBORBase64X);
876
873
  if (errorCode !== 0)
877
- throw new Error(errorMessage() || `ditto_auth_client_make_with_static_x509() failed with error code: ${errorCode}`);
878
- return authClient;
874
+ throw new Error(errorMessage() || `ditto_identity_config_make_manual_v0() failed with error code: ${errorCode}`);
875
+ return identityConfig;
879
876
  }
880
877
  /** @internal */
881
- function dittoAuthClientGetSiteID(authClient) {
878
+ function dittoAuthClientGetSiteID(ditto) {
882
879
  ensureInitialized();
883
- return ditto_auth_client_get_site_id(authClient);
880
+ return ditto_auth_client_get_site_id(ditto);
884
881
  }
885
- /** @internal */
886
- function dittoAuthClientFree(authClient) {
887
- ensureInitialized();
888
- return ditto_auth_client_free(authClient);
889
- }
890
- function dittoAuthClientIsWebValid(authClient) {
882
+ function dittoAuthClientIsWebValid(ditto) {
891
883
  ensureInitialized();
892
- return ditto_auth_client_is_web_valid(authClient) !== 0;
884
+ return ditto_auth_client_is_web_valid(ditto) !== 0;
893
885
  }
894
- function dittoAuthClientUserID(authClient) {
886
+ function dittoAuthClientUserID(ditto) {
895
887
  ensureInitialized();
896
- const cStr = ditto_auth_client_user_id(authClient);
888
+ const cStr = ditto_auth_client_user_id(ditto);
897
889
  return boxCStringIntoString(cStr);
898
890
  }
899
- function dittoAuthClientIsX509Valid(authClient) {
891
+ function dittoAuthClientIsX509Valid(ditto) {
900
892
  ensureInitialized();
901
- return ditto_auth_client_is_x509_valid(authClient) !== 0;
893
+ return ditto_auth_client_is_x509_valid(ditto) !== 0;
902
894
  }
903
- async function dittoAuthClientLoginWithToken(authClient, token, provider) {
895
+ async function dittoAuthClientLoginWithToken(ditto, token, provider) {
904
896
  ensureInitialized();
905
897
  const tokenBytes = bytesFromString(token);
906
898
  const providerBytes = bytesFromString(provider);
907
- const errorCode = await ditto_auth_client_login_with_token(authClient, tokenBytes, providerBytes);
899
+ const errorCode = await ditto_auth_client_login_with_token(ditto, tokenBytes, providerBytes);
908
900
  if (errorCode !== 0)
909
901
  throw new Error(errorMessage() || `Ditto failed to authenticate (error code: ${errorCode}).`);
910
902
  }
911
- async function dittoAuthClientLoginWithUsernameAndPassword(authClient, username, password, provider) {
903
+ async function dittoAuthClientLoginWithUsernameAndPassword(ditto, username, password, provider) {
912
904
  ensureInitialized();
913
905
  const usernameBytes = bytesFromString(username);
914
906
  const passwordBytes = bytesFromString(password);
915
907
  const providerBytes = bytesFromString(provider);
916
- const errorCode = await ditto_auth_client_login_with_credentials(authClient, usernameBytes, passwordBytes, providerBytes);
908
+ const errorCode = await ditto_auth_client_login_with_credentials(ditto, usernameBytes, passwordBytes, providerBytes);
917
909
  if (errorCode !== 0)
918
910
  throw new Error(errorMessage() || `Ditto failed to authenticate (error code: ${errorCode}).`);
919
911
  }
920
- async function dittoAuthClientLogout(authClient) {
912
+ async function dittoAuthClientLogout(ditto) {
921
913
  ensureInitialized();
922
- const errorCode = await ditto_auth_client_logout(authClient);
914
+ const errorCode = await ditto_auth_client_logout(ditto);
923
915
  if (errorCode !== 0)
924
916
  throw new Error(errorMessage() || `Ditto failed to logout (error code: ${errorCode}).`);
925
917
  }
@@ -931,9 +923,9 @@ function uninitializedDittoMake(path) {
931
923
  return uninitialized_ditto_make(pathX);
932
924
  }
933
925
  /** @internal */
934
- function dittoMake(uninitializedDitto, authClient) {
926
+ function dittoMake(uninitializedDitto, identityConfig) {
935
927
  ensureInitialized();
936
- return ditto_make(uninitializedDitto, authClient, 'Disabled');
928
+ return ditto_make(uninitializedDitto, identityConfig, 'Disabled');
937
929
  }
938
930
  /** @internal */
939
931
  async function dittoGetCollectionNames(self) {
@@ -958,15 +950,15 @@ function dittoFree(self) {
958
950
  /** @internal */
959
951
  async function dittoRegisterPresenceV1Callback(self, cb) {
960
952
  ensureInitialized();
961
- if (cb) {
962
- ditto_register_presence_v1_callback(self, wrapBackgroundCbForFFI((err) => console.error(`The registered presence callback errored with ${err}`), (cJsonStr) => {
963
- const jsonStr = refCStringToString(cJsonStr);
964
- cb(jsonStr);
965
- }));
966
- }
967
- else {
968
- await ditto_clear_presence_callback(self);
969
- }
953
+ ditto_register_presence_v1_callback(self, wrapBackgroundCbForFFI((err) => console.error(`The registered presence callback v1 errored with ${err}`), (cJsonStr) => {
954
+ const jsonStr = refCStringToString(cJsonStr);
955
+ cb(jsonStr);
956
+ }));
957
+ }
958
+ /** @internal */
959
+ async function dittoClearPresenceCallback(self) {
960
+ ensureInitialized();
961
+ await ditto_clear_presence_callback(self);
970
962
  }
971
963
  /** @internal */
972
964
  async function dittoRegisterPresenceV3Callback(self, cb) {
@@ -982,11 +974,6 @@ async function dittoClearPresenceV3Callback(self) {
982
974
  ditto_clear_presence_v3_callback(self);
983
975
  }
984
976
  /** @internal */
985
- async function dittoClearPresenceCallback(self) {
986
- ensureInitialized();
987
- await ditto_clear_presence_callback(self);
988
- }
989
- /** @internal */
990
977
  function dittoRegisterTransportConditionChangedCallback(self, cb) {
991
978
  ensureInitialized();
992
979
  if (!cb) {
@@ -1100,6 +1087,11 @@ function dittoStopTCPServer(dittoPointer) {
1100
1087
  return ditto_stop_tcp_server(dittoPointer);
1101
1088
  }
1102
1089
  /** @internal */
1090
+ async function dittoShutdown(dittoPointer) {
1091
+ ensureInitialized();
1092
+ return await ditto_shutdown(dittoPointer);
1093
+ }
1094
+ /** @internal */
1103
1095
  function dittoAddMulticastTransport(dittoPointer) {
1104
1096
  ensureInitialized();
1105
1097
  return ditto_add_multicast_transport(dittoPointer);
@@ -1154,6 +1146,11 @@ function documentsHashMnemonic(documents) {
1154
1146
  return boxCStringIntoString(c_string);
1155
1147
  }
1156
1148
  /** @internal */
1149
+ async function dittoAuthSetLoginProvider(ditto, loginProvider) {
1150
+ ensureInitialized();
1151
+ return await ditto_auth_set_login_provider(ditto, loginProvider);
1152
+ }
1153
+ /** @internal */
1157
1154
  function dittoAuthClientMakeLoginProvider(expiringCb,
1158
1155
  // Cb may be called in parallel at any point, so let's use an optional error handler
1159
1156
  onError) {
@@ -1161,14 +1158,14 @@ onError) {
1161
1158
  return ditto_auth_client_make_login_provider(wrapBackgroundCbForFFI(onError, expiringCb));
1162
1159
  }
1163
1160
  /** @internal */
1164
- function dittoAuthClientSetValidityListener(authClient, validityUpdateCb,
1161
+ function dittoAuthClientSetValidityListener(ditto, validityUpdateCb,
1165
1162
  // Cb may be called in parallel at any point, so let's use an optional error handler
1166
1163
  onError) {
1167
1164
  ensureInitialized();
1168
1165
  const validityUpdateRawCb = wrapBackgroundCbForFFI(onError, function (isWebValidInt, isX509ValidInt) {
1169
1166
  return validityUpdateCb(isWebValidInt === 1, isX509ValidInt === 1);
1170
1167
  });
1171
- return ditto_auth_client_set_validity_listener(authClient, validityUpdateRawCb);
1168
+ return ditto_auth_client_set_validity_listener(ditto, validityUpdateRawCb);
1172
1169
  }
1173
1170
  // ---------------------------------------------------------------- Other ------
1174
1171
  /** @internal */
@@ -1442,7 +1439,7 @@ function awdlDestroy(awdl) {
1442
1439
 
1443
1440
  // NOTE: this is patched up with the actual build version by Jake task
1444
1441
  // build:package and has to be a valid semantic version as defined here: https://semver.org.
1445
- const fullBuildVersionString = '4.0.3-alpha.linux-ble-fixes-2';
1442
+ const fullBuildVersionString = '4.1.0';
1446
1443
 
1447
1444
  //
1448
1445
  /**
@@ -2220,19 +2217,111 @@ function validateDocumentIDCBOR(idCBOR) {
2220
2217
  const DEBUG_TYPE_NAMES = [];
2221
2218
  const DEBUG_ALL_TYPES = false;
2222
2219
  /**
2223
- * For every object registered in a bridge, a {@link Meta} object is stored in
2224
- * the bridge instance to help with debugging and testing.
2220
+ * A handle serves as a safe wrapper around a pointer to a native object.
2221
+ *
2222
+ * A bridge keeps track of all handles that have been created on its
2223
+ * {@link Bridge.handlesByAddress | `handlesByAddress`} property, which allows
2224
+ * enumerating all objects that are currently managed by the bridge.
2225
2225
  *
2226
2226
  * @internal */
2227
- class Meta {
2228
- /** @internal */
2229
- constructor(type, object, pointer) {
2230
- this.type = type;
2231
- this.object = object;
2232
- this.pointer = pointer;
2227
+ class Handle {
2228
+ /**
2229
+ * Warning: Do not call this constructor directly. Use
2230
+ * {@link Bridge.handleFor | `Bridge.<type>.handleFor()`} instead.
2231
+ *
2232
+ * @internal */
2233
+ constructor(bridge, object) {
2234
+ this.isClosed = false;
2235
+ this.bridge = bridge;
2236
+ this.objectWeakRef = new WeakRef(object);
2237
+ }
2238
+ /** The type of this handle's bridge */
2239
+ get type() {
2240
+ return this.bridge.type;
2241
+ }
2242
+ /**
2243
+ * Returns the pointer associated with this handle.
2244
+ *
2245
+ * @throws {Error} if the object has already been closed, garbage collected,
2246
+ * or unregistered from the bridge.
2247
+ */
2248
+ deref() {
2249
+ const object = this.objectWeakRef.deref();
2250
+ if (object == null) {
2251
+ throw new Error(`Bridging error: can't get pointer for an object that has been garbage collected.`);
2252
+ }
2253
+ if (this.isClosed) {
2254
+ throw new Error(`Bridging error: can't get pointer for an object that has been closed.`);
2255
+ }
2256
+ // Instead of storing the pointer in the handle, we look it up in the
2257
+ // bridge's pointer map to avoid storing the pointer twice.
2258
+ const pointer = this.bridge.rawPointerFor(object);
2259
+ if (pointer == null) {
2260
+ throw new Error(`Bridging error: ${this.type.name} object is not currently registered in this bridge.`);
2261
+ }
2262
+ return pointer;
2263
+ }
2264
+ /**
2265
+ * Returns the pointer associated with this handle or `null` if the object
2266
+ * has been closed, garbage collected, or unregistered.
2267
+ */
2268
+ derefOrNull() {
2269
+ const object = this.objectWeakRef.deref();
2270
+ if (object == null) {
2271
+ return null;
2272
+ }
2273
+ if (this.isClosed) {
2274
+ return null;
2275
+ }
2276
+ const pointer = this.bridge.rawPointerFor(object);
2277
+ return pointer !== null && pointer !== void 0 ? pointer : null;
2278
+ }
2279
+ /**
2280
+ * Returns the object associated with this handle.
2281
+ *
2282
+ * @throws {Error} if the object has already been garbage collected,
2283
+ * closed or unregistered.
2284
+ */
2285
+ object() {
2286
+ const object = this.objectWeakRef.deref();
2287
+ if (object == null) {
2288
+ throw new Error(`Bridging error: ${this.bridge.type.name} object has already been garbage collected.`);
2289
+ }
2290
+ if (this.isClosed) {
2291
+ throw new Error(`Bridging error: ${this.bridge.type.name} object has already been closed.`);
2292
+ }
2293
+ return object;
2294
+ }
2295
+ /**
2296
+ * Returns the object associated with this handle or `null` if the object
2297
+ * has already been garbage collected.
2298
+ */
2299
+ objectOrNull() {
2300
+ var _b;
2301
+ if (this.isClosed) {
2302
+ throw new Error(`Bridging error: ${this.bridge.type.name} object has already been closed.`);
2303
+ }
2304
+ return (_b = this.objectWeakRef.deref()) !== null && _b !== void 0 ? _b : null;
2233
2305
  }
2234
2306
  toString() {
2235
- return `{ Meta | type: ${this.type.name}, object: ${this.object.deref()}, FFI address: ${this.pointer.addr}, FFI type: ${this.pointer.type} }`;
2307
+ const pointer = this.derefOrNull();
2308
+ return `{ Handle | type: ${this.bridge.type}, object: ${this.objectWeakRef.deref()}, FFI address: ${pointer === null || pointer === void 0 ? void 0 : pointer.addr}, FFI type: ${pointer === null || pointer === void 0 ? void 0 : pointer.type} }`;
2309
+ }
2310
+ }
2311
+ /**
2312
+ * Use this for passing arrays of pointers to the FFI.
2313
+ */
2314
+ // REFACTOR: Use an iterator instead of an array to deref handles lazily.
2315
+ class Handles {
2316
+ /**
2317
+ * @throws {Error} if any of the objects are not registered in the bridge.
2318
+ * @throws {Error} if any of the objects have already been garbage collected.
2319
+ */
2320
+ constructor(bridge, objects) {
2321
+ this.handles = objects.map((object) => bridge.handleFor(object));
2322
+ }
2323
+ deref() {
2324
+ return this.handles.map((handle) => handle.deref());
2236
2325
  }
2237
2326
  }
2238
2327
  /**
@@ -2254,9 +2343,9 @@ class Meta {
2254
2343
  * - {@link StaticTCPClient}: `Bridge.staticTCPClient`
2255
2344
  * - {@link WebsocketClient}: `Bridge.websocketClient`
2256
2345
  *
2257
- * Use `Bridge.<type>.pointerFor()` to lookup the pointer for a given object and
2258
- * `Bridge.<type>.bridge()` to get or create the matching object for a memory
2259
- * address.
2346
+ * Use `Bridge.<type>.handleFor()` to obtain a handle, which is a wrapper around
2347
+ * the raw pointer, and `Bridge.<type>.bridge()` to get or create the matching
2348
+ * object for a memory address.
2260
2349
  *
2261
2350
  * @internal */
2262
2351
  class Bridge {
@@ -2279,12 +2368,12 @@ class Bridge {
2279
2368
  /**
2280
2369
  * Look up a pointer given its object.
2281
2370
  *
2282
- * As `WeakMap` does not allow for enumeration, use `this.metasByAddress` to
2371
+ * As `WeakMap` does not allow for enumeration, use `this.handlesByAddress` to
2283
2372
  * iterate over all pointers.
2284
2373
  */
2285
2374
  this.pointerByObject = new WeakMap();
2286
2375
  this.release = release;
2287
- this.metasByAddress = {};
2376
+ this.handlesByAddress = {};
2288
2377
  this.finalizationRegistry = new FinalizationRegistry(this.finalize.bind(this));
2289
2378
  Bridge.all.push(new WeakRef(this));
2290
2379
  }
@@ -2314,20 +2403,32 @@ class Bridge {
2314
2403
  this.internalType = value;
2315
2404
  }
2316
2405
  /**
2317
- * Returns the FFI pointer for `object`.
2406
+ * Returns the handle for a bridged object.
2318
2407
  *
2319
- * Throws an exception if object is currently not registered, which can be the
2320
- * case when object has never been registered or when it has already been
2321
- * unregistered.
2408
+ * Use `handle.deref()` to get the pointer for the object at the time of use.
2409
+ *
2410
+ * @throws {Error} if the object is not registered.
2322
2411
  *
2323
2412
  * @internal
2324
2413
  */
2325
- pointerFor(object) {
2326
- const val = this.pointerByObject.get(object);
2327
- if (val == null) {
2414
+ handleFor(object) {
2415
+ const pointer = this.pointerByObject.get(object);
2416
+ if (pointer == null) {
2328
2417
  throw new Error(`Bridging error: ${this.type.name} object is not currently registered in this bridge.`);
2329
2418
  }
2330
- return val;
2419
+ const handle = this.handlesByAddress[pointer.addr];
2420
+ if (handle == null) {
2421
+ throw new Error(`Internal inconsistency, found a pointer for an object whose handle is undefined: ${pointer}`);
2422
+ }
2423
+ return handle;
2424
+ }
2425
+ /**
2426
+ * Returns a `Handles` instance for an array of objects.
2427
+ *
2428
+ * @internal
2429
+ */
2430
+ handlesFor(objects) {
2431
+ return new Handles(this, objects);
2331
2432
  }
2332
2433
  /**
2333
2434
  * Convenience method, returns the object for the FFI `pointer` if registered,
@@ -2337,15 +2438,14 @@ class Bridge {
2337
2438
  * @internal
2338
2439
  */
2339
2440
  objectFor(pointer) {
2340
- const meta = this.metasByAddress[pointer.addr];
2341
- if (!meta)
2441
+ const handle = this.handlesByAddress[pointer.addr];
2442
+ if (!handle)
2342
2443
  return undefined;
2343
- if (meta.type !== this.type)
2344
- throw new Error(`Can't return object for pointer, pointer is associated with an object of type ${meta.type} but this bridge is configured for ${this.type}`);
2345
- const object = meta.object.deref();
2346
- if (!object)
2347
- throw new Error(`Internal inconsistency, found a meta entry for an object whose object is undefined (garbage collected): ${pointer}`);
2348
- return object;
2444
+ if (handle.type !== this.type)
2445
+ throw new Error(`Can't return object for pointer, pointer is associated with an object of type ${handle.type} but this bridge is configured for ${this.type}`);
2446
+ // This throws an error if the object has been garbage collected but the
2447
+ // finalizer has not been called yet.
2448
+ return handle.object();
2349
2449
  }
2350
2450
  /**
2351
2451
  * Returns the object for the FFI `pointer` if registered. Otherwise, calls
@@ -2356,6 +2456,7 @@ class Bridge {
2356
2456
  *
2357
2457
  * @param pointer reference to the FFi instance for the object
2358
2458
  * @param objectOrCreate can either be the JS object, or a function that returns the instance when called. If undefined, an object is created based on the Bridge type.
2459
+ * @throws {Error} if `objectOrCreate` is a function that returns an object that is not an instance of the bridge's type.
2359
2460
  * @internal
2360
2461
  */
2361
2462
  bridge(pointer, objectOrCreate) {
@@ -2393,26 +2494,49 @@ class Bridge {
2393
2494
  * @private */
2394
2495
  register(object, pointer) {
2395
2496
  const objectType = object.constructor;
2396
- const bridgeType = this.type;
2397
- if (object.constructor !== this.type)
2398
- throw new Error(`Can't register, bridge is configured for type ${bridgeType.name} but passed in object is of type ${objectType.name}`);
2497
+ if (objectType !== this.type)
2498
+ throw new Error(`Can't register, bridge is configured for type ${this.type.name} but passed in object is of type ${objectType.name}`);
2399
2499
  const existingPointer = this.pointerByObject.get(object);
2400
- const existingMeta = this.metasByAddress[pointer.addr];
2401
- // Check that both pointer and meta are undefined at this point.
2402
- if (existingPointer != null && existingMeta != null)
2403
- throw new Error(`Can't register, an object for the passed in pointer has previously been registered: ${existingMeta.pointer}`);
2404
- if (existingPointer != null && existingMeta == null)
2405
- throw new Error(`Internal inconsistency, trying to register an object which has an associated pointer but no meta entry: ${objectType.name} at ${existingPointer.type} ${existingPointer.addr}`);
2406
- if (existingPointer == null && existingMeta != null)
2407
- throw new Error(`Internal inconsistency, trying to register an object which has a meta entry but no associated pointer: ${objectType.name} ${object}`);
2408
- const meta = new Meta(this.type, new WeakRef(object), pointer);
2409
- this.metasByAddress[meta.pointer.addr] = meta;
2500
+ const existingHandle = this.handlesByAddress[pointer.addr];
2501
+ // Check that both pointer and handle are undefined at this point.
2502
+ if (existingPointer != null && existingHandle != null)
2503
+ throw new Error(`Can't register, an object for the passed in pointer has previously been registered: ${existingHandle.object()}`);
2504
+ if (existingPointer != null && existingHandle == null)
2505
+ throw new Error(`Internal inconsistency, trying to register an object which has an associated pointer but no handle entry: ${objectType.name} at ${existingPointer.type} ${existingPointer.addr}`);
2506
+ if (existingPointer == null && existingHandle != null)
2507
+ throw new Error(`Internal inconsistency, trying to register an object which has a handle entry but no associated pointer: ${objectType.name} ${object}`);
2508
+ const handle = new Handle(this, object);
2509
+ this.handlesByAddress[pointer.addr] = handle;
2410
2510
  this.pointerByObject.set(object, pointer);
2411
2511
  this.finalizationRegistry.register(object, pointer, object);
2412
2512
  if (DEBUG_TYPE_NAMES.includes(this.type.name) || DEBUG_ALL_TYPES) {
2413
- console.log(`[VERBOSE] Bridge REGISTERED a new instance of ${this.type.name}, current count: ${Object.keys(this.metasByAddress).length}`);
2513
+ console.log(`[VERBOSE] Bridge REGISTERED a new instance of ${this.type.name}, current count: ${Object.keys(this.handlesByAddress).length}`);
2414
2514
  }
2415
2515
  }
2516
+ /**
2517
+ * Marks the object as closed while still keeping it registered in the
2518
+ * {@link FinalizationRegistry}, such that it still gets finalized by GC.
2519
+ * Otherwise, the behavior is exactly as if the object would've been
2520
+ * unregistered.
2521
+ *
2522
+ * @internal */
2523
+ markAsClosed(object) {
2524
+ const objectType = object.constructor;
2525
+ const bridgeType = this.type;
2526
+ if (objectType !== bridgeType)
2527
+ throw new Error(`Can't unregister, bridge is configured for type ${bridgeType.name} but passed in object is of type ${objectType.name}`);
2528
+ const pointer = this.pointerByObject.get(object);
2529
+ if (pointer == null)
2530
+ throw new Error(`Can't unregister, object that has not been registered before: ${object}`);
2531
+ const handle = this.handlesByAddress[pointer.addr];
2532
+ if (handle == null)
2533
+ throw new Error(`Internal inconsistency, trying to unregister an object which has an associated pointer but no handle: ${object}`);
2534
+ if (handle.type !== bridgeType)
2535
+ throw new Error(`Internal inconsistency, trying to unregister an object that has a handle with a different type than that of the bridge: ${handle}`);
2536
+ if (handle.objectOrNull() !== object)
2537
+ throw new Error(`Internal inconsistency, trying to unregister an object whose associated handle holds a different object: ${handle}`);
2538
+ handle['isClosed'] = true;
2539
+ }
2416
2540
  /**
2417
2541
  * Removes an instance from this bridge's {@link FinalizationRegistry}.
2418
2542
  *
@@ -2429,20 +2553,18 @@ class Bridge {
2429
2553
  const pointer = this.pointerByObject.get(object);
2430
2554
  if (pointer == null)
2431
2555
  throw new Error(`Can't unregister, object that has not been registered before: ${object}`);
2432
- const meta = this.metasByAddress[pointer.addr];
2433
- if (meta == null)
2434
- throw new Error(`Internal inconsistency, trying to unregister an object which has an associated pointer but no meta entry: ${object}`);
2435
- if (meta.type !== bridgeType)
2436
- throw new Error(`Internal inconsistency, trying to unregister an object that has a meta entry with a different type than that of the bridge: ${meta}`);
2437
- if (meta.object.deref() == null)
2438
- throw new Error(`Internal inconsistency, trying to unregister an object that has a meta entry whose object is undfined (garbage collected): ${meta}`);
2439
- if (meta.object.deref() !== object)
2440
- throw new Error(`Internal inconsistency, trying to unregister an object that has a meta entry holding a different object: ${meta}`);
2556
+ const handle = this.handlesByAddress[pointer.addr];
2557
+ if (handle == null)
2558
+ throw new Error(`Internal inconsistency, trying to unregister an object which has an associated pointer but no handle: ${object}`);
2559
+ if (handle.type !== bridgeType)
2560
+ throw new Error(`Internal inconsistency, trying to unregister an object that has a handle with a different type than that of the bridge: ${handle}`);
2561
+ if (handle.objectOrNull() !== object)
2562
+ throw new Error(`Internal inconsistency, trying to unregister an object whose associated handle holds a different object: ${handle}`);
2441
2563
  this.finalizationRegistry.unregister(object);
2442
- delete this.metasByAddress[pointer.addr];
2564
+ delete this.handlesByAddress[pointer.addr];
2443
2565
  this.pointerByObject.delete(object);
2444
2566
  if (DEBUG_TYPE_NAMES.includes(this.type.name) || DEBUG_ALL_TYPES) {
2445
- console.log(`[VERBOSE] Bridge UNREGISTERED an instance of ${this.type.name}, current count: ${Object.keys(this.metasByAddress).length}`);
2567
+ console.log(`[VERBOSE] Bridge UNREGISTERED an instance of ${this.type.name}, current count: ${Object.keys(this.handlesByAddress).length}`);
2446
2568
  }
2447
2569
  }
2448
2570
  /** @internal */
@@ -2450,8 +2572,8 @@ class Bridge {
2450
2572
  if (DEBUG_TYPE_NAMES.includes(this.type.name) || DEBUG_ALL_TYPES) {
2451
2573
  console.log(`[VERBOSE] Unregistering ALL bridged instances of type ${this.type.name}.`);
2452
2574
  }
2453
- for (const meta of Object.values(this.metasByAddress)) {
2454
- const object = meta.object.deref();
2575
+ for (const handle of Object.values(this.handlesByAddress)) {
2576
+ const object = handle.object();
2455
2577
  if (object) {
2456
2578
  this.unregister(object);
2457
2579
  }
@@ -2459,16 +2581,20 @@ class Bridge {
2459
2581
  }
2460
2582
  /** @internal */
2461
2583
  get count() {
2462
- return Object.keys(this.metasByAddress).length;
2584
+ return Object.keys(this.handlesByAddress).length;
2585
+ }
2586
+ /* @internal */
2587
+ rawPointerFor(object) {
2588
+ return this.pointerByObject.get(object);
2463
2589
  }
2464
2590
  finalize(pointer) {
2465
- const meta = this.metasByAddress[pointer.addr];
2466
- if (!meta)
2467
- throw new Error(`Internal inconsistency, tried to finalize an instance for a pointer, that has no meta entry: ${pointer}`);
2468
- delete this.metasByAddress[pointer.addr];
2469
- this.release(pointer);
2591
+ const handle = this.handlesByAddress[pointer.addr];
2592
+ if (!handle)
2593
+ throw new Error(`Internal inconsistency, tried to finalize an instance for a pointer, that has no handle: ${pointer}`);
2594
+ delete this.handlesByAddress[pointer.addr];
2595
+ this.release(pointer, handle);
2470
2596
  if (DEBUG_TYPE_NAMES.includes(this.type.name) || DEBUG_ALL_TYPES) {
2471
- console.log(`[VERBOSE] Bridge FINALIZED an instance of ${this.type.name}, current count: ${Object.keys(this.metasByAddress).length}`);
2597
+ console.log(`[VERBOSE] Bridge FINALIZED an instance of ${this.type.name}, current count: ${Object.keys(this.handlesByAddress).length}`);
2472
2598
  }
2473
2599
  }
2474
2600
  }
@@ -2490,11 +2616,14 @@ Bridge.staticTCPClient = new Bridge(staticTCPClientFreeHandle);
2490
2616
  /** @internal */
2491
2617
  Bridge.websocketClient = new Bridge(websocketClientFreeHandle);
2492
2618
  /** @internal */
2493
- Bridge.ditto = new Bridge((dittoPointer) => {
2619
+ Bridge.ditto = new Bridge(async (dittoPointer, handle) => {
2494
2620
  // HACK: quick and dirty, clear all presence callbacks. This covers presence
2495
2621
  // v1 and v2 callbacks. v3 should be cleared properly by the `Presence`
2496
2622
  // class itself.
2497
2623
  dittoClearPresenceCallback(dittoPointer);
2624
+ if (!handle.isClosed) {
2625
+ await dittoShutdown(dittoPointer);
2626
+ }
2498
2627
  dittoFree(dittoPointer);
2499
2628
  });
2500
2629
 
@@ -2551,13 +2680,16 @@ class Attachment {
2551
2680
  * Returns the attachment's data.
2552
2681
  */
2553
2682
  getData() {
2554
- {
2555
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
2556
- const attachmentHandlePointer = Bridge.attachment.pointerFor(this);
2557
- const attachmentPath = dittoGetCompleteAttachmentPath(dittoPointer, attachmentHandlePointer);
2558
- const fs = require('fs/promises');
2559
- return fs.readFile(attachmentPath);
2560
- }
2683
+ const ditto = this.ditto;
2684
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
2685
+ return this.ditto.deferCloseAsync(async () => {
2686
+ {
2687
+ const attachmentHandle = Bridge.attachment.handleFor(this);
2688
+ const attachmentPath = dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref());
2689
+ const fs = require('fs/promises');
2690
+ return await fs.readFile(attachmentPath);
2691
+ }
2692
+ });
2561
2693
  }
2562
2694
  /**
2563
2695
  * Copies the attachment to the specified file path. Node-only,
@@ -2566,15 +2698,18 @@ class Attachment {
2566
2698
  * @param path The path that the attachment should be copied to.
2567
2699
  */
2568
2700
  copyToPath(path) {
2569
- {
2570
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
2571
- const attachmentHandlePointer = Bridge.attachment.pointerFor(this);
2572
- const attachmentPath = dittoGetCompleteAttachmentPath(dittoPointer, attachmentHandlePointer);
2573
- const fs = require('fs/promises');
2574
- // If the file already exists, we fail. This is the same behavior as
2575
- // for the Swift/ObjC SDK.
2576
- return fs.copyFile(attachmentPath, path, fs.COPYFILE_EXCL);
2577
- }
2701
+ const ditto = this.ditto;
2702
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
2703
+ return this.ditto.deferCloseAsync(async () => {
2704
+ {
2705
+ const attachmentHandle = Bridge.attachment.handleFor(this);
2706
+ const attachmentPath = dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref());
2707
+ const fs = require('fs/promises');
2708
+ // If the file already exists, we fail. This is the same behavior as
2709
+ // for the Swift/ObjC SDK.
2710
+ return await fs.copyFile(attachmentPath, path, fs.COPYFILE_EXCL);
2711
+ }
2712
+ });
2578
2713
  }
2579
2714
  /** @internal */
2580
2715
  constructor(ditto, token) {
@@ -2719,7 +2854,10 @@ function validateQuery(query, options = {}) {
2719
2854
  return validatedQuery;
2720
2855
  }
2721
2856
  // -------------------------------------------------------------- Helpers ------
2722
- /** @internal */
2857
+ /**
2858
+ * Generate a random hex-encoded token using the WebCrypto API.
2859
+ *
2860
+ * @internal */
2723
2861
  function generateEphemeralToken() {
2724
2862
  let webcrypto = undefined;
2725
2863
  {
@@ -2738,9 +2876,9 @@ function sleep(milliseconds) {
2738
2876
  });
2739
2877
  }
2740
2878
  /** @internal use this to asyncify chunks of code. */
2741
- async function step(block) {
2879
+ async function step(closure) {
2742
2880
  await sleep(0);
2743
- return block();
2881
+ return closure();
2744
2882
  }
2745
2883
  /** @internal */
2746
2884
  // WORKAROUND: the corresponding FFI function(s) is not async at the
@@ -2999,8 +3137,8 @@ class DocumentPath {
2999
3137
  underlyingValueForPathType(pathType) {
3000
3138
  const path = this.path;
3001
3139
  const document = this.document;
3002
- const documentX = Bridge.document.pointerFor(document);
3003
- const cborPathResult = documentGetCBORWithPathType(documentX, path, pathType);
3140
+ const documentHandle = Bridge.document.handleFor(document);
3141
+ const cborPathResult = documentGetCBORWithPathType(documentHandle.deref(), path, pathType);
3004
3142
  return cborPathResult.cbor !== null ? CBOR.decode(cborPathResult.cbor) : undefined;
3005
3143
  }
3006
3144
  }
@@ -3103,8 +3241,8 @@ class MutableDocumentPath {
3103
3241
  underlyingValueForPathType(pathType) {
3104
3242
  const path = this.path;
3105
3243
  const document = this.mutableDocument;
3106
- const documentX = Bridge.mutableDocument.pointerFor(document);
3107
- const cborPathResult = documentGetCBORWithPathType(documentX, path, pathType);
3244
+ const documentHandle = Bridge.mutableDocument.handleFor(document);
3245
+ const cborPathResult = documentGetCBORWithPathType(documentHandle.deref(), path, pathType);
3108
3246
  return cborPathResult.cbor !== null ? CBOR.decode(cborPathResult.cbor) : undefined;
3109
3247
  }
3110
3248
  /** @internal */
@@ -3112,21 +3250,21 @@ class MutableDocumentPath {
3112
3250
  // REFACTOR: this body should probably move to
3113
3251
  // `MutableCounter.incrementBy()`. Keeping as-is for now until we implement
3114
3252
  // more explicit CRDT types.
3115
- const documentPointer = Bridge.mutableDocument.pointerFor(this.mutableDocument);
3116
- documentIncrementCounter(documentPointer, this.path, amount);
3253
+ const documentHandle = Bridge.mutableDocument.handleFor(this.mutableDocument);
3254
+ documentIncrementCounter(documentHandle.deref(), this.path, amount);
3117
3255
  const updateResult = UpdateResult.incremented(this.mutableDocument.id, this.path, amount);
3118
3256
  this.recordUpdateResult(updateResult);
3119
3257
  }
3120
3258
  /** @internal */
3121
3259
  '@ditto.set'(value, isDefault) {
3122
- const documentX = Bridge.mutableDocument.pointerFor(this.mutableDocument);
3260
+ const documentHandle = Bridge.mutableDocument.handleFor(this.mutableDocument);
3123
3261
  const valueJSON = desugarJSObject(value, false);
3124
3262
  const valueCBOR = CBOR.encode(valueJSON);
3125
3263
  if (isDefault) {
3126
- documentSetCBORWithTimestamp(documentX, this.path, valueCBOR, 0);
3264
+ documentSetCBORWithTimestamp(documentHandle.deref(), this.path, valueCBOR, 0);
3127
3265
  }
3128
3266
  else {
3129
- documentSetCBOR(documentX, this.path, valueCBOR);
3267
+ documentSetCBOR(documentHandle.deref(), this.path, valueCBOR);
3130
3268
  }
3131
3269
  // HACK: we need a copy of value because the original value can be mutated
3132
3270
  // later on and an update result needs the state of the value at this
@@ -3139,8 +3277,8 @@ class MutableDocumentPath {
3139
3277
  }
3140
3278
  /** @internal */
3141
3279
  '@ditto.remove'() {
3142
- const documentPointer = Bridge.mutableDocument.pointerFor(this.mutableDocument);
3143
- documentRemove(documentPointer, this.path);
3280
+ const documentHandle = Bridge.mutableDocument.handleFor(this.mutableDocument);
3281
+ documentRemove(documentHandle.deref(), this.path);
3144
3282
  this.updateInMemory((container, lastPathComponent) => {
3145
3283
  if (Array.isArray(container) && typeof lastPathComponent === 'number') {
3146
3284
  container.splice(lastPathComponent, 1);
@@ -3178,8 +3316,8 @@ class Document {
3178
3316
  */
3179
3317
  static hash(documentOrMany) {
3180
3318
  const documents = documentsFrom(documentOrMany);
3181
- const documentPointers = documents.map((doc) => Bridge.document.pointerFor(doc));
3182
- return documentsHash(documentPointers);
3319
+ const documentHandles = documents.map((doc) => Bridge.document.handleFor(doc));
3320
+ return documentsHash(documentHandles.map((handle) => handle.deref()));
3183
3321
  }
3184
3322
  /**
3185
3323
  * Returns a pattern of words that together create a mnemonic, which
@@ -3187,8 +3325,8 @@ class Document {
3187
3325
  */
3188
3326
  static hashMnemonic(documentOrMany) {
3189
3327
  const documents = documentsFrom(documentOrMany);
3190
- const documentPointers = documents.map((doc) => Bridge.document.pointerFor(doc));
3191
- return documentsHashMnemonic(documentPointers);
3328
+ const documentHandles = Bridge.document.handlesFor(documents);
3329
+ return documentsHashMnemonic(documentHandles.deref());
3192
3330
  }
3193
3331
  /**
3194
3332
  * Returns the document ID.
@@ -3196,8 +3334,8 @@ class Document {
3196
3334
  get id() {
3197
3335
  let id = this['@ditto.id'];
3198
3336
  if (typeof id === 'undefined') {
3199
- const documentX = Bridge.document.pointerFor(this);
3200
- const documentIDCBOR = documentID(documentX);
3337
+ const documentHandle = Bridge.document.handleFor(this);
3338
+ const documentIDCBOR = documentID(documentHandle.deref());
3201
3339
  id = new DocumentID(documentIDCBOR, true);
3202
3340
  this['@ditto.id'] = id;
3203
3341
  }
@@ -3233,8 +3371,8 @@ class Document {
3233
3371
  // TEMPORARY: helpers to deal with non-canonical IDs.
3234
3372
  /** @internal */
3235
3373
  static idCBOR(document) {
3236
- const documentX = Bridge.document.pointerFor(document);
3237
- return documentID(documentX);
3374
+ const documentHandle = Bridge.document.handleFor(document);
3375
+ return documentID(documentHandle.deref());
3238
3376
  }
3239
3377
  /** @internal */
3240
3378
  static canonicalizedIDCBOR(idCBOR) {
@@ -3269,8 +3407,8 @@ class MutableDocument {
3269
3407
  get id() {
3270
3408
  let id = this['@ditto.id'];
3271
3409
  if (typeof id === 'undefined') {
3272
- const documentX = Bridge.mutableDocument.pointerFor(this);
3273
- const documentIDCBOR = documentID(documentX);
3410
+ const documentHandle = Bridge.mutableDocument.handleFor(this);
3411
+ const documentIDCBOR = documentID(documentHandle.deref());
3274
3412
  id = new DocumentID(documentIDCBOR, true);
3275
3413
  this['@ditto.id'] = id;
3276
3414
  }
@@ -3302,8 +3440,8 @@ class MutableDocument {
3302
3440
  // TEMPORARY: helpers to deal with non-canonical IDs.
3303
3441
  /** @internal */
3304
3442
  static idCBOR(mutableDocument) {
3305
- const documentX = Bridge.mutableDocument.pointerFor(mutableDocument);
3306
- return documentID(documentX);
3443
+ const documentHandle = Bridge.mutableDocument.handleFor(mutableDocument);
3444
+ return documentID(documentHandle.deref());
3307
3445
  }
3308
3446
  }
3309
3447
  /** @internal */
@@ -3454,13 +3592,16 @@ class BasePendingCursorOperation {
3454
3592
  * the query generated by the preceding function chaining.
3455
3593
  */
3456
3594
  async exec() {
3457
- const query = this.query;
3458
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
3459
- const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3460
- return await collectionExecQueryStr(dittoX, this.collection.name, null, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
3461
- });
3462
- return documentsX.map((documentX) => {
3463
- return Bridge.document.bridge(documentX);
3595
+ const ditto = this.collection.store.ditto;
3596
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3597
+ return ditto.deferCloseAsync(async () => {
3598
+ const query = this.query;
3599
+ const documentPointers = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3600
+ return await collectionExecQueryStr(dittoHandle.deref(), this.collection.name, null, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
3601
+ });
3602
+ return documentPointers.map((documentPointer) => {
3603
+ return Bridge.document.bridge(documentPointer);
3604
+ });
3464
3605
  });
3465
3606
  }
3466
3607
  // ----------------------------------------------------------- Internal ------
@@ -3479,37 +3620,40 @@ class BasePendingCursorOperation {
3479
3620
  * @internal
3480
3621
  */
3481
3622
  async updateWithTransaction(closure, writeTransactionX) {
3482
- return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3483
- const query = this.query;
3484
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
3485
- const documentsX = await collectionExecQueryStr(dittoX, this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
3486
- const mutableDocuments = documentsX.map((documentX) => {
3487
- return Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
3488
- });
3489
- closure(mutableDocuments);
3490
- const updateResultsDocumentIDs = [];
3491
- const updateResultsByDocumentIDString = {};
3492
- for (const mutableDocument of mutableDocuments) {
3493
- const documentID = mutableDocument.id;
3494
- const documentIDString = documentID.toString();
3495
- const updateResults = mutableDocument['@ditto.updateResults'];
3496
- if (updateResultsByDocumentIDString[documentIDString]) {
3497
- // HACK: in theory, 2 different document IDs can have the exact
3498
- // same string representation at the time of this writing. There is
3499
- // already a REFACTOR comment in `UpdateResultsMap` to implement a
3500
- // proper and correct data structure for holding these update results.
3501
- // Until then, we'll leave this check here to see how often we hit
3502
- // this edge case.
3503
- throw new Error(`Internal inconsistency, update results for document ID as string already exist: ${documentIDString}`);
3623
+ const ditto = this.collection.store.ditto;
3624
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3625
+ return ditto.deferCloseAsync(async () => {
3626
+ return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3627
+ const query = this.query;
3628
+ const documentsX = await collectionExecQueryStr(dittoHandle.deref(), this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
3629
+ const mutableDocuments = documentsX.map((documentX) => {
3630
+ return Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
3631
+ });
3632
+ closure(mutableDocuments);
3633
+ const updateResultsDocumentIDs = [];
3634
+ const updateResultsByDocumentIDString = {};
3635
+ for (const mutableDocument of mutableDocuments) {
3636
+ const documentID = mutableDocument.id;
3637
+ const documentIDString = documentID.toString();
3638
+ const updateResults = mutableDocument['@ditto.updateResults'];
3639
+ if (updateResultsByDocumentIDString[documentIDString]) {
3640
+ // HACK: in theory, 2 different document IDs can have the exact
3641
+ // same string representation at the time of this writing. There is
3642
+ // already a REFACTOR comment in `UpdateResultsMap` to implement a
3643
+ // proper and correct data structure for holding these update results.
3644
+ // Until then, we'll leave this check here to see how often we hit
3645
+ // this edge case.
3646
+ throw new Error(`Internal inconsistency, update results for document ID as string already exist: ${documentIDString}`);
3647
+ }
3648
+ updateResultsDocumentIDs.push(documentID);
3649
+ updateResultsByDocumentIDString[documentIDString] = updateResults;
3650
+ Bridge.mutableDocument.unregister(mutableDocument);
3504
3651
  }
3505
- updateResultsDocumentIDs.push(documentID);
3506
- updateResultsByDocumentIDString[documentIDString] = updateResults;
3507
- Bridge.mutableDocument.unregister(mutableDocument);
3508
- }
3509
- // NOTE: ownership of documentsX (and contained documents)
3510
- // is transferred to Rust at this point.
3511
- await collectionUpdateMultiple(dittoX, this.collection.name, writeTransactionX, documentsX);
3512
- return new UpdateResultsMap(updateResultsDocumentIDs, updateResultsByDocumentIDString);
3652
+ // NOTE: ownership of documentsX (and contained documents)
3653
+ // is transferred to Rust at this point.
3654
+ await collectionUpdateMultiple(dittoHandle.deref(), this.collection.name, writeTransactionX, documentsX);
3655
+ return new UpdateResultsMap(updateResultsDocumentIDs, updateResultsByDocumentIDString);
3656
+ });
3513
3657
  });
3514
3658
  }
3515
3659
  /** @internal */
@@ -3551,15 +3695,18 @@ class BasePendingIDSpecificOperation {
3551
3695
  * not found.
3552
3696
  */
3553
3697
  async exec() {
3554
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
3555
- return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3556
- const readTransactionX = await readTransaction(dittoX);
3557
- const documentX = await collectionGet(dittoX, this.collection.name, this.documentIDCBOR, readTransactionX);
3558
- let document = undefined;
3559
- if (documentX)
3560
- document = Bridge.document.bridge(documentX);
3561
- readTransactionFree(readTransactionX);
3562
- return document;
3698
+ const ditto = this.collection.store.ditto;
3699
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3700
+ return ditto.deferCloseAsync(async () => {
3701
+ return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
3702
+ const readTransactionX = await readTransaction(dittoHandle.deref());
3703
+ const documentX = await collectionGet(dittoHandle.deref(), this.collection.name, this.documentIDCBOR, readTransactionX);
3704
+ let document = undefined;
3705
+ if (documentX)
3706
+ document = Bridge.document.bridge(documentX);
3707
+ readTransactionFree(readTransactionX);
3708
+ return document;
3709
+ });
3563
3710
  });
3564
3711
  }
3565
3712
  // ----------------------------------------------------------- Internal ------
@@ -3591,6 +3738,7 @@ class ObserverManager {
3591
3738
  const process = (_d = options.process) !== null && _d !== void 0 ? _d : null;
3592
3739
  this.id = id;
3593
3740
  this.keepAlive = keepAlive;
3741
+ this.isClosed = false;
3594
3742
  this.isRegistered = false;
3595
3743
  this.callbacksByToken = {};
3596
3744
  if (register !== null) {
@@ -3606,6 +3754,11 @@ class ObserverManager {
3606
3754
  /** @internal */
3607
3755
  addObserver(callback) {
3608
3756
  var _a;
3757
+ if (this.isClosed) {
3758
+ // REFACTOR: throw a catchable error here, such that calling code
3759
+ // can be more specific when forwarding it to the user.
3760
+ throw new Error(`Internal inconsistency, can't add '${this.id}' observer, observer mananger close()-ed.`);
3761
+ }
3609
3762
  this.registerIfNeeded();
3610
3763
  const token = generateEphemeralToken();
3611
3764
  this.callbacksByToken[token] = callback;
@@ -3615,18 +3768,45 @@ class ObserverManager {
3615
3768
  /** @internal */
3616
3769
  removeObserver(token) {
3617
3770
  var _a;
3618
- delete this.callbacksByToken[token];
3771
+ const callback = this.callbacksByToken[token];
3772
+ if (typeof callback === 'undefined') {
3773
+ throw new Error(`Can't remove '${this.id}' observer, token '${token}' has never been registered before.`);
3774
+ }
3775
+ if (callback === null) {
3776
+ // Observer has already been removed, no-op.
3777
+ return;
3778
+ }
3779
+ // REFACTOR: not deleting the token here will keep eating up
3780
+ // memory over long periods of time. We actually need to track
3781
+ // the observer objects themselves and remove it from the table
3782
+ // as soon as the observer object is garbage collected.
3783
+ this.callbacksByToken[token] = null;
3619
3784
  (_a = this.keepAlive) === null || _a === void 0 ? void 0 : _a.release(`${this.id}.${token}`);
3620
3785
  this.unregisterIfNeeded();
3621
3786
  }
3787
+ hasObserver(token) {
3788
+ return typeof this.callbacksByToken[token] != 'undefined';
3789
+ }
3622
3790
  /** @internal */
3623
3791
  notify(...args) {
3792
+ if (this.isClosed) {
3793
+ // NOTE: we don't notify observers after closing and just swallow
3794
+ // the event.
3795
+ return;
3796
+ }
3624
3797
  const processedArgs = this.process(...args);
3625
3798
  for (const token in this.callbacksByToken) {
3626
3799
  const callback = this.callbacksByToken[token];
3627
3800
  callback(...processedArgs);
3628
3801
  }
3629
3802
  }
3803
+ /** @internal */
3804
+ close() {
3805
+ this.isClosed = true;
3806
+ for (const token in this.callbacksByToken) {
3807
+ this.removeObserver(token);
3808
+ }
3809
+ }
3630
3810
  /**
3631
3811
  * Can be injected and replaced via constructor options.
3632
3812
  *
@@ -3676,9 +3856,6 @@ class ObserverManager {
3676
3856
  this.unregister();
3677
3857
  }
3678
3858
  }
3679
- async finalize(token) {
3680
- await this.removeObserver(token);
3681
- }
3682
3859
  }
3683
3860
 
3684
3861
  //
@@ -3748,39 +3925,65 @@ class Authenticator {
3748
3925
  '@ditto.authClientValidityChanged'(isWebValid, isX509Valid) {
3749
3926
  throw new Error(`Authenticator['@ditto.authClientValidityChanged']() is abstract and must be implemented by subclasses.`);
3750
3927
  }
3928
+ /** @internal */
3929
+ close() {
3930
+ this.observerManager.close();
3931
+ }
3751
3932
  }
3752
3933
  // -----------------------------------------------------------------------------
3753
3934
  /** @internal */
3754
3935
  class OnlineAuthenticator extends Authenticator {
3755
3936
  async loginWithToken(token, provider) {
3756
- await dittoAuthClientLoginWithToken(this.authClientPointer, token, provider);
3937
+ const ditto = this.ditto.deref();
3938
+ if (!ditto) {
3939
+ return;
3940
+ }
3941
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3942
+ const dittoPointer = dittoHandle.derefOrNull();
3943
+ if (!dittoPointer) {
3944
+ return;
3945
+ }
3946
+ return ditto.deferCloseAsync(async () => {
3947
+ await dittoAuthClientLoginWithToken(dittoPointer, token, provider);
3948
+ });
3757
3949
  }
3758
3950
  async loginWithUsernameAndPassword(username, password, provider) {
3759
- await dittoAuthClientLoginWithUsernameAndPassword(this.authClientPointer, username, password, provider);
3951
+ const ditto = this.ditto.deref();
3952
+ if (!ditto) {
3953
+ return;
3954
+ }
3955
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3956
+ const dittoPointer = dittoHandle.derefOrNull();
3957
+ if (!dittoPointer) {
3958
+ return;
3959
+ }
3960
+ return ditto.deferCloseAsync(async () => {
3961
+ await dittoAuthClientLoginWithUsernameAndPassword(dittoPointer, username, password, provider);
3962
+ });
3760
3963
  }
3761
3964
  async logout(cleanupFn) {
3762
3965
  const ditto = this.ditto.deref();
3763
- if (ditto) {
3764
- await dittoAuthClientLogout(this.authClientPointer);
3765
- ditto.stopSync();
3766
- cleanupFn === null || cleanupFn === void 0 ? void 0 : cleanupFn(this.ditto);
3966
+ if (!ditto) {
3967
+ return;
3767
3968
  }
3768
- else {
3769
- Logger.warning('Unable to logout, related Ditto object does not exist anymore.');
3969
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
3970
+ const dittoPointer = dittoHandle.derefOrNull();
3971
+ if (!dittoPointer) {
3972
+ return;
3770
3973
  }
3974
+ return ditto.deferCloseAsync(async () => {
3975
+ await dittoAuthClientLogout(dittoPointer);
3976
+ ditto.stopSync();
3977
+ cleanupFn === null || cleanupFn === void 0 ? void 0 : cleanupFn(this.ditto);
3978
+ });
3771
3979
  }
3772
- constructor(keepAlive, authClientPointer, ditto, authenticationHandler) {
3980
+ constructor(keepAlive, ditto, authenticationHandler) {
3773
3981
  super(keepAlive);
3774
3982
  this['loginSupported'] = true;
3775
3983
  this['status'] = { isAuthenticated: false, userID: null };
3776
- this.authClientPointer = authClientPointer;
3777
3984
  this.ditto = new WeakRef(ditto);
3778
3985
  this.authenticationHandler = authenticationHandler;
3779
3986
  this.updateAndNotify(false);
3780
- // NOTE: The ownership of the auth client is transferred to us (from Ditto),
3781
- // so we have to free the auth client once this instance is garbage
3782
- // collected.
3783
- OnlineAuthenticator.finalizationRegistry.register(this, authClientPointer);
3784
3987
  }
3785
3988
  '@ditto.authenticationExpiring'(secondsRemaining) {
3786
3989
  const authenticationHandler = this.authenticationHandler;
@@ -3796,10 +3999,21 @@ class OnlineAuthenticator extends Authenticator {
3796
3999
  }
3797
4000
  updateAndNotify(shouldNotify) {
3798
4001
  var _a;
4002
+ const ditto = this.ditto.deref();
4003
+ if (!ditto) {
4004
+ Logger.debug('Unable to update auth status and notify, related Ditto object does not exist anymore.');
4005
+ return;
4006
+ }
4007
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4008
+ const dittoPointer = dittoHandle.derefOrNull();
4009
+ if (!dittoPointer) {
4010
+ Logger.debug('Unable to update auth status and notify, related Ditto object does not exist anymore.');
4011
+ return;
4012
+ }
3799
4013
  const wasAuthenticated = this.status.isAuthenticated;
3800
4014
  const previousUserID = this.status.userID;
3801
- const isAuthenticated = dittoAuthClientIsWebValid(this.authClientPointer);
3802
- const userID = dittoAuthClientUserID(this.authClientPointer);
4015
+ const isAuthenticated = dittoAuthClientIsWebValid(dittoPointer);
4016
+ const userID = dittoAuthClientUserID(dittoPointer);
3803
4017
  const status = { isAuthenticated, userID };
3804
4018
  this['status'] = status;
3805
4019
  if (shouldNotify) {
@@ -3810,11 +4024,7 @@ class OnlineAuthenticator extends Authenticator {
3810
4024
  }
3811
4025
  }
3812
4026
  }
3813
- static finalize(authClientPointer) {
3814
- dittoAuthClientFree(authClientPointer);
3815
- }
3816
4027
  }
3817
- OnlineAuthenticator.finalizationRegistry = new FinalizationRegistry(OnlineAuthenticator.finalize);
3818
4028
  // -----------------------------------------------------------------------------
3819
4029
  /** @internal */
3820
4030
  class NotAvailableAuthenticator extends Authenticator {
@@ -3980,94 +4190,6 @@ class TransportConfig {
3980
4190
  }
3981
4191
  }
3982
4192
 
3983
- //
3984
- /**
3985
- * These objects are returned by calls to
3986
- * {@link Collection.fetchAttachment | fetchAttachment()} on {@link Collection}
3987
- * and allow you to stop an in-flight attachment fetch.
3988
- */
3989
- class AttachmentFetcher {
3990
- /**
3991
- * Stops fetching the associated attachment and cleans up any associated
3992
- * resources.
3993
- *
3994
- * Note that you are not required to call `stop()` once your attachment fetch
3995
- * operation has finished. The method primarily exists to allow you to cancel
3996
- * an attachment fetch request while it is ongoing if you no longer wish for
3997
- * the attachment to be made available locally to the device.
3998
- */
3999
- stop() {
4000
- AttachmentFetcher.stopWithContextInfo({
4001
- ditto: this.ditto,
4002
- attachmentTokenID: this.token.id,
4003
- cancelTokenPromise: this.cancelTokenPromise,
4004
- });
4005
- }
4006
- /** @internal */
4007
- then(onfulfilled, onrejected) {
4008
- return this.attachment.then(onfulfilled, onrejected);
4009
- }
4010
- /** @internal */
4011
- constructor(ditto, token, eventHandler) {
4012
- this.ditto = ditto;
4013
- this.token = token;
4014
- this.eventHandler = eventHandler || null;
4015
- this.cancelTokenPromise = null;
4016
- const eventHandlerOrNoOp = eventHandler || function () { };
4017
- this.attachment = new Promise((resolve, reject) => {
4018
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
4019
- // not awaited yet; only needs to be fully resolved (and `await`ed over)
4020
- // when using the actual `cancelTokenPromise`, which only happens in `stop()`.
4021
- this.cancelTokenPromise = dittoResolveAttachment(dittoPointer, token.id, {
4022
- onComplete: (attachmentHandlePointer) => {
4023
- this['eventHandler'] = null;
4024
- this['cancelTokenPromise'] = null;
4025
- const attachment = new Attachment(this.ditto, this.token);
4026
- Bridge.attachment.bridge(attachmentHandlePointer, () => attachment);
4027
- eventHandlerOrNoOp({ type: 'Completed', attachment });
4028
- resolve(attachment);
4029
- },
4030
- onProgress: (downloaded, toDownload) => {
4031
- eventHandlerOrNoOp({ type: 'Progress', totalBytes: toDownload, downloadedBytes: downloaded });
4032
- },
4033
- onDelete: () => {
4034
- this['eventHandler'] = null;
4035
- this['cancelTokenPromise'] = null;
4036
- eventHandlerOrNoOp({ type: 'Deleted' });
4037
- resolve(null);
4038
- },
4039
- },
4040
- /* onError: */ (err) => {
4041
- this['eventHandler'] = null;
4042
- this['cancelTokenPromise'] = null;
4043
- // REFACTOR: We simply throw this error at the moment. Maybe we should
4044
- // bubble it up?
4045
- reject(err);
4046
- });
4047
- });
4048
- const contextInfo = {
4049
- ditto: ditto,
4050
- attachmentTokenID: token.id,
4051
- cancelTokenPromise: this.cancelTokenPromise,
4052
- };
4053
- AttachmentFetcher.finalizationRegistry.register(this, contextInfo, contextInfo);
4054
- }
4055
- static stopWithContextInfo(contextInfo) {
4056
- // No need for synchronicity, here: we let the "stop promise", float / run
4057
- // in a detached fashion since there is no point in awaiting the
4058
- // "stop signaling" itself.
4059
- step(async () => {
4060
- // await the initial resolution of `FFI.dittoResolveAttachment` below.
4061
- const cancelToken = await contextInfo.cancelTokenPromise;
4062
- if (cancelToken) {
4063
- const dittoPointer = Bridge.ditto.pointerFor(contextInfo.ditto);
4064
- dittoCancelResolveAttachment(dittoPointer, contextInfo.attachmentTokenID, cancelToken);
4065
- }
4066
- });
4067
- }
4068
- }
4069
- AttachmentFetcher.finalizationRegistry = new FinalizationRegistry(AttachmentFetcher.stopWithContextInfo);
4070
-
4071
4193
  //
4072
4194
  /**
4073
4195
  * Used to subscribe to receive updates from remote peers about matching
@@ -4090,7 +4212,7 @@ class Subscription {
4090
4212
  cancel() {
4091
4213
  if (!this.isCancelled) {
4092
4214
  this['isCancelled'] = true;
4093
- Subscription.remove(this, this.contextInfo);
4215
+ this.manager.remove(this);
4094
4216
  }
4095
4217
  }
4096
4218
  // ----------------------------------------------------------- Internal ------
@@ -4106,7 +4228,7 @@ class Subscription {
4106
4228
  this.queryArgsCBOR = queryArgsCBOR;
4107
4229
  this.collection = collection;
4108
4230
  this.contextInfo = {
4109
- ditto: collection.store.ditto,
4231
+ id: generateEphemeralToken(),
4110
4232
  collectionName: collection.name,
4111
4233
  query,
4112
4234
  queryArgsCBOR,
@@ -4114,23 +4236,10 @@ class Subscription {
4114
4236
  limit,
4115
4237
  offset,
4116
4238
  };
4117
- Subscription.add(this, this.contextInfo);
4118
- }
4119
- static add(subscription, contextInfo) {
4120
- const dittoX = Bridge.ditto.pointerFor(contextInfo.ditto);
4121
- addSubscription(dittoX, contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset);
4122
- this.finalizationRegistry.register(subscription, contextInfo, contextInfo);
4239
+ this.manager = collection.store.ditto.subscriptionManager;
4240
+ this.manager.add(this);
4123
4241
  }
4124
- static remove(subscription, contextInfo) {
4125
- const dittoX = Bridge.ditto.pointerFor(contextInfo.ditto);
4126
- removeSubscription(dittoX, contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset);
4127
- if (subscription)
4128
- this.finalizationRegistry.unregister(contextInfo);
4129
- }
4130
- }
4131
- Subscription.finalizationRegistry = new FinalizationRegistry((contextInfo) => {
4132
- Subscription.remove(null, contextInfo);
4133
- });
4242
+ }
4134
4243
 
4135
4244
  //
4136
4245
  // -----------------------------------------------------------------------------
@@ -4150,14 +4259,16 @@ class LiveQueryEventInitial {
4150
4259
  * Returns a hash that represents the set of matching documents.
4151
4260
  */
4152
4261
  hash(documents) {
4153
- return documentsHash(documents.map((doc) => Bridge.document.pointerFor(doc)));
4262
+ const documentHandles = Bridge.document.handlesFor(documents);
4263
+ return documentsHash(documentHandles.deref());
4154
4264
  }
4155
4265
  /**
4156
4266
  * Returns a pattern of words that together create a mnemonic, which
4157
4267
  * represents the set of matching documents.
4158
4268
  */
4159
4269
  hashMnemonic(documents) {
4160
- return documentsHashMnemonic(documents.map((doc) => Bridge.document.pointerFor(doc)));
4270
+ const documentHandles = Bridge.document.handlesFor(documents);
4271
+ return documentsHashMnemonic(documentHandles.deref());
4161
4272
  }
4162
4273
  }
4163
4274
  // -----------------------------------------------------------------------------
@@ -4170,14 +4281,16 @@ class LiveQueryEventUpdate {
4170
4281
  * Returns a hash that represents the set of matching documents.
4171
4282
  */
4172
4283
  hash(documents) {
4173
- return documentsHash(documents.map((doc) => Bridge.document.pointerFor(doc)));
4284
+ const documentHandles = Bridge.document.handlesFor(documents);
4285
+ return documentsHash(documentHandles.deref());
4174
4286
  }
4175
4287
  /**
4176
4288
  * Returns a pattern of words that together create a mnemonic, which
4177
4289
  * represents the set of matching documents.
4178
4290
  */
4179
4291
  hashMnemonic(documents) {
4180
- return documentsHashMnemonic(documents.map((doc) => Bridge.document.pointerFor(doc)));
4292
+ const documentHandles = Bridge.document.handlesFor(documents);
4293
+ return documentsHashMnemonic(documentHandles.deref());
4181
4294
  }
4182
4295
  /** @internal */
4183
4296
  constructor(params) {
@@ -4203,14 +4316,16 @@ class SingleDocumentLiveQueryEvent {
4203
4316
  * Returns a hash that represents the set of matching documents.
4204
4317
  */
4205
4318
  hash(document) {
4206
- return documentsHash(document === null ? [] : [Bridge.document.pointerFor(document)]);
4319
+ const documentHandles = Bridge.document.handlesFor(document == null ? [] : [document]);
4320
+ return documentsHash(documentHandles.deref());
4207
4321
  }
4208
4322
  /**
4209
4323
  * Returns a pattern of words that together create a mnemonic, which
4210
4324
  * represents the set of matching documents.
4211
4325
  */
4212
4326
  hashMnemonic(document) {
4213
- return documentsHashMnemonic(document === null ? [] : [Bridge.document.pointerFor(document)]);
4327
+ const documentHandles = Bridge.document.handlesFor(document == null ? [] : [document]);
4328
+ return documentsHashMnemonic(documentHandles.deref());
4214
4329
  }
4215
4330
  /** @internal */
4216
4331
  constructor(isInitial, oldDocument) {
@@ -4241,24 +4356,18 @@ class LiveQuery {
4241
4356
  }
4242
4357
  /** Returns true if the receiver has been stopped. */
4243
4358
  get isStopped() {
4244
- return this.liveQueryID === null;
4359
+ return !this.liveQueryManager;
4245
4360
  }
4246
4361
  /**
4247
4362
  * Stop the live query from delivering updates.
4248
4363
  */
4249
4364
  stop() {
4250
- var _a;
4251
- const liveQueryID = this.liveQueryID;
4252
- if (liveQueryID !== null) {
4253
- this.collection.store.ditto.keepAlive.release(`LiveQuery.${liveQueryID}`);
4254
- this.liveQueryID = null;
4255
- const dittoPointer = Bridge.ditto.pointerFor(this.collection.store.ditto);
4256
- (_a = this.subscription) === null || _a === void 0 ? void 0 : _a.cancel();
4257
- liveQueryStop(dittoPointer, liveQueryID);
4365
+ if (!this.isStopped) {
4366
+ this.liveQueryManager.stopLiveQuery(this);
4258
4367
  }
4259
4368
  }
4260
4369
  /** @internal */
4261
- constructor(query, queryArgs, queryArgsCBOR, orderBys, limit, offset, collection, subscription, handler) {
4370
+ constructor(query, queryArgs, queryArgsCBOR, orderBys, limit, offset, collection, handler) {
4262
4371
  // Query should be validated at this point.
4263
4372
  this.query = query;
4264
4373
  this.queryArgs = queryArgs ? Object.freeze({ ...queryArgs }) : null;
@@ -4268,52 +4377,59 @@ class LiveQuery {
4268
4377
  this.offset = offset;
4269
4378
  this.collection = collection;
4270
4379
  this.handler = handler;
4271
- if (subscription) {
4272
- this.subscription = subscription;
4273
- }
4380
+ this.liveQueryManager = null;
4274
4381
  const collectionName = collection.name;
4275
4382
  const weakDitto = new WeakRef(collection.store.ditto);
4276
4383
  let liveQueryID = undefined;
4277
4384
  const signalNext = async () => {
4278
4385
  const ditto = weakDitto.deref();
4279
- if (ditto) {
4280
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
4386
+ if (!ditto)
4387
+ return;
4388
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4389
+ const dittoPointer = dittoHandle.derefOrNull();
4390
+ if (!dittoPointer)
4391
+ return;
4392
+ return ditto.deferCloseAsync(async () => {
4281
4393
  await liveQuerySignalAvailableNext(dittoPointer, liveQueryID);
4282
- }
4394
+ });
4283
4395
  };
4284
- const dittoPointer = Bridge.ditto.pointerFor(collection.store.ditto);
4285
- liveQueryID = liveQueryRegister(dittoPointer, collectionName, query, queryArgsCBOR, this.orderBys, limit, offset, (cCBParams) => {
4286
- const documents = cCBParams.documents.map((ptr) => Bridge.document.bridge(ptr));
4287
- let event;
4288
- if (cCBParams.is_initial) {
4289
- event = new LiveQueryEventInitial();
4290
- }
4291
- else {
4292
- event = new LiveQueryEventUpdate({
4293
- oldDocuments: cCBParams.old_documents.map((ptr) => Bridge.document.bridge(ptr)),
4294
- insertions: cCBParams.insertions,
4295
- deletions: cCBParams.deletions,
4296
- updates: cCBParams.updates,
4297
- moves: cCBParams.moves.map((move) => ({ from: move[0], to: move[1] })),
4298
- });
4396
+ const ditto = collection.store.ditto;
4397
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4398
+ ditto.deferClose(() => {
4399
+ liveQueryID = liveQueryRegister(dittoHandle.deref(), collectionName, query, queryArgsCBOR, this.orderBys, limit, offset, (cCBParams) => {
4400
+ const documents = cCBParams.documents.map((ptr) => Bridge.document.bridge(ptr));
4401
+ let event;
4402
+ if (cCBParams.is_initial) {
4403
+ event = new LiveQueryEventInitial();
4404
+ }
4405
+ else {
4406
+ event = new LiveQueryEventUpdate({
4407
+ oldDocuments: cCBParams.old_documents.map((ptr) => Bridge.document.bridge(ptr)),
4408
+ insertions: cCBParams.insertions,
4409
+ deletions: cCBParams.deletions,
4410
+ updates: cCBParams.updates,
4411
+ moves: cCBParams.moves.map((move) => ({ from: move[0], to: move[1] })),
4412
+ });
4413
+ }
4414
+ handler(documents, event, signalNext);
4415
+ });
4416
+ if (!liveQueryID) {
4417
+ throw new Error("Internal inconsistency, couldn't create a valid live query ID.");
4299
4418
  }
4300
- handler(documents, event, signalNext);
4419
+ this['liveQueryID'] = liveQueryID;
4301
4420
  });
4302
- if (!liveQueryID) {
4303
- throw new Error("Internal inconsistency, couldn't create a valid live query ID.");
4304
- }
4305
- this.liveQueryID = liveQueryID;
4306
- // not awaited on purpose; let the invocation of the initial observation
4307
- // happen in a "fire-and-forget" / detached / escaping fashion, to be
4308
- // consistent with the subsequent invocations.
4309
- step(async () => await liveQueryStart(dittoPointer, liveQueryID));
4310
- collection.store.ditto.keepAlive.retain(`LiveQuery.${liveQueryID}`);
4311
4421
  }
4312
4422
  /** @internal */
4313
4423
  async signalNext() {
4314
4424
  // IDEA: make this public?
4315
- const dittoPointer = Bridge.ditto.pointerFor(this.collection.store.ditto);
4316
- await liveQuerySignalAvailableNext(dittoPointer, this.liveQueryID);
4425
+ const ditto = this.collection.store.ditto;
4426
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4427
+ const dittoPointer = dittoHandle.derefOrNull();
4428
+ if (!dittoPointer)
4429
+ return;
4430
+ return ditto.deferCloseAsync(async () => {
4431
+ await liveQuerySignalAvailableNext(dittoHandle.deref(), this.liveQueryID);
4432
+ });
4317
4433
  }
4318
4434
  }
4319
4435
 
@@ -4355,40 +4471,48 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4355
4471
  return super.limit(limit);
4356
4472
  }
4357
4473
  async remove() {
4358
- const query = this.query;
4359
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4360
- const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4361
- const writeTransactionX = await writeTransaction(dittoX);
4362
- const results = await collectionRemoveQueryStr(dittoX, this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4363
- await writeTransactionCommit(dittoX, writeTransactionX);
4364
- return results;
4365
- });
4366
- return documentsX.map((idCBOR) => {
4367
- return new DocumentID(idCBOR, true);
4474
+ const ditto = this.collection.store.ditto;
4475
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4476
+ return ditto.deferCloseAsync(async () => {
4477
+ const query = this.query;
4478
+ const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4479
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4480
+ const results = await collectionRemoveQueryStr(dittoHandle.deref(), this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4481
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4482
+ return results;
4483
+ });
4484
+ return documentsX.map((idCBOR) => {
4485
+ return new DocumentID(idCBOR, true);
4486
+ });
4368
4487
  });
4369
4488
  }
4370
4489
  async evict() {
4371
- const query = this.query;
4372
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4373
- const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4374
- const writeTransactionX = await writeTransaction(dittoX);
4375
- const results = await collectionEvictQueryStr(dittoX, this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4376
- await writeTransactionCommit(dittoX, writeTransactionX);
4377
- return results;
4378
- });
4379
- return documentsX.map((idCBOR) => {
4380
- return new DocumentID(idCBOR, true);
4490
+ const ditto = this.collection.store.ditto;
4491
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4492
+ return ditto.deferCloseAsync(async () => {
4493
+ const query = this.query;
4494
+ const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4495
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4496
+ const results = await collectionEvictQueryStr(dittoHandle.deref(), this.collection.name, writeTransactionX, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4497
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4498
+ return results;
4499
+ });
4500
+ return documentsX.map((idCBOR) => {
4501
+ return new DocumentID(idCBOR, true);
4502
+ });
4381
4503
  });
4382
4504
  }
4383
4505
  async update(closure) {
4384
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4385
- const results = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4386
- const writeTransactionX = await writeTransaction(dittoX);
4387
- const results = await super.updateWithTransaction(closure, writeTransactionX);
4388
- await writeTransactionCommit(dittoX, writeTransactionX);
4389
- return results;
4506
+ const ditto = this.collection.store.ditto;
4507
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4508
+ return ditto.deferCloseAsync(async () => {
4509
+ return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4510
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4511
+ const results = await super.updateWithTransaction(closure, writeTransactionX);
4512
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4513
+ return results;
4514
+ });
4390
4515
  });
4391
- return results;
4392
4516
  }
4393
4517
  /**
4394
4518
  * Enables you to subscribe to changes that occur in a collection remotely.
@@ -4406,7 +4530,9 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4406
4530
  * query specified in the preceding chain.
4407
4531
  */
4408
4532
  subscribe() {
4409
- return new Subscription(this.collection, this.query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4533
+ const subscription = new Subscription(this.collection, this.query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset);
4534
+ this.collection.store.ditto.subscriptionManager.add(subscription);
4535
+ return subscription;
4410
4536
  }
4411
4537
  /**
4412
4538
  * Enables you to listen for changes that occur in a collection locally.
@@ -4433,7 +4559,7 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4433
4559
  * as you want to keep receiving updates.
4434
4560
  */
4435
4561
  observeLocal(handler) {
4436
- return this._observe(handler, false, false);
4562
+ return this._observe(handler, false);
4437
4563
  }
4438
4564
  /**
4439
4565
  * Enables you to listen for changes that occur in a collection locally and
@@ -4462,7 +4588,7 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4462
4588
  * as you want to keep receiving updates.
4463
4589
  */
4464
4590
  observeLocalWithNextSignal(handler) {
4465
- return this._observe(handler, false, true);
4591
+ return this._observe(handler, true);
4466
4592
  }
4467
4593
  // ----------------------------------------------------------- Internal ------
4468
4594
  /** @internal */
@@ -4470,8 +4596,7 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4470
4596
  super(query, queryArgs, collection);
4471
4597
  }
4472
4598
  /** @internal */
4473
- _observe(handler, createSubscription, waitForNextSignal) {
4474
- const subscription = createSubscription ? this.subscribe() : null;
4599
+ _observe(handler, waitForNextSignal) {
4475
4600
  function wrappedHandler(documents, event, nextSignal) {
4476
4601
  try {
4477
4602
  return handler.call(this, documents, event);
@@ -4481,7 +4606,9 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4481
4606
  }
4482
4607
  }
4483
4608
  const handlerOrWrapped = waitForNextSignal ? handler : wrappedHandler;
4484
- return new LiveQuery(this.query, this.queryArgs, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset, this.collection, subscription, handlerOrWrapped);
4609
+ const liveQuery = new LiveQuery(this.query, this.queryArgs, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset, this.collection, handlerOrWrapped);
4610
+ this.collection.store.ditto.liveQueryManager.startLiveQuery(liveQuery);
4611
+ return liveQuery;
4485
4612
  }
4486
4613
  }
4487
4614
 
@@ -4509,39 +4636,48 @@ class PendingCursorOperation extends BasePendingCursorOperation {
4509
4636
  */
4510
4637
  class PendingIDSpecificOperation extends BasePendingIDSpecificOperation {
4511
4638
  async remove() {
4512
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4513
- return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4514
- const writeTransactionX = await writeTransaction(dittoX);
4515
- const didRemove = await collectionRemove(dittoX, this.collection.name, writeTransactionX, this.documentIDCBOR);
4516
- await writeTransactionCommit(dittoX, writeTransactionX);
4517
- return didRemove;
4639
+ const ditto = this.collection.store.ditto;
4640
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4641
+ return ditto.deferCloseAsync(async () => {
4642
+ return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4643
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4644
+ const didRemove = await collectionRemove(dittoHandle.deref(), this.collection.name, writeTransactionX, this.documentIDCBOR);
4645
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4646
+ return didRemove;
4647
+ });
4518
4648
  });
4519
4649
  }
4520
4650
  async evict() {
4521
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4522
- return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4523
- const writeTransactionX = await writeTransaction(dittoX);
4524
- const didEvict = await collectionEvict(dittoX, this.collection.name, writeTransactionX, this.documentIDCBOR);
4525
- await writeTransactionCommit(dittoX, writeTransactionX);
4526
- return didEvict;
4651
+ const ditto = this.collection.store.ditto;
4652
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4653
+ return ditto.deferCloseAsync(async () => {
4654
+ return await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4655
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4656
+ const didEvict = await collectionEvict(dittoHandle.deref(), this.collection.name, writeTransactionX, this.documentIDCBOR);
4657
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4658
+ return didEvict;
4659
+ });
4527
4660
  });
4528
4661
  }
4529
4662
  async update(closure) {
4530
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
4531
- const readTransactionX = await readTransaction(dittoX);
4532
- const documentX = await collectionGet(dittoX, this.collection.name, this.documentIDCBOR, readTransactionX);
4533
- readTransactionFree(readTransactionX);
4534
- if (!documentX)
4535
- throw new Error(`Can't update, document with ID '${this.documentID.toString()}' not found in collection named '${this.collection.name}'`);
4536
- const mutableDocument = Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
4537
- closure(mutableDocument);
4538
- // Ownership is transferred back to the FFI layer via collectionUpdate(),
4539
- // we therefore need to explicitly unregister the instance.
4540
- Bridge.mutableDocument.unregister(mutableDocument);
4541
- const writeTransactionX = await writeTransaction(dittoX);
4542
- await collectionUpdate(dittoX, this.collection.name, writeTransactionX, documentX);
4543
- await writeTransactionCommit(dittoX, writeTransactionX);
4544
- return mutableDocument['@ditto.updateResults'].slice();
4663
+ const ditto = this.collection.store.ditto;
4664
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4665
+ return ditto.deferCloseAsync(async () => {
4666
+ const readTransactionX = await readTransaction(dittoHandle.deref());
4667
+ const documentX = await collectionGet(dittoHandle.deref(), this.collection.name, this.documentIDCBOR, readTransactionX);
4668
+ readTransactionFree(readTransactionX);
4669
+ if (!documentX)
4670
+ throw new Error(`Can't update, document with ID '${this.documentID.toString()}' not found in collection named '${this.collection.name}'`);
4671
+ const mutableDocument = Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
4672
+ closure(mutableDocument);
4673
+ // Ownership is transferred back to the FFI layer via collectionUpdate(),
4674
+ // we therefore need to explicitly unregister the instance.
4675
+ Bridge.mutableDocument.unregister(mutableDocument);
4676
+ const writeTransactionX = await writeTransaction(dittoHandle.deref());
4677
+ await collectionUpdate(dittoHandle.deref(), this.collection.name, writeTransactionX, documentX);
4678
+ await writeTransactionCommit(dittoHandle.deref(), writeTransactionX);
4679
+ return mutableDocument['@ditto.updateResults'].slice();
4680
+ });
4545
4681
  }
4546
4682
  /**
4547
4683
  * Enables you to subscribe to changes that occur in relation to a document
@@ -4558,7 +4694,9 @@ class PendingIDSpecificOperation extends BasePendingIDSpecificOperation {
4558
4694
  * long as you want to keep receiving updates for the document.
4559
4695
  */
4560
4696
  subscribe() {
4561
- return new Subscription(this.collection, this.query, null, [], -1, 0);
4697
+ const subscription = new Subscription(this.collection, this.query, null, [], -1, 0);
4698
+ this.collection.store.ditto.subscriptionManager.add(subscription);
4699
+ return subscription;
4562
4700
  }
4563
4701
  /**
4564
4702
  * Enables you to listen for changes that occur in relation to a document
@@ -4583,7 +4721,7 @@ class PendingIDSpecificOperation extends BasePendingIDSpecificOperation {
4583
4721
  * as you want to keep receiving updates.
4584
4722
  */
4585
4723
  observeLocal(handler) {
4586
- return this._observe(handler, false, false);
4724
+ return this._observe(handler, false);
4587
4725
  }
4588
4726
  /**
4589
4727
  * Enables you to listen for changes that occur in relation to a document
@@ -4609,16 +4747,15 @@ class PendingIDSpecificOperation extends BasePendingIDSpecificOperation {
4609
4747
  * as you want to keep receiving updates.
4610
4748
  */
4611
4749
  observeLocalWithNextSignal(handler) {
4612
- return this._observe(handler, false, true);
4750
+ return this._observe(handler, true);
4613
4751
  }
4614
4752
  /** @internal */
4615
4753
  constructor(documentID, collection) {
4616
4754
  super(documentID, collection);
4617
4755
  }
4618
4756
  /** @internal */
4619
- _observe(handler, createSubscription, waitForNextSignal) {
4620
- const subscription = createSubscription ? this.subscribe() : null;
4621
- return new LiveQuery(this.query, null, null, [], -1, 0, this.collection, subscription, (documents, event, signalNext) => {
4757
+ _observe(handler, waitForNextSignal) {
4758
+ const liveQuery = new LiveQuery(this.query, null, null, [], -1, 0, this.collection, (documents, event, signalNext) => {
4622
4759
  if (documents.length > 1) {
4623
4760
  const documentIDsAsBase64Strings = documents.map((document) => document.id.toBase64String());
4624
4761
  throw new Error(`Internal inconsistency, single document live query returned more than one document. Query: ${this.query}, documentIDs: ${documentIDsAsBase64Strings.join(', ')}.`);
@@ -4653,6 +4790,8 @@ class PendingIDSpecificOperation extends BasePendingIDSpecificOperation {
4653
4790
  }
4654
4791
  }
4655
4792
  });
4793
+ this.collection.store.ditto.liveQueryManager.startLiveQuery(liveQuery);
4794
+ return liveQuery;
4656
4795
  }
4657
4796
  }
4658
4797
 
@@ -4712,28 +4851,18 @@ class Collection {
4712
4851
  return new PendingIDSpecificOperation(documentID, this);
4713
4852
  }
4714
4853
  async upsert(value, options = {}) {
4715
- var _a;
4716
- const writeStrategy = (_a = options.writeStrategy) !== null && _a !== void 0 ? _a : 'merge';
4717
4854
  const ditto = this.store.ditto;
4718
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
4719
- const id = value._id;
4720
- let documentID;
4721
- if (typeof id === 'undefined') {
4722
- documentID = undefined;
4723
- }
4724
- else if (id instanceof DocumentID) {
4725
- documentID = id;
4726
- }
4727
- else {
4728
- documentID = new DocumentID(id);
4729
- }
4730
- const documentIDCBOR = typeof documentID === 'undefined' ? null : documentID.toCBOR();
4731
- const documentValueJSON = desugarJSObject(value, true);
4732
- const documentValueCBOR = CBOR.encode(documentValueJSON);
4733
- const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4734
- return await collectionInsertValue(dittoPointer, this.name, documentValueCBOR, documentIDCBOR, writeStrategy, undefined);
4855
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4856
+ return ditto.deferCloseAsync(async () => {
4857
+ var _a;
4858
+ const writeStrategy = (_a = options.writeStrategy) !== null && _a !== void 0 ? _a : 'merge';
4859
+ const documentValueJSON = desugarJSObject(value, true);
4860
+ const documentValueCBOR = CBOR.encode(documentValueJSON);
4861
+ const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
4862
+ return await collectionInsertValue(dittoHandle.deref(), this.name, documentValueCBOR, writeStrategy, undefined);
4863
+ });
4864
+ return new DocumentID(idCBOR, true);
4735
4865
  });
4736
- return new DocumentID(idCBOR, true);
4737
4866
  }
4738
4867
  /**
4739
4868
  * Creates a new {@link Attachment} object, which can then be inserted into a
@@ -4763,23 +4892,25 @@ class Collection {
4763
4892
  */
4764
4893
  async newAttachment(pathOrData, metadata = {}) {
4765
4894
  const ditto = this.store.ditto;
4766
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
4767
- const { id, len, handle } = await (async () => {
4768
- if (typeof pathOrData === 'string') {
4769
- {
4770
- return dittoNewAttachmentFromFile(dittoPointer, pathOrData, 'Copy');
4895
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
4896
+ return ditto.deferCloseAsync(async () => {
4897
+ const { id, len, handle } = await (async () => {
4898
+ if (typeof pathOrData === 'string') {
4899
+ {
4900
+ return dittoNewAttachmentFromFile(dittoHandle.deref(), pathOrData, 'Copy');
4901
+ }
4771
4902
  }
4772
- }
4773
- if (pathOrData instanceof Uint8Array) {
4774
- return await dittoNewAttachmentFromBytes(dittoPointer, pathOrData);
4775
- }
4776
- throw new Error(`Can't create new attachment, only file path as string or raw data as Uint8Array are supported, but got: ${typeof pathOrData}, ${pathOrData}`);
4777
- })();
4778
- const attachmentTokenJSON = { _id: id, _len: len, _meta: { ...metadata } };
4779
- attachmentTokenJSON[DittoCRDTTypeKey] = DittoCRDTType.attachment;
4780
- const attachmentToken = new AttachmentToken(attachmentTokenJSON);
4781
- const attachment = new Attachment(ditto, attachmentToken);
4782
- return Bridge.attachment.bridge(handle, () => attachment);
4903
+ if (pathOrData instanceof Uint8Array) {
4904
+ return await dittoNewAttachmentFromBytes(dittoHandle.deref(), pathOrData);
4905
+ }
4906
+ throw new Error(`Can't create new attachment, only file path as string or raw data as Uint8Array are supported, but got: ${typeof pathOrData}, ${pathOrData}`);
4907
+ })();
4908
+ const attachmentTokenJSON = { _id: id, _len: len, _meta: { ...metadata } };
4909
+ attachmentTokenJSON[DittoCRDTTypeKey] = DittoCRDTType.attachment;
4910
+ const attachmentToken = new AttachmentToken(attachmentTokenJSON);
4911
+ const attachment = new Attachment(ditto, attachmentToken);
4912
+ return Bridge.attachment.bridge(handle, () => attachment);
4913
+ });
4783
4914
  }
4784
4915
  /**
4785
4916
  * Trigger an attachment to be downloaded locally to the device and observe
@@ -4806,8 +4937,13 @@ class Collection {
4806
4937
  * fetch status changes.
4807
4938
  */
4808
4939
  fetchAttachment(token, eventHandler) {
4940
+ if (token == null || !(token instanceof AttachmentToken)) {
4941
+ throw new Error(`Invalid attachment token: ${token}`);
4942
+ }
4809
4943
  const ditto = this.store.ditto;
4810
- return new AttachmentFetcher(ditto, token, eventHandler);
4944
+ return ditto.deferClose(() => {
4945
+ return ditto.attachmentFetcherManager.startAttachmentFetcher(token, eventHandler);
4946
+ });
4811
4947
  }
4812
4948
  /** @internal */
4813
4949
  constructor(name, store) {
@@ -4959,7 +5095,7 @@ class PendingCollectionsOperation {
4959
5095
  * as you want to keep receiving updates.
4960
5096
  */
4961
5097
  observeLocal(handler) {
4962
- return this._observe(handler, false, false);
5098
+ return this._observe(handler, false);
4963
5099
  }
4964
5100
  /**
4965
5101
  * Enables you to listen for changes that occur in relation to the collections
@@ -4980,7 +5116,7 @@ class PendingCollectionsOperation {
4980
5116
  * as you want to keep receiving updates.
4981
5117
  */
4982
5118
  observeLocalWithNextSignal(handler) {
4983
- return this._observe(handler, false, true);
5119
+ return this._observe(handler, true);
4984
5120
  }
4985
5121
  /**
4986
5122
  * Return the list of collections requested based on the preceding function
@@ -5004,7 +5140,7 @@ class PendingCollectionsOperation {
5004
5140
  }
5005
5141
  // ----------------------------------------------------------- Internal ------
5006
5142
  /** @internal */
5007
- _observe(handler, createSubscription, waitForNextSignal) {
5143
+ _observe(handler, waitForNextSignal) {
5008
5144
  const weakStore = new WeakRef(this.store);
5009
5145
  const collectionsObservationHandler = function (documents, event, nextSignal) {
5010
5146
  const strongStore = weakStore.deref();
@@ -5035,7 +5171,7 @@ class PendingCollectionsOperation {
5035
5171
  handler(collEvent);
5036
5172
  }
5037
5173
  };
5038
- return this.pendingCursorOperation._observe(collectionsObservationHandler, createSubscription, waitForNextSignal);
5174
+ return this.pendingCursorOperation._observe(collectionsObservationHandler, waitForNextSignal);
5039
5175
  }
5040
5176
  }
5041
5177
  /** @private */
@@ -5076,16 +5212,23 @@ class ExperimentalStore {
5076
5212
  * @returns a promise for an array of Ditto documents matching the query
5077
5213
  **/
5078
5214
  async execute(query) {
5079
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5080
- // A one-off query execution uses a transaction internally but a transaction
5081
- // API is not implemented yet.
5082
- const writeTransaction = null;
5083
- // Not implemented yet.
5084
- const queryParameters = null;
5085
- const documentPointers = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
5086
- return await experimentalExecQueryStr(dittoPointer, writeTransaction, query, queryParameters);
5215
+ const ditto = this.ditto;
5216
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5217
+ return ditto.deferCloseAsync(async () => {
5218
+ // A one-off query execution uses a transaction internally but a
5219
+ // transaction API is not implemented yet.
5220
+ const writeTransaction = null;
5221
+ // Not implemented yet.
5222
+ const queryParameters = null;
5223
+ const documentPointers = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
5224
+ return await experimentalExecQueryStr(dittoHandle.deref(), writeTransaction, query, queryParameters);
5225
+ });
5226
+ return documentPointers.map((documentPointer) => Bridge.document.bridge(documentPointer));
5087
5227
  });
5088
- return documentPointers.map((documentPointer) => Bridge.document.bridge(documentPointer));
5228
+ }
5229
+ /** @internal */
5230
+ close() {
5231
+ // Nothing to do yet.
5089
5232
  }
5090
5233
  }
5091
5234
 
@@ -5115,30 +5258,36 @@ class WriteTransactionPendingCursorOperation extends BasePendingCursorOperation
5115
5258
  return super.limit(limit);
5116
5259
  }
5117
5260
  async remove() {
5118
- const query = this.query;
5119
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
5120
- const transaction = this.collection.writeTransaction;
5121
- const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionRemoveQueryStr(dittoX, this.collection.name, transaction.writeTransactionPointer, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset));
5122
- const results = documentsX.map((idCBOR) => {
5123
- return new DocumentID(idCBOR, true);
5124
- });
5125
- results.forEach((documentId) => {
5126
- transaction.addResult('removed', documentId, this.collection.name);
5261
+ const ditto = this.collection.store.ditto;
5262
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5263
+ return ditto.deferCloseAsync(async () => {
5264
+ const query = this.query;
5265
+ const transaction = this.collection.writeTransaction;
5266
+ const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionRemoveQueryStr(dittoHandle.deref(), this.collection.name, transaction.writeTransactionPointer, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset));
5267
+ const results = documentsX.map((idCBOR) => {
5268
+ return new DocumentID(idCBOR, true);
5269
+ });
5270
+ results.forEach((documentId) => {
5271
+ transaction.addResult('removed', documentId, this.collection.name);
5272
+ });
5273
+ return results;
5127
5274
  });
5128
- return results;
5129
5275
  }
5130
5276
  async evict() {
5131
- const query = this.query;
5132
- const dittoX = Bridge.ditto.pointerFor(this.collection.store.ditto);
5133
- const transaction = this.collection.writeTransaction;
5134
- const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionEvictQueryStr(dittoX, this.collection.name, transaction.writeTransactionPointer, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset));
5135
- const results = documentsX.map((idCBOR) => {
5136
- return new DocumentID(idCBOR, true);
5137
- });
5138
- results.forEach((documentId) => {
5139
- transaction.addResult('evicted', documentId, this.collection.name);
5277
+ const ditto = this.collection.store.ditto;
5278
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5279
+ return ditto.deferCloseAsync(async () => {
5280
+ const query = this.query;
5281
+ const transaction = this.collection.writeTransaction;
5282
+ const documentsX = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionEvictQueryStr(dittoHandle.deref(), this.collection.name, transaction.writeTransactionPointer, query, this.queryArgsCBOR, this.orderBys, this.currentLimit, this.currentOffset));
5283
+ const results = documentsX.map((idCBOR) => {
5284
+ return new DocumentID(idCBOR, true);
5285
+ });
5286
+ results.forEach((documentId) => {
5287
+ transaction.addResult('evicted', documentId, this.collection.name);
5288
+ });
5289
+ return results;
5140
5290
  });
5141
- return results;
5142
5291
  }
5143
5292
  async update(closure) {
5144
5293
  const transaction = this.collection.writeTransaction;
@@ -5158,35 +5307,44 @@ class WriteTransactionPendingCursorOperation extends BasePendingCursorOperation
5158
5307
  //
5159
5308
  class WriteTransactionPendingIDSpecificOperation extends BasePendingIDSpecificOperation {
5160
5309
  async remove() {
5161
- const dittoPointer = Bridge.ditto.pointerFor(this.collection.store.ditto);
5162
- const transaction = this.collection.writeTransaction;
5163
- const result = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionRemove(dittoPointer, this.collection.name, transaction.writeTransactionPointer, this.documentIDCBOR));
5164
- transaction.addResult('removed', this.documentID, this.collection.name);
5165
- return result;
5310
+ const ditto = this.collection.store.ditto;
5311
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5312
+ return ditto.deferCloseAsync(async () => {
5313
+ const transaction = this.collection.writeTransaction;
5314
+ const result = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => collectionRemove(dittoHandle.deref(), this.collection.name, transaction.writeTransactionPointer, this.documentIDCBOR));
5315
+ transaction.addResult('removed', this.documentID, this.collection.name);
5316
+ return result;
5317
+ });
5166
5318
  }
5167
5319
  async evict() {
5168
- const dittoPointer = Bridge.ditto.pointerFor(this.collection.store.ditto);
5169
- const transaction = this.collection.writeTransaction;
5170
- const result = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => await collectionEvict(dittoPointer, this.collection.name, transaction.writeTransactionPointer, this.documentIDCBOR));
5171
- transaction.addResult('evicted', this.documentID, this.collection.name);
5172
- return result;
5320
+ const ditto = this.collection.store.ditto;
5321
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5322
+ return ditto.deferCloseAsync(async () => {
5323
+ const transaction = this.collection.writeTransaction;
5324
+ const result = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => await collectionEvict(dittoHandle.deref(), this.collection.name, transaction.writeTransactionPointer, this.documentIDCBOR));
5325
+ transaction.addResult('evicted', this.documentID, this.collection.name);
5326
+ return result;
5327
+ });
5173
5328
  }
5174
5329
  async update(closure) {
5175
- const dittoPointer = Bridge.ditto.pointerFor(this.collection.store.ditto);
5176
- const transaction = this.collection.writeTransaction;
5177
- const readTransactionX = await readTransaction(dittoPointer);
5178
- const documentX = await collectionGet(dittoPointer, this.collection.name, this.documentIDCBOR, readTransactionX);
5179
- readTransactionFree(readTransactionX);
5180
- if (!documentX)
5181
- throw new Error(`Can't update, document with ID '${this.documentID.toString()}' not found in collection named '${this.collection.name}'`);
5182
- const mutableDocument = Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
5183
- closure(mutableDocument);
5184
- // Ownership is transferred back to the FFI layer via collectionUpdate(),
5185
- // we therefore need to explicitly unregister the instance.
5186
- Bridge.mutableDocument.unregister(mutableDocument);
5187
- await collectionUpdate(dittoPointer, this.collection.name, transaction.writeTransactionPointer, documentX);
5188
- transaction.addResult('updated', this.documentID, this.collection.name);
5189
- return mutableDocument['@ditto.updateResults'].slice();
5330
+ const ditto = this.collection.store.ditto;
5331
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5332
+ return ditto.deferCloseAsync(async () => {
5333
+ const transaction = this.collection.writeTransaction;
5334
+ const readTransactionX = await readTransaction(dittoHandle.deref());
5335
+ const documentX = await collectionGet(dittoHandle.deref(), this.collection.name, this.documentIDCBOR, readTransactionX);
5336
+ readTransactionFree(readTransactionX);
5337
+ if (!documentX)
5338
+ throw new Error(`Can't update, document with ID '${this.documentID.toString()}' not found in collection named '${this.collection.name}'`);
5339
+ const mutableDocument = Bridge.mutableDocument.bridge(documentX, () => new MutableDocument());
5340
+ closure(mutableDocument);
5341
+ // Ownership is transferred back to the FFI layer via collectionUpdate(),
5342
+ // we therefore need to explicitly unregister the instance.
5343
+ Bridge.mutableDocument.unregister(mutableDocument);
5344
+ await collectionUpdate(dittoHandle.deref(), this.collection.name, transaction.writeTransactionPointer, documentX);
5345
+ transaction.addResult('updated', this.documentID, this.collection.name);
5346
+ return mutableDocument['@ditto.updateResults'].slice();
5347
+ });
5190
5348
  }
5191
5349
  // ----------------------------------------------------------- Internal ------
5192
5350
  /** @internal */
@@ -5241,30 +5399,20 @@ class WriteTransactionCollection {
5241
5399
  return new WriteTransactionPendingIDSpecificOperation(documentID, this);
5242
5400
  }
5243
5401
  async upsert(value, options = {}) {
5244
- var _a;
5245
- const writeStrategy = (_a = options.writeStrategy) !== null && _a !== void 0 ? _a : 'merge';
5246
- const dittoPointer = Bridge.ditto.pointerFor(this.store.ditto);
5247
- const id = value._id;
5248
- let documentID;
5249
- if (typeof id === 'undefined') {
5250
- documentID = undefined;
5251
- }
5252
- else if (id instanceof DocumentID) {
5253
- documentID = id;
5254
- }
5255
- else {
5256
- documentID = new DocumentID(id);
5257
- }
5258
- const documentIDCBOR = typeof documentID === 'undefined' ? null : documentID.toCBOR();
5259
- const documentValueJSON = desugarJSObject(value, true);
5260
- const documentValueCBOR = CBOR.encode(documentValueJSON);
5261
- const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
5262
- // @ts-ignore
5263
- return await collectionInsertValue(dittoPointer, this.name, documentValueCBOR, documentIDCBOR, writeStrategy, this.writeTransaction.writeTransactionPointer);
5402
+ const ditto = this.store.ditto;
5403
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5404
+ return ditto.deferCloseAsync(async () => {
5405
+ var _a;
5406
+ const writeStrategy = (_a = options.writeStrategy) !== null && _a !== void 0 ? _a : 'merge';
5407
+ const documentValueJSON = desugarJSObject(value, true);
5408
+ const documentValueCBOR = CBOR.encode(documentValueJSON);
5409
+ const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
5410
+ return await collectionInsertValue(dittoHandle.deref(), this.name, documentValueCBOR, writeStrategy, this.writeTransaction.writeTransactionPointer);
5411
+ });
5412
+ const insertedDocumentId = new DocumentID(idCBOR, true);
5413
+ this.writeTransaction.addResult('inserted', insertedDocumentId, this.name);
5414
+ return new DocumentID(idCBOR, true);
5264
5415
  });
5265
- const insertedDocumentId = new DocumentID(idCBOR, true);
5266
- this.writeTransaction.addResult('inserted', insertedDocumentId, this.name);
5267
- return new DocumentID(idCBOR, true);
5268
5416
  }
5269
5417
  /**
5270
5418
  * See comment in {@link CollectionInterface.findByIDCBOR()}
@@ -5303,9 +5451,11 @@ class WriteTransaction {
5303
5451
  * @internal
5304
5452
  */
5305
5453
  static async init(ditto) {
5306
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
5307
- const writeTransactionPointer = await writeTransaction(dittoPointer);
5308
- return new WriteTransaction(ditto, writeTransactionPointer);
5454
+ return ditto.deferCloseAsync(async () => {
5455
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5456
+ const writeTransactionPointer = await writeTransaction(dittoHandle.deref());
5457
+ return new WriteTransaction(ditto, writeTransactionPointer);
5458
+ });
5309
5459
  }
5310
5460
  /**
5311
5461
  * Creates a transaction-specific
@@ -5324,13 +5474,19 @@ class WriteTransaction {
5324
5474
  // ----------------------------------------------------------- Internal ------
5325
5475
  /** @internal */
5326
5476
  async commit() {
5327
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5328
- return writeTransactionCommit(dittoPointer, this.writeTransactionPointer);
5477
+ const ditto = this.ditto;
5478
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5479
+ return ditto.deferCloseAsync(async () => {
5480
+ return writeTransactionCommit(dittoHandle.deref(), this.writeTransactionPointer);
5481
+ });
5329
5482
  }
5330
5483
  /** @internal */
5331
5484
  async rollback() {
5332
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5333
- return writeTransactionRollback(dittoPointer, this.writeTransactionPointer);
5485
+ const ditto = this.ditto;
5486
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5487
+ return ditto.deferCloseAsync(async () => {
5488
+ return writeTransactionRollback(dittoHandle.deref(), this.writeTransactionPointer);
5489
+ });
5334
5490
  }
5335
5491
  /**
5336
5492
  * Adds an entry to the list of results that is returned at the end of a
@@ -5377,14 +5533,18 @@ class Store {
5377
5533
  * fetch or observe the collections in the store
5378
5534
  */
5379
5535
  collections() {
5380
- return new PendingCollectionsOperation(this.ditto.store);
5536
+ return new PendingCollectionsOperation(this);
5381
5537
  }
5382
5538
  /**
5383
5539
  * Returns the names of all available collections in the store of the
5384
5540
  * related {@link Ditto} instance.
5385
5541
  */
5386
5542
  collectionNames() {
5387
- return dittoGetCollectionNames(Bridge.ditto.pointerFor(this.ditto));
5543
+ const ditto = this.ditto;
5544
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5545
+ return ditto.deferClose(() => {
5546
+ return dittoGetCollectionNames(dittoHandle.deref());
5547
+ });
5388
5548
  }
5389
5549
  /**
5390
5550
  * Initiate a write transaction in a callback.
@@ -5395,26 +5555,20 @@ class Store {
5395
5555
  * @returns a list of `WriteTransactionResult`s. There is a result for each operation performed as part of the write transaction.
5396
5556
  */
5397
5557
  async write(callback) {
5398
- const transaction = await WriteTransaction.init(this.ditto);
5399
5558
  // Run caller's callback, rolling back if needed.
5400
- try {
5401
- await callback(transaction);
5402
- }
5403
- catch (error) {
5404
- await transaction.rollback();
5405
- Logger.warning(`Transaction rolled back due to an error: ${error === null || error === void 0 ? void 0 : error.message}`);
5406
- throw error;
5407
- }
5408
- // Commit transaction, rolling back if needed.
5409
- try {
5559
+ return this.ditto.deferCloseAsync(async () => {
5560
+ const transaction = await WriteTransaction.init(this.ditto);
5561
+ try {
5562
+ await callback(transaction);
5563
+ }
5564
+ catch (error) {
5565
+ await transaction.rollback();
5566
+ Logger.warning(`Transaction rolled back due to an error: ${error === null || error === void 0 ? void 0 : error.message}`);
5567
+ throw error;
5568
+ }
5410
5569
  await transaction.commit();
5411
- }
5412
- catch (error) {
5413
- await transaction.rollback();
5414
- Logger.warning(`Transaction rolled back due to an error: ${error === null || error === void 0 ? void 0 : error.message}`);
5415
- throw error;
5416
- }
5417
- return transaction.results;
5570
+ return transaction.results;
5571
+ });
5418
5572
  }
5419
5573
  // ----------------------------------------------------------- Internal ------
5420
5574
  /** @internal */
@@ -5422,14 +5576,24 @@ class Store {
5422
5576
  this.ditto = ditto;
5423
5577
  this.experimental = new ExperimentalStore(ditto);
5424
5578
  }
5579
+ /** @internal */
5580
+ close() {
5581
+ this.experimental.close();
5582
+ // NOTE: live query webhook is taken care of by the FFI's
5583
+ // `ditto_shutdown()`, no need to unregister it here.
5584
+ }
5425
5585
  /**
5426
5586
  * Private method, used only by the Portal https://github.com/getditto/ditto/pull/3652
5427
5587
  * @internal
5428
5588
  */
5429
5589
  async registerLiveQueryWebhook(collectionName, query, url) {
5430
- const validatedQuery = validateQuery(query);
5431
- const idCBOR = await liveQueryWebhookRegister(Bridge.ditto.pointerFor(this.ditto), collectionName, validatedQuery, [], 0, 0, url);
5432
- return new DocumentID(idCBOR, true);
5590
+ const ditto = this.ditto;
5591
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5592
+ return ditto.deferCloseAsync(async () => {
5593
+ const validatedQuery = validateQuery(query);
5594
+ const idCBOR = await liveQueryWebhookRegister(dittoHandle.deref(), collectionName, validatedQuery, [], 0, 0, url);
5595
+ return new DocumentID(idCBOR, true);
5596
+ });
5433
5597
  }
5434
5598
  }
5435
5599
 
@@ -5457,9 +5621,12 @@ class Presence {
5457
5621
  * connections between them.
5458
5622
  */
5459
5623
  get graph() {
5460
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5461
- const graphJSONString = dittoPresenceV3(dittoPointer);
5462
- return JSON.parse(graphJSONString);
5624
+ const ditto = this.ditto;
5625
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5626
+ return ditto.deferClose(() => {
5627
+ const graphJSONString = dittoPresenceV3(dittoHandle.deref());
5628
+ return JSON.parse(graphJSONString);
5629
+ });
5463
5630
  }
5464
5631
  /**
5465
5632
  * Request information about Ditto peers in range of this device.
@@ -5486,12 +5653,18 @@ class Presence {
5486
5653
  this.observerManager = new ObserverManager('PresenceObservation', {
5487
5654
  keepAlive: ditto.keepAlive,
5488
5655
  register: (callback) => {
5489
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5490
- dittoRegisterPresenceV3Callback(dittoPointer, callback);
5656
+ const ditto = this.ditto;
5657
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5658
+ ditto.deferClose(() => {
5659
+ dittoRegisterPresenceV3Callback(dittoHandle.deref(), callback);
5660
+ });
5491
5661
  },
5492
5662
  unregister: () => {
5493
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5494
- dittoClearPresenceV3Callback(dittoPointer);
5663
+ const ditto = this.ditto;
5664
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5665
+ ditto.deferClose(() => {
5666
+ dittoClearPresenceV3Callback(dittoHandle.deref());
5667
+ });
5495
5668
  },
5496
5669
  process: (presenceGraphJSONString) => {
5497
5670
  const presenceGraph = JSON.parse(presenceGraphJSONString);
@@ -5499,22 +5672,114 @@ class Presence {
5499
5672
  },
5500
5673
  });
5501
5674
  }
5675
+ /** @internal */
5676
+ close() {
5677
+ this.observerManager.close();
5678
+ }
5679
+ }
5680
+
5681
+ //
5682
+ /** @internal */
5683
+ class LiveQueryManager {
5684
+ /** @internal */
5685
+ constructor(ditto, keepAlive) {
5686
+ this.finalizationRegistry = new FinalizationRegistry(this.finalize);
5687
+ this.ditto = ditto;
5688
+ this.keepAlive = keepAlive;
5689
+ this.liveQueriesByID = {};
5690
+ }
5691
+ /** @internal */
5692
+ startLiveQuery(liveQuery) {
5693
+ const ditto = this.ditto;
5694
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5695
+ // REFACTOR: the starting closure runs detached from here which is a smell.
5696
+ // Can we make this whole starting mechanism non-async? The culprit is
5697
+ // the workaround for a zalgo with `ditto_live_query_start()` FFI function:
5698
+ // It immediately triggers the live query handler making it run "in-line"
5699
+ // when creating a live query, while subsequent invocations are async
5700
+ // (by definition). This is a classic zalgo case. Fix by making
5701
+ // `ditto_live_query_start()` trigger the first live query callback `async`,
5702
+ // just like the subsequent ones.
5703
+ ditto.deferCloseAsync(async () => {
5704
+ const liveQueryID = liveQuery.liveQueryID;
5705
+ if (!liveQueryID) {
5706
+ throw new Error("Internal inconsistency, tried to add a live query that doesn't have a live query ID (probably stopped).");
5707
+ }
5708
+ const existingLiveQuery = this.liveQueriesByID[liveQueryID];
5709
+ if (existingLiveQuery) {
5710
+ throw new Error('Internal inconsistency, tried to add a live query with an ID that has already been added.');
5711
+ }
5712
+ const weakLiveQuery = new WeakRef(liveQuery);
5713
+ this.liveQueriesByID[liveQueryID] = weakLiveQuery;
5714
+ this.finalizationRegistry.register(liveQuery, liveQueryID, this.finalize);
5715
+ liveQuery.liveQueryManager = this;
5716
+ ditto.keepAlive.retain(`LiveQuery.${liveQueryID}`);
5717
+ return new Promise((resolve, reject) => {
5718
+ // not awaited on purpose; let the invocation of the initial observation
5719
+ // happen in a "fire-and-forget" / detached / escaping fashion, to be
5720
+ // consistent with the subsequent invocations.
5721
+ step(async () => {
5722
+ await liveQueryStart(dittoHandle.deref(), liveQueryID);
5723
+ // @ts-ignore
5724
+ resolve();
5725
+ });
5726
+ });
5727
+ });
5728
+ }
5729
+ /** @internal */
5730
+ stopLiveQuery(liveQuery) {
5731
+ this.finalizationRegistry.unregister(liveQuery);
5732
+ const liveQueryID = liveQuery.liveQueryID;
5733
+ if (!liveQueryID) {
5734
+ throw new Error("Internal inconsistency, tried to remove a live query that doesn't have a live query ID (probably stopped).");
5735
+ }
5736
+ liveQuery.liveQueryManager = null;
5737
+ this.stopLiveQueryWithID(liveQueryID);
5738
+ }
5739
+ /** @internal */
5740
+ close() {
5741
+ for (const liveQueryID in this.liveQueriesByID) {
5742
+ const weakLiveQuery = this.liveQueriesByID[liveQueryID];
5743
+ const liveQuery = weakLiveQuery.deref();
5744
+ if (liveQuery) {
5745
+ this.stopLiveQuery(liveQuery);
5746
+ }
5747
+ }
5748
+ }
5749
+ stopLiveQueryWithID(liveQueryID) {
5750
+ const ditto = this.ditto;
5751
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5752
+ ditto.deferClose(() => {
5753
+ liveQueryStop(dittoHandle.deref(), liveQueryID);
5754
+ this.keepAlive.release(`LiveQuery.${liveQueryID}`);
5755
+ delete this.liveQueriesByID[liveQueryID];
5756
+ });
5757
+ }
5758
+ finalize(liveQueryID) {
5759
+ this.stopLiveQueryWithID(liveQueryID);
5760
+ }
5502
5761
  }
5503
5762
 
5504
5763
  //
5505
5764
  /**
5506
- @internal
5507
- @deprecated Replaced by `Presence`.
5508
- */
5765
+ * @internal
5766
+ * @deprecated Replaced by `Presence`.
5767
+ */
5509
5768
  class PresenceManager {
5510
5769
  constructor(ditto) {
5511
5770
  this.ditto = ditto;
5771
+ this.isClosed = false;
5512
5772
  this.isRegistered = false;
5513
5773
  this.currentRemotePeers = [];
5514
5774
  this.callbacksByPresenceToken = {};
5515
5775
  }
5516
5776
  /** @internal */
5517
5777
  addObserver(callback) {
5778
+ if (this.isClosed) {
5779
+ // REFACTOR: throw a catchable error here, such that calling code
5780
+ // can be more specific when forwarding it to the user.
5781
+ throw new Error(`Internal inconsistency, can't add presence observer, observer mananger close()-ed.`);
5782
+ }
5518
5783
  this.registerIfNeeded();
5519
5784
  const token = generateEphemeralToken();
5520
5785
  this.callbacksByPresenceToken[token] = callback;
@@ -5529,31 +5794,62 @@ class PresenceManager {
5529
5794
  }
5530
5795
  /** @internal */
5531
5796
  removeObserver(token) {
5532
- this.ditto.keepAlive.release(`PresenceObservation.${token}`);
5533
- delete this.callbacksByPresenceToken[token];
5534
- this.unregisterIfNeeded();
5797
+ const callback = this.callbacksByPresenceToken[token];
5798
+ if (typeof callback === 'undefined') {
5799
+ throw new Error(`Can't remove presence observer, token '${token}' has never been registered before.`);
5800
+ }
5801
+ if (callback === null) {
5802
+ // Observer has already been removed, no-op.
5803
+ return;
5804
+ }
5805
+ if (typeof this.callbacksByPresenceToken[token] != 'undefined') {
5806
+ this.ditto.keepAlive.release(`PresenceObservation.${token}`);
5807
+ // REFACTOR: not deleting the token here will keep eating up
5808
+ // memory over long periods of time. We actually need to track
5809
+ // the observer objects themselves and remove it from the table
5810
+ // as soon as the observer object is garbage collected.
5811
+ this.callbacksByPresenceToken[token] = null;
5812
+ this.unregisterIfNeeded();
5813
+ }
5814
+ }
5815
+ /** @internal */
5816
+ hasObserver(token) {
5817
+ return typeof this.callbacksByPresenceToken[token] != 'undefined';
5818
+ }
5819
+ /** @internal */
5820
+ close() {
5821
+ this.isClosed = true;
5822
+ for (const token in this.callbacksByPresenceToken) {
5823
+ this.removeObserver(token);
5824
+ }
5535
5825
  }
5536
5826
  hasObservers() {
5537
5827
  return Object.keys(this.callbacksByPresenceToken).length > 0;
5538
5828
  }
5539
5829
  registerIfNeeded() {
5540
- const needsToRegister = !this.isRegistered;
5541
- if (needsToRegister) {
5542
- this.isRegistered = true;
5543
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5544
- const remotePeersJSONString = dittoPresenceV1(dittoPointer);
5545
- this.currentRemotePeers = this.decode(remotePeersJSONString).sort(this.compareRemotePeers);
5546
- dittoRegisterPresenceV1Callback(dittoPointer, this.handlePresenceV1Callback.bind(this));
5547
- }
5830
+ const ditto = this.ditto;
5831
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto);
5832
+ ditto.deferClose(() => {
5833
+ const needsToRegister = !this.isRegistered;
5834
+ if (needsToRegister) {
5835
+ this.isRegistered = true;
5836
+ const remotePeersJSONString = dittoPresenceV1(dittoHandle.deref());
5837
+ this.currentRemotePeers = this.decode(remotePeersJSONString).sort(this.compareRemotePeers);
5838
+ dittoRegisterPresenceV1Callback(dittoHandle.deref(), this.handlePresenceV1Callback.bind(this));
5839
+ }
5840
+ });
5548
5841
  }
5549
5842
  unregisterIfNeeded() {
5550
- const needsToUnregister = !this.hasObservers() && this.isRegistered;
5551
- if (needsToUnregister) {
5552
- this.isRegistered = false;
5553
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5554
- dittoRegisterPresenceV1Callback(dittoPointer, null);
5555
- this.currentRemotePeers = [];
5556
- }
5843
+ const ditto = this.ditto;
5844
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5845
+ ditto.deferClose(() => {
5846
+ const needsToUnregister = !this.hasObservers() && this.isRegistered;
5847
+ if (needsToUnregister) {
5848
+ this.isRegistered = false;
5849
+ dittoClearPresenceCallback(dittoHandle.deref());
5850
+ this.currentRemotePeers = [];
5851
+ }
5852
+ });
5557
5853
  }
5558
5854
  handlePresenceV1Callback(remotePeersJSONString) {
5559
5855
  const remotePeers = this.decode(remotePeersJSONString).sort(this.compareRemotePeers);
@@ -5561,6 +5857,10 @@ class PresenceManager {
5561
5857
  this.notify();
5562
5858
  }
5563
5859
  notify() {
5860
+ if (this.isClosed) {
5861
+ // NOTE: we don't notify observers after closing.
5862
+ return;
5863
+ }
5564
5864
  for (const token in this.callbacksByPresenceToken) {
5565
5865
  const callback = this.callbacksByPresenceToken[token];
5566
5866
  callback(this.currentRemotePeers);
@@ -5579,9 +5879,6 @@ class PresenceManager {
5579
5879
  };
5580
5880
  });
5581
5881
  }
5582
- finalize(token) {
5583
- this.removeObserver(token);
5584
- }
5585
5882
  compareRemotePeers(left, right) {
5586
5883
  // NOTE: we use the exact same sort order here as in the ObjC version.
5587
5884
  if (left.connections.length === 0 && right.connections.length > 0)
@@ -5605,12 +5902,18 @@ class TransportConditionsManager extends ObserverManager {
5605
5902
  this.ditto = ditto;
5606
5903
  }
5607
5904
  register(callback) {
5608
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5609
- return dittoRegisterTransportConditionChangedCallback(dittoPointer, callback);
5905
+ const ditto = this.ditto;
5906
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5907
+ return ditto.deferClose(() => {
5908
+ return dittoRegisterTransportConditionChangedCallback(dittoHandle.deref(), callback);
5909
+ });
5610
5910
  }
5611
5911
  unregister() {
5612
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5613
- return dittoRegisterTransportConditionChangedCallback(dittoPointer, null);
5912
+ const ditto = this.ditto;
5913
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
5914
+ return ditto.deferClose(() => {
5915
+ return dittoRegisterTransportConditionChangedCallback(dittoHandle.deref(), null);
5916
+ });
5614
5917
  }
5615
5918
  process(conditionSource, transportCondition) {
5616
5919
  /* eslint-disable */
@@ -5717,277 +6020,582 @@ class Sync {
5717
6020
  this.updateConnectRetryInterval(stateOld, stateNew);
5718
6021
  this.state = stateNew;
5719
6022
  }
6023
+ /** @internal */
6024
+ close() {
6025
+ if (this.parameters.isSyncActive) {
6026
+ throw new Error(`Internal inconsistency, can't close sync object while sync is active, please 'stopSync()' first.`);
6027
+ }
6028
+ // Nothing to do, when sync is stopped, this object should be
6029
+ // already be cleaned up properly.
6030
+ }
5720
6031
  updatePeerToPeerBluetoothLE(stateOld, stateNew) {
5721
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5722
- const bluetoothLEOld = stateOld.effectiveTransportConfig.peerToPeer.bluetoothLE;
5723
- const bluetoothLENew = stateNew.effectiveTransportConfig.peerToPeer.bluetoothLE;
5724
- const shouldStart = !bluetoothLEOld.isEnabled && bluetoothLENew.isEnabled;
5725
- const shouldStop = bluetoothLEOld.isEnabled && !bluetoothLENew.isEnabled;
5726
- if (shouldStart && this.bluetoothLETransportPointer)
5727
- throw new Error(`Internal inconsistency, when starting BLE transport, no BLE transport pointer should exist.`);
5728
- if (shouldStop && !this.bluetoothLETransportPointer)
5729
- throw new Error(`Internal inconsistency, when stopping BLE transport, a BLE transport pointer should exist.`);
5730
- {
5731
- // HACK: quick & dirty Linux & Windows hack. A proper implementation
5732
- // should encapsulate everything behind the transports module. To undo,
5733
- // remove the whole if block.
5734
- if (process.platform === 'linux' || process.platform === 'win32') {
5735
- if (shouldStart) {
5736
- const clientTransport = dittoAddInternalBLEClientTransport(dittoPointer);
5737
- const serverTransport = dittoAddInternalBLEServerTransport(dittoPointer);
5738
- const blePlatform = { clientTransport, serverTransport };
5739
- this.bluetoothLETransportPointer = blePlatform;
5740
- }
5741
- if (shouldStop) {
5742
- const blePlatform = this.bluetoothLETransportPointer;
5743
- const { clientTransport, serverTransport } = blePlatform;
5744
- bleServerFreeHandle(serverTransport);
5745
- bleClientFreeHandle(clientTransport);
5746
- this.bluetoothLETransportPointer = null;
6032
+ const ditto = this.ditto;
6033
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6034
+ ditto.deferClose(() => {
6035
+ const bluetoothLEOld = stateOld.effectiveTransportConfig.peerToPeer.bluetoothLE;
6036
+ const bluetoothLENew = stateNew.effectiveTransportConfig.peerToPeer.bluetoothLE;
6037
+ const shouldStart = !bluetoothLEOld.isEnabled && bluetoothLENew.isEnabled;
6038
+ const shouldStop = bluetoothLEOld.isEnabled && !bluetoothLENew.isEnabled;
6039
+ if (shouldStart && this.bluetoothLETransportPointer)
6040
+ throw new Error(`Internal inconsistency, when starting BLE transport, no BLE transport pointer should exist.`);
6041
+ if (shouldStop && !this.bluetoothLETransportPointer)
6042
+ throw new Error(`Internal inconsistency, when stopping BLE transport, a BLE transport pointer should exist.`);
6043
+ {
6044
+ // HACK: quick & dirty Linux & Windows hack. A proper implementation
6045
+ // should encapsulate everything behind the transports module. To undo,
6046
+ // remove the whole if block.
6047
+ if (process.platform === 'linux' || process.platform === 'win32') {
6048
+ if (shouldStart) {
6049
+ const clientTransport = dittoAddInternalBLEClientTransport(dittoHandle.deref());
6050
+ const serverTransport = dittoAddInternalBLEServerTransport(dittoHandle.deref());
6051
+ const blePlatform = { clientTransport, serverTransport };
6052
+ this.bluetoothLETransportPointer = blePlatform;
6053
+ }
6054
+ if (shouldStop) {
6055
+ const blePlatform = this.bluetoothLETransportPointer;
6056
+ const { clientTransport, serverTransport } = blePlatform;
6057
+ bleServerFreeHandle(serverTransport);
6058
+ bleClientFreeHandle(clientTransport);
6059
+ this.bluetoothLETransportPointer = null;
6060
+ }
6061
+ return;
5747
6062
  }
5748
- return;
5749
6063
  }
5750
- }
5751
- if (shouldStart) {
5752
- this.bluetoothLETransportPointer = bleCreate(dittoPointer);
5753
- }
5754
- if (shouldStop) {
5755
- bleDestroy(this.bluetoothLETransportPointer);
5756
- delete this.bluetoothLETransportPointer;
5757
- }
6064
+ if (shouldStart) {
6065
+ this.bluetoothLETransportPointer = bleCreate(dittoHandle.deref());
6066
+ }
6067
+ if (shouldStop) {
6068
+ bleDestroy(this.bluetoothLETransportPointer);
6069
+ delete this.bluetoothLETransportPointer;
6070
+ }
6071
+ });
5758
6072
  }
5759
6073
  updatePeerToPeerAWDL(stateOld, stateNew) {
5760
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5761
- const awdlOld = stateOld.effectiveTransportConfig.peerToPeer.awdl;
5762
- const awdlNew = stateNew.effectiveTransportConfig.peerToPeer.awdl;
5763
- const shouldStart = !awdlOld.isEnabled && awdlNew.isEnabled;
5764
- const shouldStop = awdlOld.isEnabled && !awdlNew.isEnabled;
5765
- if (shouldStart && this.awdlTransportPointer)
5766
- throw new Error(`Internal inconsistency, when starting AWDL transport, no AWDL transport pointer should exist.`);
5767
- if (shouldStop && !this.awdlTransportPointer)
5768
- throw new Error(`Internal inconsistency, when stopping AWDL transport, an AWDL transport pointer should exist.`);
5769
- if (shouldStart) {
5770
- this.awdlTransportPointer = awdlCreate(dittoPointer);
5771
- }
5772
- if (shouldStop) {
5773
- awdlDestroy(this.awdlTransportPointer);
5774
- this.awdlTransportPointer = null;
5775
- }
6074
+ const ditto = this.ditto;
6075
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6076
+ ditto.deferClose(() => {
6077
+ const awdlOld = stateOld.effectiveTransportConfig.peerToPeer.awdl;
6078
+ const awdlNew = stateNew.effectiveTransportConfig.peerToPeer.awdl;
6079
+ const shouldStart = !awdlOld.isEnabled && awdlNew.isEnabled;
6080
+ const shouldStop = awdlOld.isEnabled && !awdlNew.isEnabled;
6081
+ if (shouldStart && this.awdlTransportPointer)
6082
+ throw new Error(`Internal inconsistency, when starting AWDL transport, no AWDL transport pointer should exist.`);
6083
+ if (shouldStop && !this.awdlTransportPointer)
6084
+ throw new Error(`Internal inconsistency, when stopping AWDL transport, an AWDL transport pointer should exist.`);
6085
+ if (shouldStart) {
6086
+ this.awdlTransportPointer = awdlCreate(dittoHandle.deref());
6087
+ }
6088
+ if (shouldStop) {
6089
+ awdlDestroy(this.awdlTransportPointer);
6090
+ this.awdlTransportPointer = null;
6091
+ }
6092
+ });
5776
6093
  }
5777
6094
  updatePeerToPeerLAN(stateOld, stateNew) {
5778
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5779
- const lanOld = stateOld.effectiveTransportConfig.peerToPeer.lan;
5780
- const lanNew = stateNew.effectiveTransportConfig.peerToPeer.lan;
5781
- {
5782
- // HACK: quick & dirty Linux & Windows hack. A proper implementation
5783
- // should encapsulate everything behind the transports module. To undo,
5784
- // remove the whole if block.
5785
- if (process.platform === 'win32' || process.platform === 'linux') {
5786
- if (lanOld.isEnabled) {
5787
- if (lanOld.isMdnsEnabled) {
5788
- mdnsClientFreeHandle(this.mdnsClientTransportPointer);
5789
- this.mdnsClientTransportPointer = null;
5790
- mdnsServerFreeHandle(this.mdnsServerAdvertiserPointer);
5791
- this.mdnsServerAdvertiserPointer = null;
5792
- }
5793
- if (lanOld.isMulticastEnabled) {
5794
- dittoRemoveMulticastTransport(dittoPointer);
6095
+ const ditto = this.ditto;
6096
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6097
+ ditto.deferClose(() => {
6098
+ const lanOld = stateOld.effectiveTransportConfig.peerToPeer.lan;
6099
+ const lanNew = stateNew.effectiveTransportConfig.peerToPeer.lan;
6100
+ {
6101
+ // HACK: quick & dirty Linux & Windows hack. A proper implementation
6102
+ // should encapsulate everything behind the transports module. To undo,
6103
+ // remove the whole if block.
6104
+ if (process.platform === 'win32' || process.platform === 'linux') {
6105
+ if (lanOld.isEnabled) {
6106
+ if (lanOld.isMdnsEnabled) {
6107
+ mdnsClientFreeHandle(this.mdnsClientTransportPointer);
6108
+ this.mdnsClientTransportPointer = null;
6109
+ mdnsServerFreeHandle(this.mdnsServerAdvertiserPointer);
6110
+ this.mdnsServerAdvertiserPointer = null;
6111
+ }
6112
+ if (lanOld.isMulticastEnabled) {
6113
+ dittoRemoveMulticastTransport(dittoHandle.deref());
6114
+ }
5795
6115
  }
5796
- }
5797
- if (lanNew.isEnabled) {
5798
- if (lanNew.isMdnsEnabled) {
5799
- this.mdnsClientTransportPointer = dittoAddInternalMdnsTransport(dittoPointer);
5800
- this.mdnsServerAdvertiserPointer = dittoAddInternalMdnsAdvertiser(dittoPointer);
5801
- }
5802
- if (lanNew.isMulticastEnabled) {
5803
- dittoAddMulticastTransport(dittoPointer);
6116
+ if (lanNew.isEnabled) {
6117
+ if (lanNew.isMdnsEnabled) {
6118
+ this.mdnsClientTransportPointer = dittoAddInternalMdnsTransport(dittoHandle.deref());
6119
+ this.mdnsServerAdvertiserPointer = dittoAddInternalMdnsAdvertiser(dittoHandle.deref());
6120
+ }
6121
+ if (lanNew.isMulticastEnabled) {
6122
+ dittoAddMulticastTransport(dittoHandle.deref());
6123
+ }
5804
6124
  }
6125
+ return;
5805
6126
  }
5806
- return;
5807
- }
5808
- }
5809
- // IDEA: move the logic for this dance into stateFrom() signal
5810
- // via some additional state signaling the actions here.
5811
- if (lanOld.isEnabled) {
5812
- if (lanOld.isMdnsEnabled) {
5813
- lanDestroy(this.lanTransportPointer);
5814
- delete this.lanTransportPointer;
5815
6127
  }
5816
- if (lanOld.isMulticastEnabled) {
5817
- dittoRemoveMulticastTransport(dittoPointer);
5818
- }
5819
- }
5820
- if (lanNew.isEnabled) {
5821
- if (lanNew.isMdnsEnabled) {
5822
- this.lanTransportPointer = lanCreate(dittoPointer);
6128
+ // IDEA: move the logic for this dance into stateFrom() signal
6129
+ // via some additional state signaling the actions here.
6130
+ if (lanOld.isEnabled) {
6131
+ if (lanOld.isMdnsEnabled) {
6132
+ lanDestroy(this.lanTransportPointer);
6133
+ delete this.lanTransportPointer;
6134
+ }
6135
+ if (lanOld.isMulticastEnabled) {
6136
+ dittoRemoveMulticastTransport(dittoHandle.deref());
6137
+ }
5823
6138
  }
5824
- if (lanNew.isMulticastEnabled) {
5825
- dittoAddMulticastTransport(dittoPointer);
6139
+ if (lanNew.isEnabled) {
6140
+ if (lanNew.isMdnsEnabled) {
6141
+ this.lanTransportPointer = lanCreate(dittoHandle.deref());
6142
+ }
6143
+ if (lanNew.isMulticastEnabled) {
6144
+ dittoAddMulticastTransport(dittoHandle.deref());
6145
+ }
5826
6146
  }
5827
- }
6147
+ });
5828
6148
  }
5829
6149
  updateListenTCP(stateOld, stateNew) {
5830
- const tcpOld = stateOld.effectiveTransportConfig.listen.tcp;
5831
- const tcpNew = stateNew.effectiveTransportConfig.listen.tcp;
5832
- if (TransportConfig.areListenTCPsEqual(tcpNew, tcpOld))
5833
- return;
5834
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5835
- if (tcpOld.isEnabled)
5836
- dittoStopTCPServer(dittoPointer);
5837
- if (tcpNew.isEnabled)
5838
- dittoStartTCPServer(dittoPointer, `${tcpNew.interfaceIP}:${tcpNew.port}`);
6150
+ const ditto = this.ditto;
6151
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6152
+ ditto.deferClose(() => {
6153
+ const tcpOld = stateOld.effectiveTransportConfig.listen.tcp;
6154
+ const tcpNew = stateNew.effectiveTransportConfig.listen.tcp;
6155
+ if (TransportConfig.areListenTCPsEqual(tcpNew, tcpOld))
6156
+ return;
6157
+ if (tcpOld.isEnabled)
6158
+ dittoStopTCPServer(dittoHandle.deref());
6159
+ if (tcpNew.isEnabled)
6160
+ dittoStartTCPServer(dittoHandle.deref(), `${tcpNew.interfaceIP}:${tcpNew.port}`);
6161
+ });
5839
6162
  }
5840
6163
  updateListenHTTP(stateOld, stateNew) {
5841
- const httpOld = stateOld.effectiveTransportConfig.listen.http;
5842
- const httpNew = stateNew.effectiveTransportConfig.listen.http;
5843
- if (TransportConfig.areListenHTTPsEqual(httpOld, httpNew))
5844
- return;
5845
- const dittoPointer = Bridge.ditto.pointerFor(this.ditto);
5846
- if (httpOld.isEnabled)
5847
- dittoStopHTTPServer(dittoPointer);
5848
- /* eslint-disable */
5849
- if (httpNew.isEnabled) {
5850
- dittoStartHTTPServer(dittoPointer, `${httpNew.interfaceIP}:${httpNew.port}`, httpNew.staticContentPath || null, httpNew.websocketSync ? 'Enabled' : 'Disabled', httpNew.tlsCertificatePath || null, httpNew.tlsKeyPath || null);
5851
- }
5852
- /* eslint-enable */
6164
+ const ditto = this.ditto;
6165
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6166
+ ditto.deferClose(() => {
6167
+ const httpOld = stateOld.effectiveTransportConfig.listen.http;
6168
+ const httpNew = stateNew.effectiveTransportConfig.listen.http;
6169
+ if (TransportConfig.areListenHTTPsEqual(httpOld, httpNew))
6170
+ return;
6171
+ if (httpOld.isEnabled)
6172
+ dittoStopHTTPServer(dittoHandle.deref());
6173
+ /* eslint-disable */
6174
+ if (httpNew.isEnabled) {
6175
+ dittoStartHTTPServer(dittoHandle.deref(), `${httpNew.interfaceIP}:${httpNew.port}`, httpNew.staticContentPath || null, httpNew.websocketSync ? 'Enabled' : 'Disabled', httpNew.tlsCertificatePath || null, httpNew.tlsKeyPath || null);
6176
+ }
6177
+ /* eslint-enable */
6178
+ });
5853
6179
  }
5854
6180
  updateConnectTCPServers(stateOld, stateNew) {
5855
- // REFACTOR: same "algorithm" as for Websockets below, DRY up.
5856
- const currentTCPServers = Object.getOwnPropertyNames(this.staticTCPClientsByAddress);
5857
- const desiredTCPServers = stateNew.effectiveTransportConfig.connect.tcpServers;
5858
- const tcpServersToConnectToSet = new Set(desiredTCPServers);
5859
- for (const tcpServer of currentTCPServers)
5860
- tcpServersToConnectToSet.delete(tcpServer);
5861
- const tcpServersToDisconnectFromSet = new Set(currentTCPServers);
5862
- for (const tcpServer of desiredTCPServers)
5863
- tcpServersToDisconnectFromSet.delete(tcpServer);
5864
- const tcpServersToConnectTo = tcpServersToConnectToSet.values();
5865
- const tcpServersToDisconnectFrom = tcpServersToDisconnectFromSet.values();
5866
- for (const tcpServer of tcpServersToConnectTo) {
5867
- const staticTCPClientPointer = addStaticTCPClient(Bridge.ditto.pointerFor(this.ditto), tcpServer);
5868
- const staticTCPClient = Bridge.staticTCPClient.bridge(staticTCPClientPointer);
5869
- this.staticTCPClientsByAddress[tcpServer] = staticTCPClient;
5870
- }
5871
- for (const tcpServer of tcpServersToDisconnectFrom) {
5872
- // REFACTOR: we have to make sure freeing the tcpServer handle is done
5873
- // BEFORE the Ditto instance itself is freed.
5874
- const staticTCPClient = this.staticTCPClientsByAddress[tcpServer];
5875
- if (!staticTCPClient)
5876
- throw new Error(`Internal inconsistency, can't disconnect from TCP address '${tcpServer}', no staticTCPClient found.`);
5877
- const staticTCPClientPointer = Bridge.staticTCPClient.pointerFor(staticTCPClient);
5878
- // Unregister to avoid double-free for this pointer, then free manually:
5879
- Bridge.staticTCPClient.unregister(staticTCPClient);
5880
- staticTCPClientFreeHandle(staticTCPClientPointer);
5881
- delete this.staticTCPClientsByAddress[tcpServer];
5882
- }
6181
+ const ditto = this.ditto;
6182
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6183
+ 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
+ }
6212
+ });
5883
6213
  }
5884
6214
  updateConnectWebsocketURLs(stateOld, stateNew) {
5885
- // REFACTOR: same "algorithm" as for TCP servers above, DRY up.
5886
- // IDEA: normalize URLs so that we don't connect to the same URL twice?
5887
- const currentWebsocketURLs = Object.getOwnPropertyNames(this.websocketClientsByURL);
5888
- const desiredWebsocketURLs = stateNew.effectiveTransportConfig.connect.websocketURLs.slice();
5889
- const websocketURLsToConnectToSet = new Set(desiredWebsocketURLs);
5890
- for (const websocketURL of currentWebsocketURLs)
5891
- websocketURLsToConnectToSet.delete(websocketURL);
5892
- const websocketURLsToDisconnectFromSet = new Set(currentWebsocketURLs);
5893
- for (const websocketURL of desiredWebsocketURLs)
5894
- websocketURLsToDisconnectFromSet.delete(websocketURL);
5895
- const websocketURLsToConnectTo = websocketURLsToConnectToSet.values();
5896
- const websocketURLsToDisconnectFrom = websocketURLsToDisconnectFromSet.values();
5897
- const routingHint = stateNew.effectiveTransportConfig.global.routingHint;
5898
- for (const websocketURL of websocketURLsToConnectTo) {
5899
- const websocketClientPointer = addWebsocketClient(Bridge.ditto.pointerFor(this.ditto), websocketURL, routingHint);
5900
- const websocketClient = Bridge.websocketClient.bridge(websocketClientPointer);
5901
- this.websocketClientsByURL[websocketURL] = websocketClient;
5902
- }
5903
- for (const websocketURL of websocketURLsToDisconnectFrom) {
5904
- // REFACTOR: we have to make sure freeing the websocket handle is done
5905
- // BEFORE the Ditto instance itself is freed.
5906
- const websocketClient = this.websocketClientsByURL[websocketURL];
5907
- if (!websocketClient)
5908
- throw new Error(`Internal inconsistency, can't disconnect from websocket URL '${websocketURL}', no websocketClient found.`);
5909
- const websocketClientPointer = Bridge.websocketClient.pointerFor(websocketClient);
5910
- // Unregister to avoid double-free for this pointer, then free manually:
5911
- Bridge.websocketClient.unregister(websocketClient);
5912
- websocketClientFreeHandle(websocketClientPointer);
5913
- delete this.websocketClientsByURL[websocketURL];
5914
- }
6215
+ const ditto = this.ditto;
6216
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6217
+ 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();
6230
+ 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
+ }
6248
+ });
5915
6249
  }
5916
6250
  updateGlobal(stateOld, stateNew) {
5917
- if (stateOld.effectiveTransportConfig.global.syncGroup !== stateNew.effectiveTransportConfig.global.syncGroup) {
5918
- dittoSetSyncGroup(Bridge.ditto.pointerFor(this.ditto), stateNew.effectiveTransportConfig.global.syncGroup);
5919
- }
6251
+ const ditto = this.ditto;
6252
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6253
+ ditto.deferClose(() => {
6254
+ if (stateOld.effectiveTransportConfig.global.syncGroup !== stateNew.effectiveTransportConfig.global.syncGroup) {
6255
+ dittoSetSyncGroup(dittoHandle.deref(), stateNew.effectiveTransportConfig.global.syncGroup);
6256
+ }
6257
+ });
5920
6258
  }
5921
6259
  updateConnectRetryInterval(stateOld, stateNew) {
5922
- dittoSetConnectRetryInterval(Bridge.ditto.pointerFor(this.ditto), stateNew.effectiveTransportConfig.connect.retryInterval);
6260
+ const ditto = this.ditto;
6261
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6262
+ ditto.deferClose(() => {
6263
+ dittoSetConnectRetryInterval(dittoHandle.deref(), stateNew.effectiveTransportConfig.connect.retryInterval);
6264
+ });
5923
6265
  }
5924
6266
  }
5925
6267
  /**
5926
6268
  * @private
5927
6269
  */
5928
6270
  function stateFrom(parameters) {
5929
- // IMPORTANT: this function maps a set of sync parameters to an effective
5930
- // transport config plus some extra state where need be. Make sure
5931
- // to keep this function free of side-effects.
5932
- var _a;
5933
- const transportConfig = parameters.transportConfig.copy();
5934
- const identity = parameters.identity;
5935
6271
  const ditto = parameters.ditto;
5936
- const isSyncActive = parameters.isSyncActive;
5937
- const isX509Valid = parameters.isX509Valid;
5938
- const isWebValid = parameters.isWebValid;
5939
- transportConfig.connect.tcpServers;
5940
- transportConfig.connect.websocketURLs;
5941
- let appID;
5942
- let customDittoCloudURL;
5943
- let isDittoCloudSyncEnabled = false;
5944
- if (identity.type === 'onlinePlayground' || identity.type === 'onlineWithAuthentication') {
5945
- // NOTE: enableDittoCloudSync is true by default for any online identity.
5946
- appID = identity.appID;
5947
- customDittoCloudURL = (_a = identity.customDittoCloudURL) !== null && _a !== void 0 ? _a : null;
5948
- isDittoCloudSyncEnabled = identity.hasOwnProperty('enableDittoCloudSync') ? identity.enableDittoCloudSync : true;
5949
- }
5950
- const dittoPointer = Bridge.ditto.pointerFor(ditto);
5951
- // Not all P2P transports are always supported.
5952
- const unavailableButEnabledP2PTransports = [];
5953
- const isBLEAvailable = bleIsAvailable(dittoPointer);
5954
- const isAWDLAvailable = awdlIsAvailable(dittoPointer);
5955
- const isLANAvailable = lanIsAvailable(dittoPointer);
5956
- if (transportConfig.peerToPeer.bluetoothLE.isEnabled && !isBLEAvailable) {
5957
- unavailableButEnabledP2PTransports.push('BluetoothLE');
5958
- }
5959
- if (transportConfig.peerToPeer.awdl.isEnabled && !isAWDLAvailable) {
5960
- unavailableButEnabledP2PTransports.push('AWDL');
5961
- }
5962
- if (transportConfig.peerToPeer.lan.isEnabled && !isLANAvailable) {
5963
- unavailableButEnabledP2PTransports.push('LAN');
5964
- }
5965
- if (unavailableButEnabledP2PTransports.length > 0) {
5966
- throw new Error(`The following P2P transports are enabled in the transport config but are not supported in the current environment: ${unavailableButEnabledP2PTransports.join(', ')}`);
5967
- }
5968
- if (!isSyncActive || !isX509Valid) {
5969
- transportConfig.peerToPeer.bluetoothLE.isEnabled = false;
5970
- transportConfig.peerToPeer.awdl.isEnabled = false;
5971
- transportConfig.peerToPeer.lan.isEnabled = false;
5972
- transportConfig.listen.tcp.isEnabled = false;
5973
- transportConfig.connect.tcpServers = [];
5974
- }
5975
- if (!isSyncActive || !isWebValid) {
5976
- transportConfig.connect.websocketURLs = [];
5977
- }
5978
- if (!isSyncActive) {
5979
- transportConfig.listen.http.isEnabled = false;
5980
- }
5981
- // NOTE: we have to smuggle in an additional Ditto Cloud websocket URL to
5982
- // connect to if cloud sync is enabled.
5983
- if (isSyncActive && isWebValid && isDittoCloudSyncEnabled) {
5984
- const dittoCloudURL = customDittoCloudURL !== null && customDittoCloudURL !== void 0 ? customDittoCloudURL : defaultDittoCloudURL(appID);
5985
- transportConfig.connect.websocketURLs.push(dittoCloudURL);
5986
- }
5987
- return {
5988
- underlyingSyncParameters: parameters,
5989
- effectiveTransportConfig: transportConfig.freeze(),
5990
- };
6272
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6273
+ return ditto.deferClose(() => {
6274
+ // IMPORTANT: this function maps a set of sync parameters to an effective
6275
+ // transport config plus some extra state where need be. Make sure
6276
+ // to keep this function free of side-effects.
6277
+ var _a;
6278
+ const transportConfig = parameters.transportConfig.copy();
6279
+ const identity = parameters.identity;
6280
+ const isSyncActive = parameters.isSyncActive;
6281
+ const isX509Valid = parameters.isX509Valid;
6282
+ const isWebValid = parameters.isWebValid;
6283
+ transportConfig.connect.tcpServers;
6284
+ transportConfig.connect.websocketURLs;
6285
+ let appID;
6286
+ let customDittoCloudURL;
6287
+ let isDittoCloudSyncEnabled = false;
6288
+ if (identity.type === 'onlinePlayground' || identity.type === 'onlineWithAuthentication') {
6289
+ // NOTE: enableDittoCloudSync is true by default for any online identity.
6290
+ appID = identity.appID;
6291
+ customDittoCloudURL = (_a = identity.customDittoCloudURL) !== null && _a !== void 0 ? _a : null;
6292
+ isDittoCloudSyncEnabled = identity.hasOwnProperty('enableDittoCloudSync') ? identity.enableDittoCloudSync : true;
6293
+ }
6294
+ // Not all P2P transports are always supported.
6295
+ const unavailableButEnabledP2PTransports = [];
6296
+ const isBLEAvailable = bleIsAvailable(dittoHandle.deref());
6297
+ const isAWDLAvailable = awdlIsAvailable(dittoHandle.deref());
6298
+ const isLANAvailable = lanIsAvailable(dittoHandle.deref());
6299
+ if (transportConfig.peerToPeer.bluetoothLE.isEnabled && !isBLEAvailable) {
6300
+ unavailableButEnabledP2PTransports.push('BluetoothLE');
6301
+ }
6302
+ if (transportConfig.peerToPeer.awdl.isEnabled && !isAWDLAvailable) {
6303
+ unavailableButEnabledP2PTransports.push('AWDL');
6304
+ }
6305
+ if (transportConfig.peerToPeer.lan.isEnabled && !isLANAvailable) {
6306
+ unavailableButEnabledP2PTransports.push('LAN');
6307
+ }
6308
+ if (unavailableButEnabledP2PTransports.length > 0) {
6309
+ throw new Error(`The following P2P transports are enabled in the transport config but are not supported in the current environment: ${unavailableButEnabledP2PTransports.join(', ')}`);
6310
+ }
6311
+ if (!isSyncActive || !isX509Valid) {
6312
+ transportConfig.peerToPeer.bluetoothLE.isEnabled = false;
6313
+ transportConfig.peerToPeer.awdl.isEnabled = false;
6314
+ transportConfig.peerToPeer.lan.isEnabled = false;
6315
+ transportConfig.listen.tcp.isEnabled = false;
6316
+ transportConfig.connect.tcpServers = [];
6317
+ }
6318
+ if (!isSyncActive || !isWebValid) {
6319
+ transportConfig.connect.websocketURLs = [];
6320
+ }
6321
+ if (!isSyncActive) {
6322
+ transportConfig.listen.http.isEnabled = false;
6323
+ }
6324
+ // NOTE: we have to smuggle in an additional Ditto Cloud websocket URL to
6325
+ // connect to if cloud sync is enabled.
6326
+ if (isSyncActive && isWebValid && isDittoCloudSyncEnabled) {
6327
+ const dittoCloudURL = customDittoCloudURL !== null && customDittoCloudURL !== void 0 ? customDittoCloudURL : defaultDittoCloudURL(appID);
6328
+ transportConfig.connect.websocketURLs.push(dittoCloudURL);
6329
+ }
6330
+ return {
6331
+ underlyingSyncParameters: parameters,
6332
+ effectiveTransportConfig: transportConfig.freeze(),
6333
+ };
6334
+ });
6335
+ }
6336
+
6337
+ //
6338
+ /**
6339
+ * Tracks `Subscription` instances in order to remove them when Ditto is
6340
+ * closed.
6341
+ *
6342
+ * @internal
6343
+ */
6344
+ class SubscriptionManager {
6345
+ /** @internal */
6346
+ constructor(ditto) {
6347
+ this.ditto = ditto;
6348
+ this.subscriptions = {};
6349
+ this.finalizationRegistry = new FinalizationRegistry(this.removeWithContextinfo.bind(this));
6350
+ }
6351
+ /**
6352
+ * Begin tracking a subscription instance and start it.
6353
+ *
6354
+ * @internal */
6355
+ add(subscription) {
6356
+ const ditto = this.ditto;
6357
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6358
+ const contextInfo = subscription.contextInfo;
6359
+ ditto.deferClose(async () => {
6360
+ this.subscriptions[contextInfo.id] = new WeakRef(subscription);
6361
+ this.finalizationRegistry.register(subscription, subscription.contextInfo, subscription);
6362
+ addSubscription(dittoHandle.deref(), contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset);
6363
+ });
6364
+ }
6365
+ /**
6366
+ * Stop tracking a subscription instance and cancel it.
6367
+ *
6368
+ * @internal */
6369
+ remove(subscription) {
6370
+ if (this.subscriptions[subscription.contextInfo.id] == null) {
6371
+ throw new Error(`Internal inconsistency, tried to remove a subscription that is not tracked: ${subscription.contextInfo.id}`);
6372
+ }
6373
+ this.finalizationRegistry.unregister(subscription);
6374
+ this.removeWithContextinfo(subscription.contextInfo);
6375
+ }
6376
+ /**
6377
+ * Stop tracking all subscriptions and cancel them.
6378
+ *
6379
+ * @internal */
6380
+ close() {
6381
+ this.ditto.deferClose(async () => {
6382
+ for (const subscriptionID in this.subscriptions) {
6383
+ const subscription = this.subscriptions[subscriptionID].deref();
6384
+ if (subscription != null) {
6385
+ // This doesn't call `Subscription.cancel()` because that is not
6386
+ // async and we want to wait for all subscriptions to be removed.
6387
+ this.remove(subscription);
6388
+ }
6389
+ }
6390
+ });
6391
+ }
6392
+ /**
6393
+ * Remove tracked subscription without unregistering from finalization
6394
+ * registry.
6395
+ *
6396
+ * @internal */
6397
+ removeWithContextinfo(contextInfo) {
6398
+ const ditto = this.ditto;
6399
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6400
+ ditto.deferClose(() => {
6401
+ delete this.subscriptions[contextInfo.id];
6402
+ removeSubscription(dittoHandle.deref(), contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset);
6403
+ });
6404
+ }
6405
+ }
6406
+
6407
+ //
6408
+ /**
6409
+ * These objects are returned by calls to
6410
+ * {@link Collection.fetchAttachment | fetchAttachment()} on {@link Collection}
6411
+ * and allow you to stop an in-flight attachment fetch.
6412
+ */
6413
+ class AttachmentFetcher {
6414
+ /**
6415
+ * Stops fetching the associated attachment and cleans up any associated
6416
+ * resources.
6417
+ *
6418
+ * Note that you are not required to call `stop()` once your attachment fetch
6419
+ * operation has finished. The method primarily exists to allow you to cancel
6420
+ * an attachment fetch request while it is ongoing if you no longer wish for
6421
+ * the attachment to be made available locally to the device.
6422
+ */
6423
+ stop() {
6424
+ // No need for synchronicity here: we let the "stop promise" float / run in
6425
+ // a detached fashion since there is no point in awaiting the "stop
6426
+ // signaling" itself.
6427
+ step(async () => {
6428
+ await this.manager.stopAttachmentFetcher(this);
6429
+ if (this.rejectPendingFetch != null) {
6430
+ this.rejectPendingFetch();
6431
+ }
6432
+ });
6433
+ }
6434
+ /** @internal */
6435
+ then(onfulfilled, onrejected) {
6436
+ return this.attachment.then(onfulfilled, onrejected);
6437
+ }
6438
+ /** @internal */
6439
+ constructor(ditto, token, manager, eventHandler) {
6440
+ // --------------------------------------- Internal ------------------------
6441
+ /** @internal */
6442
+ this.cancelTokenPromise = null;
6443
+ /**
6444
+ * This function is defined while a fetch is in progress and is used to reject
6445
+ * the promise `this.attachment` when the fetch is canceled.
6446
+ *
6447
+ * @internal
6448
+ */
6449
+ this.rejectPendingFetch = null;
6450
+ this.ditto = ditto;
6451
+ this.token = token;
6452
+ this.manager = manager;
6453
+ this.id = generateEphemeralToken();
6454
+ const eventHandlerOrNoOp = eventHandler || function () { };
6455
+ const dittoHandle = Bridge.ditto.handleFor(ditto);
6456
+ this.attachment = new Promise((resolve, reject) => {
6457
+ // REFACTOR: The callbacks hold quite a bunch of objects with most
6458
+ // probably lead to retain cycles and therefore memory leaks. This needs
6459
+ // to be
6460
+ // fixed.
6461
+ const onComplete = (attachmentHandlePointer) => {
6462
+ const attachment = new Attachment(this.ditto, this.token);
6463
+ Bridge.attachment.bridge(attachmentHandlePointer, () => attachment);
6464
+ eventHandlerOrNoOp({ type: 'Completed', attachment });
6465
+ this.rejectPendingFetch = null;
6466
+ resolve(attachment);
6467
+ };
6468
+ const onProgress = (downloaded, toDownload) => {
6469
+ eventHandlerOrNoOp({ type: 'Progress', totalBytes: toDownload, downloadedBytes: downloaded });
6470
+ };
6471
+ const onDelete = () => {
6472
+ eventHandlerOrNoOp({ type: 'Deleted' });
6473
+ this.rejectPendingFetch = null;
6474
+ resolve(null);
6475
+ };
6476
+ const onError = () => {
6477
+ };
6478
+ // The core doesn't call any of the handlers defined above when a fetch is
6479
+ // cancelled through `this.stop()` so we use this function to reject the
6480
+ // promise from the outside.
6481
+ this.rejectPendingFetch = () => {
6482
+ reject(new Error('Attachment fetch was canceled'));
6483
+ };
6484
+ // Not awaited yet; only needs to be fully resolved (and `await`ed over)
6485
+ // when using the actual `cancelTokenPromise`, which only happens in
6486
+ // `AttachmentFetcherManager.stopWithContextInfo()`.
6487
+ // @ts-expect-error setting readonly property
6488
+ this.cancelTokenPromise = dittoResolveAttachment(dittoHandle.deref(), token.id, { onComplete, onProgress, onDelete }, onError);
6489
+ });
6490
+ }
6491
+ }
6492
+
6493
+ //
6494
+ /**
6495
+ * Manages attachment fetchers to make sure we free all resources when the
6496
+ * fetcher is garbage collected and to allow us to wait for freeing of
6497
+ * ressources to be finished before the ditto instance is closed.
6498
+ *
6499
+ * @internal */
6500
+ class AttachmentFetcherManager {
6501
+ /** @internal */
6502
+ constructor(ditto) {
6503
+ // --- Private ---
6504
+ this.contextInfoByID = {};
6505
+ this.finalizationRegistry = new FinalizationRegistry(this.stopWithContextInfo);
6506
+ this.ditto = ditto;
6507
+ }
6508
+ /**
6509
+ * Start an attachment fetcher.
6510
+ *
6511
+ * @internal */
6512
+ startAttachmentFetcher(token, eventHandler) {
6513
+ return this.ditto.deferClose(() => {
6514
+ const attachmentFetcher = new AttachmentFetcher(this.ditto, token, this, eventHandler);
6515
+ // Register in finalization registry.
6516
+ const contextInfo = {
6517
+ id: attachmentFetcher.id,
6518
+ attachmentTokenID: token.id,
6519
+ cancelTokenPromise: attachmentFetcher.cancelTokenPromise,
6520
+ attachmentFetcher: new WeakRef(attachmentFetcher),
6521
+ };
6522
+ this.finalizationRegistry.register(attachmentFetcher, contextInfo, attachmentFetcher);
6523
+ // Keep a reference to the context info so that we can stop the fetcher.
6524
+ this.contextInfoByID[attachmentFetcher.id] = contextInfo;
6525
+ // Prevent cancellation of the fetch once it was fulfilled or rejected.
6526
+ const resetCancelToken = () => {
6527
+ if (this.contextInfoByID[attachmentFetcher.id] != null) {
6528
+ this.contextInfoByID[attachmentFetcher.id].cancelTokenPromise = null;
6529
+ }
6530
+ };
6531
+ attachmentFetcher.attachment.then((value) => {
6532
+ resetCancelToken();
6533
+ return value;
6534
+ }, (reason) => {
6535
+ resetCancelToken();
6536
+ return reason;
6537
+ });
6538
+ // Keep the attachment fetcher alive until it is stopped.
6539
+ this.ditto.keepAlive.retain(`AttachmentFetcher.${attachmentFetcher.id})`);
6540
+ return attachmentFetcher;
6541
+ });
6542
+ }
6543
+ /**
6544
+ * Stop an attachment fetcher and wait for it to be stopped.
6545
+ *
6546
+ * @internal */
6547
+ async stopAttachmentFetcher(attachmentFetcher) {
6548
+ this.finalizationRegistry.unregister(attachmentFetcher);
6549
+ const contextInfo = this.contextInfoByID[attachmentFetcher.id];
6550
+ if (contextInfo == null) {
6551
+ throw new Error(`Internal inconsistency: cannot stop attachment fetcher ${attachmentFetcher.id}, which is not registered.`);
6552
+ }
6553
+ await this.stopWithContextInfo(contextInfo);
6554
+ }
6555
+ /**
6556
+ * Closing the manager will cancel all attachment fetchers.
6557
+ *
6558
+ * @internal
6559
+ */
6560
+ close() {
6561
+ // REFACTOR: the closing closure runs detached from here which is a smell.
6562
+ // Can we make this whole closing mechanism non-async? The culprit is
6563
+ // the cancelTokenPromise that _have to_ `await` on close. The reason
6564
+ // for this is that the FFI function `ditto_resolve_attachment()` is
6565
+ // async but should be non-async. Refactor and make that non-async, then
6566
+ // de-async-ify the closing mechanism.
6567
+ this.ditto.deferCloseAsync(async () => {
6568
+ const contextInfos = Object.values(this.contextInfoByID);
6569
+ const stopped = contextInfos.map(async (contextInfo) => {
6570
+ const attachmentFetcher = contextInfo.attachmentFetcher.deref();
6571
+ if (attachmentFetcher != null) {
6572
+ await this.stopAttachmentFetcher(attachmentFetcher);
6573
+ }
6574
+ });
6575
+ await Promise.all(stopped);
6576
+ });
6577
+ }
6578
+ /**
6579
+ * Stop the attachment fetcher without unregistering it from the finalization
6580
+ * registry.
6581
+ */
6582
+ stopWithContextInfo(contextInfo) {
6583
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto);
6584
+ return this.ditto.deferCloseAsync(async () => {
6585
+ // Remove the manager's own record of the context info.
6586
+ if (this.contextInfoByID[contextInfo.id] == null) {
6587
+ throw new Error(`Internal inconsistency: attachment fetcher ${contextInfo.id} not found in active attachment fetchers.`);
6588
+ }
6589
+ delete this.contextInfoByID[contextInfo.id];
6590
+ // Release the keep-alive.
6591
+ this.ditto.keepAlive.release(`AttachmentFetcher.${contextInfo.id})`);
6592
+ // Cancel the fetcher if it is still running.
6593
+ const cancelToken = await contextInfo.cancelTokenPromise;
6594
+ if (cancelToken) {
6595
+ dittoCancelResolveAttachment(dittoHandle.deref(), contextInfo.attachmentTokenID, cancelToken);
6596
+ }
6597
+ });
6598
+ }
5991
6599
  }
5992
6600
 
5993
6601
  //
@@ -5998,8 +6606,10 @@ function stateFrom(parameters) {
5998
6606
  class Ditto {
5999
6607
  /** Returns a string identifying the version of the Ditto SDK. */
6000
6608
  get sdkVersion() {
6001
- const dittoPointer = Bridge.ditto.pointerFor(this);
6002
- return dittoGetSDKVersion(dittoPointer);
6609
+ const dittoHandle = Bridge.ditto.handleFor(this);
6610
+ return this.deferClose(() => {
6611
+ return dittoGetSDKVersion(dittoHandle.deref());
6612
+ });
6003
6613
  }
6004
6614
  /**
6005
6615
  * Initializes a new `Ditto` instance.
@@ -6020,8 +6630,16 @@ class Ditto {
6020
6630
  * @see {@link Ditto.path}
6021
6631
  */
6022
6632
  constructor(identity, path) {
6633
+ /**
6634
+ * `true` once {@link close | Ditto.close()} has been called, otherwise
6635
+ * `false`.
6636
+ */
6637
+ this.isClosed = false;
6638
+ this.deferCloseAllowed = true;
6023
6639
  this.isWebValid = false;
6024
6640
  this.isX509Valid = false;
6641
+ /** Set of pending operations that need to complete before the Ditto instance can be closed in a safe manner. */
6642
+ this.pendingOperations = new Set();
6025
6643
  if (!Ditto.isEnvironmentSupported()) {
6026
6644
  throw new Error('Ditto does not support this JavaScript environment. Please consult the Ditto JavaScript documentation for a list of supported environments and browsers. You can use `Ditto.isEnvironmentSupported()` to run this check anytime.');
6027
6645
  }
@@ -6056,50 +6674,35 @@ class Ditto {
6056
6674
  // it after all pieces are in place.
6057
6675
  let secondsRemainingUntilAuthenticationExpires = null;
6058
6676
  const weakThis = new WeakRef(this);
6059
- const authClientX = (() => {
6677
+ const identityConfig = (() => {
6060
6678
  var _a, _b, _c;
6061
6679
  if (validIdentity.type === 'offlinePlayground') {
6062
- return dittoAuthClientMakeForDevelopment(pathOrDefault, validIdentity.appID, (_a = validIdentity.siteID) !== null && _a !== void 0 ? _a : 0);
6680
+ return dittoIdentityConfigMakeOfflinePlayground(validIdentity.appID, (_a = validIdentity.siteID) !== null && _a !== void 0 ? _a : 0);
6063
6681
  }
6064
6682
  if (validIdentity.type === 'manual') {
6065
- return dittoAuthClientMakeWithStaticX509(validIdentity.certificate);
6683
+ return dittoIdentityConfigMakeManual(validIdentity.certificate);
6066
6684
  }
6067
6685
  if (validIdentity.type === 'sharedKey') {
6068
- return dittoAuthClientMakeWithSharedKey(pathOrDefault, validIdentity.appID, validIdentity.sharedKey, validIdentity.siteID);
6686
+ return dittoIdentityConfigMakeSharedKey(validIdentity.appID, validIdentity.sharedKey, validIdentity.siteID);
6069
6687
  }
6070
6688
  if (validIdentity.type === 'onlinePlayground') {
6071
6689
  const authURL = (_b = validIdentity.customAuthURL) !== null && _b !== void 0 ? _b : defaultAuthURL(validIdentity.appID);
6072
- return dittoAuthClientMakeAnonymousClient(pathOrDefault, validIdentity.appID, validIdentity.token, authURL);
6690
+ return dittoIdentityConfigMakeOnlinePlayground(validIdentity.appID, validIdentity.token, authURL);
6073
6691
  }
6074
6692
  if (validIdentity.type === 'onlineWithAuthentication') {
6075
6693
  const authURL = (_c = validIdentity.customAuthURL) !== null && _c !== void 0 ? _c : defaultAuthURL(validIdentity.appID);
6076
- const loginProviderX = dittoAuthClientMakeLoginProvider(function (secondsRemaining) {
6077
- const strongThis = weakThis.deref();
6078
- if (!strongThis) {
6079
- Logger.warning(`Internal inconsistency, LoginProvider callback fired after the corresponding Ditto instance has been deallocated.`);
6080
- return;
6081
- }
6082
- if (strongThis.auth) {
6083
- strongThis.auth['@ditto.authenticationExpiring'](secondsRemaining);
6084
- }
6085
- else {
6086
- // WORKAROUND: see description above where the
6087
- // secondsRemainingUntilAuthenticationExpires variable is declared.
6088
- secondsRemainingUntilAuthenticationExpires = secondsRemaining;
6089
- }
6090
- });
6091
- return dittoAuthClientMakeWithWeb(pathOrDefault, validIdentity.appID, authURL, loginProviderX);
6694
+ return dittoIdentityConfigMakeOnlineWithAuthentication(validIdentity.appID, authURL);
6092
6695
  }
6093
6696
  throw new Error(`Can't create Ditto, unsupported identity type: ${validIdentity}`);
6094
6697
  })();
6095
- dittoAuthClientSetValidityListener(authClientX, function (...args) {
6698
+ const dittoPointer = dittoMake(uninitializedDittoX, identityConfig);
6699
+ dittoAuthClientSetValidityListener(dittoPointer, function (...args) {
6096
6700
  var _a;
6097
6701
  (_a = weakThis.deref()) === null || _a === void 0 ? void 0 : _a.authClientValidityChanged(...args);
6098
6702
  });
6099
- const isWebValid = dittoAuthClientIsWebValid(authClientX);
6100
- const isX509Valid = dittoAuthClientIsX509Valid(authClientX);
6101
- const siteID = dittoAuthClientGetSiteID(authClientX);
6102
- const dittoPointer = dittoMake(uninitializedDittoX, authClientX);
6703
+ const isWebValid = dittoAuthClientIsWebValid(dittoPointer);
6704
+ const isX509Valid = dittoAuthClientIsX509Valid(dittoPointer);
6705
+ const siteID = dittoAuthClientGetSiteID(dittoPointer);
6103
6706
  Bridge.ditto.bridge(dittoPointer, this);
6104
6707
  // IMPORTANT: Keeping the auth client around accumulates run-times and
6105
6708
  // resources which becomes a problem specifically in tests (where we use one
@@ -6107,10 +6710,30 @@ class Ditto {
6107
6710
  // _only_ for the onlineWithAuthentication and online identities and
6108
6711
  // transfer ownership of it to the authenticator.
6109
6712
  if (validIdentity.type === 'onlineWithAuthentication') {
6110
- this.auth = new OnlineAuthenticator(this.keepAlive, authClientX, this, validIdentity.authHandler);
6713
+ this.auth = new OnlineAuthenticator(this.keepAlive, this, validIdentity.authHandler);
6714
+ const loginProviderX = dittoAuthClientMakeLoginProvider(function (secondsRemaining) {
6715
+ const strongThis = weakThis.deref();
6716
+ if (!strongThis) {
6717
+ Logger.warning(`Internal inconsistency, LoginProvider callback fired after the corresponding Ditto instance has been deallocated.`);
6718
+ return;
6719
+ }
6720
+ if (strongThis.auth) {
6721
+ strongThis.auth['@ditto.authenticationExpiring'](secondsRemaining);
6722
+ }
6723
+ else {
6724
+ // WORKAROUND: see description above where the
6725
+ // secondsRemainingUntilAuthenticationExpires variable is declared.
6726
+ secondsRemainingUntilAuthenticationExpires = secondsRemaining;
6727
+ }
6728
+ });
6729
+ // We don't need to worry about awaiting the result of this call because
6730
+ // auth all happens in the background and so there are no guarantees we
6731
+ // need to uphold by making sure things are in a certain state before the
6732
+ // constructor finishes
6733
+ dittoAuthSetLoginProvider(dittoPointer, loginProviderX);
6111
6734
  }
6112
6735
  else if (validIdentity.type === 'onlinePlayground') {
6113
- this.auth = new OnlineAuthenticator(this.keepAlive, authClientX, this, {
6736
+ this.auth = new OnlineAuthenticator(this.keepAlive, this, {
6114
6737
  authenticationRequired: function (authenticator) {
6115
6738
  // No-op.
6116
6739
  },
@@ -6120,7 +6743,6 @@ class Ditto {
6120
6743
  });
6121
6744
  }
6122
6745
  else {
6123
- dittoAuthClientFree(authClientX);
6124
6746
  this.auth = new NotAvailableAuthenticator(this.keepAlive);
6125
6747
  }
6126
6748
  const transportConfig = this.makeDefaultTransportConfig();
@@ -6136,7 +6758,10 @@ class Ditto {
6136
6758
  this.store = new Store(this);
6137
6759
  this.presence = new Presence(this);
6138
6760
  this.presenceManager = new PresenceManager(this);
6761
+ this.liveQueryManager = new LiveQueryManager(this, this.keepAlive);
6762
+ this.attachmentFetcherManager = new AttachmentFetcherManager(this);
6139
6763
  this.transportConditionsManager = new TransportConditionsManager(this);
6764
+ this.subscriptionManager = new SubscriptionManager(this);
6140
6765
  // WORKAROUND: see description above where the
6141
6766
  // secondsRemainingUntilAuthenticationExpires variable is declared.
6142
6767
  if (secondsRemainingUntilAuthenticationExpires !== null) {
@@ -6305,8 +6930,10 @@ class Ditto {
6305
6930
  * Only available in Node environments at the moment, no-op in the browser.
6306
6931
  */
6307
6932
  runGarbageCollection() {
6308
- const dittoPointer = Bridge.ditto.pointerFor(this);
6309
- dittoRunGarbageCollection(dittoPointer);
6933
+ const dittoHandle = Bridge.ditto.handleFor(this);
6934
+ return this.deferClose(() => {
6935
+ dittoRunGarbageCollection(dittoHandle.deref());
6936
+ });
6310
6937
  }
6311
6938
  /**
6312
6939
  * Explicitly opt-in to disabling the ability to sync with Ditto peers running
@@ -6318,8 +6945,99 @@ class Ditto {
6318
6945
  * or has (transitively) had disabled, syncing with v3 SDK peers.
6319
6946
  */
6320
6947
  disableSyncWithV3() {
6321
- const dittoPointer = Bridge.ditto.pointerFor(this);
6322
- dittoDisableSyncWithV3(dittoPointer);
6948
+ const dittoHandle = Bridge.ditto.handleFor(this);
6949
+ return this.deferClose(() => {
6950
+ dittoDisableSyncWithV3(dittoHandle.deref());
6951
+ });
6952
+ }
6953
+ /**
6954
+ * Shut down Ditto and release all resources.
6955
+ *
6956
+ * Must be called before recreating a Ditto instance that uses the same
6957
+ * persistence directory.
6958
+ */
6959
+ async close() {
6960
+ if (this.isClosed) {
6961
+ return;
6962
+ }
6963
+ this['isClosed'] = true;
6964
+ this.stopSync();
6965
+ this.store.close();
6966
+ this.presence.close();
6967
+ this.auth.close();
6968
+ this.sync.close();
6969
+ this.presenceManager.close();
6970
+ this.liveQueryManager.close();
6971
+ this.attachmentFetcherManager.close();
6972
+ this.transportConditionsManager.close();
6973
+ this.subscriptionManager.close();
6974
+ if (this.keepAlive.isActive) {
6975
+ throw new Error('Internal inconsistency, still kept alive after the Ditto object has been close()-ed. ');
6976
+ }
6977
+ // Await all pending operations before closing. Rejected promises are
6978
+ // ignored because they are handled at the original call site.
6979
+ do {
6980
+ await Promise.allSettled(this.pendingOperations);
6981
+ // REFACTOR: in theory, we could end up in an endless loop here if for
6982
+ // some reason a resolved or rejected promise isn't removed from
6983
+ // `pendingOperations`. AFAICS, this is not possible atm due to the
6984
+ // way `deferClose` and `deferCloseAsync` is implemented. Would be
6985
+ // great to rework this and make it more solid if possible.
6986
+ } while (this.pendingOperations.size > 0);
6987
+ this.deferCloseAllowed = false;
6988
+ const dittoHandle = Bridge.ditto.handleFor(this);
6989
+ await dittoShutdown(dittoHandle.deref());
6990
+ Bridge.ditto.markAsClosed(this);
6991
+ }
6992
+ /**
6993
+ * The number of operations pending before the Ditto instance can be closed.
6994
+ *
6995
+ * For testing purposes only.
6996
+ * @internal */
6997
+ get numPendingOperations() {
6998
+ return this.pendingOperations.size;
6999
+ }
7000
+ /**
7001
+ * Makes sure that the closure is executed only if the Ditto instance hasn't
7002
+ * been closed yet.
7003
+ *
7004
+ * @param closure the synchronous closure to execute.
7005
+ * @returns the result of the closure.
7006
+ * @throws if the Ditto instance was closed before calling this method.
7007
+ * @internal */
7008
+ deferClose(closure) {
7009
+ if (!this.deferCloseAllowed) {
7010
+ throw new Error(`Can't perform operation using a Ditto instance that has been closed.`);
7011
+ }
7012
+ return closure();
7013
+ }
7014
+ /**
7015
+ * Makes sure that the closure is executed to completion before the Ditto
7016
+ * instance is closed.
7017
+ *
7018
+ * Any calls to {@link close | `Ditto.close()`} will wait until the closure
7019
+ * has completed before closing the Ditto instance.
7020
+ *
7021
+ * @param closure the asynchronous closure to execute.
7022
+ * @returns the result of the closure.
7023
+ * @throws if the Ditto instance was closed before calling this method.
7024
+ * @internal */
7025
+ async deferCloseAsync(closure) {
7026
+ if (!this.deferCloseAllowed) {
7027
+ throw new Error(`Can't perform operation using a Ditto instance that has been closed.`);
7028
+ }
7029
+ const pendingOperation = closure();
7030
+ this.pendingOperations.add(pendingOperation);
7031
+ let result;
7032
+ try {
7033
+ result = await pendingOperation;
7034
+ }
7035
+ finally {
7036
+ // Remove the promise from the set of pending operations even if it
7037
+ // rejected.
7038
+ this.pendingOperations.delete(pendingOperation);
7039
+ }
7040
+ return result;
6323
7041
  }
6324
7042
  authClientValidityChanged(isWebValid, isX509Valid) {
6325
7043
  const transportConfig = this.transportConfig;
@@ -6370,49 +7088,41 @@ class Ditto {
6370
7088
  return validIdentity;
6371
7089
  }
6372
7090
  setSyncActive(flag) {
6373
- if (flag && IdentityTypesRequiringOfflineLicenseToken.includes(this.identity.type) && !this.isActivated) {
6374
- throw new Error('Sync could not be started because Ditto has not yet been activated. This can be achieved with a successful call to `setOfflineOnlyLicenseToken`. If you need to obtain a license token then please visit https://portal.ditto.live.');
6375
- }
6376
- if (!this.isSyncActive && flag) {
6377
- this.keepAlive.retain('sync');
6378
- }
6379
- if (this.isSyncActive && !flag) {
6380
- this.keepAlive.release('sync');
6381
- }
6382
- const dittoPointer = Bridge.ditto.pointerFor(this);
6383
- this['isSyncActive'] = flag;
6384
- const isWebValid = this.isWebValid;
6385
- const isX509Valid = this.isX509Valid;
6386
- const identity = this.identity;
6387
- const transportConfig = this.transportConfig;
6388
- dittoSetDeviceName(dittoPointer, this.deviceName);
6389
- this.sync.update({ identity, transportConfig, isWebValid, isX509Valid, isSyncActive: !!flag, ditto: this });
7091
+ const dittoHandle = Bridge.ditto.handleFor(this);
7092
+ this.deferClose(() => {
7093
+ if (flag && IdentityTypesRequiringOfflineLicenseToken.includes(this.identity.type) && !this.isActivated) {
7094
+ throw new Error('Sync could not be started because Ditto has not yet been activated. This can be achieved with a successful call to `setOfflineOnlyLicenseToken`. If you need to obtain a license token then please visit https://portal.ditto.live.');
7095
+ }
7096
+ if (!this.isSyncActive && flag) {
7097
+ this.keepAlive.retain('sync');
7098
+ }
7099
+ if (this.isSyncActive && !flag) {
7100
+ this.keepAlive.release('sync');
7101
+ }
7102
+ this['isSyncActive'] = flag;
7103
+ const isWebValid = this.isWebValid;
7104
+ const isX509Valid = this.isX509Valid;
7105
+ const identity = this.identity;
7106
+ const transportConfig = this.transportConfig;
7107
+ dittoSetDeviceName(dittoHandle.deref(), this.deviceName);
7108
+ this.sync.update({ identity, transportConfig, isWebValid, isX509Valid, isSyncActive: !!flag, ditto: this });
7109
+ });
6390
7110
  }
6391
7111
  makeDefaultTransportConfig() {
6392
- const dittoPointer = Bridge.ditto.pointerFor(this);
6393
- const transportConfig = new TransportConfig();
6394
- if (bleIsAvailable(dittoPointer)) {
6395
- transportConfig.peerToPeer.bluetoothLE.isEnabled = true;
6396
- }
6397
- if (awdlIsAvailable(dittoPointer)) {
6398
- transportConfig.peerToPeer.awdl.isEnabled = true;
6399
- }
6400
- if (lanIsAvailable(dittoPointer)) {
6401
- transportConfig.peerToPeer.lan.isEnabled = true;
6402
- }
6403
- // WORKAROUND: the Linux implementation panics if no BLE hardware is
6404
- // available but should report `NoBleHardware` via transport condition
6405
- // observation instead. Until this is fixed, we'll disable the BLE transport
6406
- // by default on Linux. To undo, remove the following block of code.
6407
- //
6408
- // See issue #5686 for details:
6409
- // https://github.com/getditto/ditto/pull/5608
6410
- {
6411
- if (process.platform === 'linux') {
6412
- transportConfig.peerToPeer.bluetoothLE.isEnabled = false;
7112
+ const dittoHandle = Bridge.ditto.handleFor(this);
7113
+ return this.deferClose(() => {
7114
+ const transportConfig = new TransportConfig();
7115
+ if (bleIsAvailable(dittoHandle.deref())) {
7116
+ transportConfig.peerToPeer.bluetoothLE.isEnabled = true;
6413
7117
  }
6414
- }
6415
- return transportConfig.freeze();
7118
+ if (awdlIsAvailable(dittoHandle.deref())) {
7119
+ transportConfig.peerToPeer.awdl.isEnabled = true;
7120
+ }
7121
+ if (lanIsAvailable(dittoHandle.deref())) {
7122
+ transportConfig.peerToPeer.lan.isEnabled = true;
7123
+ }
7124
+ return transportConfig.freeze();
7125
+ });
6416
7126
  }
6417
7127
  }
6418
7128
  /** @internal */