@peerbit/document 12.3.4 → 12.3.5-3f16953

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/src/search.ts CHANGED
@@ -1338,7 +1338,23 @@ export class DocumentIndex<
1338
1338
  );
1339
1339
  }
1340
1340
  this.clearAllResultQueues();
1341
- await this.index?.stop?.();
1341
+ await this._resumableIterators.clearAll();
1342
+ if (this.iteratorKeepAliveTimers) {
1343
+ for (const timer of this.iteratorKeepAliveTimers.values()) {
1344
+ clearTimeout(timer);
1345
+ }
1346
+ this.iteratorKeepAliveTimers.clear();
1347
+ }
1348
+ try {
1349
+ await this.index?.stop?.();
1350
+ } catch (error) {
1351
+ // Be defensive during teardown: stopping an already-stopped index shouldn't
1352
+ // prevent closing the program and releasing timers/iterators.
1353
+ if (error instanceof indexerTypes.NotStartedError) {
1354
+ return closed;
1355
+ }
1356
+ throw error;
1357
+ }
1342
1358
  }
1343
1359
  return closed;
1344
1360
  }
@@ -1351,8 +1367,27 @@ export class DocumentIndex<
1351
1367
  this.handleDocumentChange,
1352
1368
  );
1353
1369
  this.clearAllResultQueues();
1354
- await this.index?.drop?.();
1355
- await this.index?.stop?.();
1370
+ await this._resumableIterators.clearAll();
1371
+ if (this.iteratorKeepAliveTimers) {
1372
+ for (const timer of this.iteratorKeepAliveTimers.values()) {
1373
+ clearTimeout(timer);
1374
+ }
1375
+ this.iteratorKeepAliveTimers.clear();
1376
+ }
1377
+ try {
1378
+ await this.index?.drop?.();
1379
+ } catch (error) {
1380
+ if (!(error instanceof indexerTypes.NotStartedError)) {
1381
+ throw error;
1382
+ }
1383
+ }
1384
+ try {
1385
+ await this.index?.stop?.();
1386
+ } catch (error) {
1387
+ if (!(error instanceof indexerTypes.NotStartedError)) {
1388
+ throw error;
1389
+ }
1390
+ }
1356
1391
  }
1357
1392
  return dropped;
1358
1393
  }
@@ -1367,17 +1402,24 @@ export class DocumentIndex<
1367
1402
  options?: Options,
1368
1403
  ): Promise<WithContext<I>>;
1369
1404
 
1370
- public async get<
1371
- Options extends GetOptions<T, I, D, Resolve>,
1372
- Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
1373
- >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
1374
- let deferred:
1375
- | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
1376
- | undefined;
1377
-
1378
- // Normalize the id key early so listeners can use it
1379
- let idKey =
1380
- key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
1405
+ public async get<
1406
+ Options extends GetOptions<T, I, D, Resolve>,
1407
+ Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
1408
+ >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
1409
+ let deferred:
1410
+ | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
1411
+ | undefined;
1412
+ let baseRemote:
1413
+ | RemoteQueryOptions<
1414
+ types.AbstractSearchRequest,
1415
+ types.AbstractSearchResult,
1416
+ D
1417
+ >
1418
+ | undefined;
1419
+
1420
+ // Normalize the id key early so listeners can use it
1421
+ let idKey =
1422
+ key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
1381
1423
 
1382
1424
  if (options?.waitFor) {
1383
1425
  // add change listener before query because we might get a concurrent change that matches the query,
@@ -1410,16 +1452,16 @@ export class DocumentIndex<
1410
1452
 
1411
1453
  let timeout = setTimeout(resolveUndefined, options.waitFor);
1412
1454
  this.events.addEventListener("close", resolveUndefined);
1413
- this.documentEvents.addEventListener("change", listener);
1414
- deferred.promise.then(cleanup);
1415
-
1416
- // Prepare remote options without mutating caller options
1417
- const baseRemote =
1418
- options?.remote === false
1419
- ? undefined
1420
- : typeof options?.remote === "object"
1421
- ? { ...options.remote }
1422
- : {};
1455
+ this.documentEvents.addEventListener("change", listener);
1456
+ deferred.promise.then(cleanup);
1457
+
1458
+ // Prepare remote options without mutating caller options
1459
+ baseRemote =
1460
+ options?.remote === false
1461
+ ? undefined
1462
+ : typeof options?.remote === "object"
1463
+ ? { ...options.remote }
1464
+ : {};
1423
1465
  if (baseRemote) {
1424
1466
  const waitPolicy = baseRemote.wait;
1425
1467
  if (
@@ -1455,16 +1497,20 @@ export class DocumentIndex<
1455
1497
  deferred!.resolve(first.value as any);
1456
1498
  }
1457
1499
  },
1458
- });
1500
+ });
1501
+ }
1459
1502
  }
1460
- }
1461
1503
 
1462
- const result = (await this.getDetailed(idKey, options))?.[0]?.results[0];
1504
+ const initialOptions = baseRemote
1505
+ ? ({ ...(options as any), remote: baseRemote } as Options)
1506
+ : options;
1507
+ const result =
1508
+ (await this.getDetailed(idKey, initialOptions))?.[0]?.results[0];
1463
1509
 
1464
- // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
1465
- if (!result) {
1466
- return deferred?.promise;
1467
- } else if (deferred) {
1510
+ // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
1511
+ if (!result) {
1512
+ return deferred?.promise;
1513
+ } else if (deferred) {
1468
1514
  deferred.resolve(undefined);
1469
1515
  }
1470
1516
  return result?.value;
@@ -1855,24 +1901,26 @@ export class DocumentIndex<
1855
1901
  const resolveFlag = resolvesDocuments(
1856
1902
  (fromQuery || query) as AnyIterationRequest,
1857
1903
  );
1858
- prevQueued = {
1859
- from,
1860
- queue: [],
1861
- timeout: setTimeout(() => {
1862
- this._resultQueue.delete(query.idString);
1863
- }, 6e4),
1864
- keptInIndex: kept,
1865
- fromQuery: (fromQuery || query) as
1866
- | types.SearchRequest
1867
- | types.SearchRequestIndexed
1868
- | types.IterationRequest,
1869
- resolveResults: resolveFlag,
1870
- };
1871
- if (
1872
- fromQuery instanceof types.IterationRequest &&
1873
- fromQuery.pushUpdates
1874
- ) {
1875
- prevQueued.pushMode = fromQuery.pushUpdates;
1904
+ prevQueued = {
1905
+ from,
1906
+ queue: [],
1907
+ timeout: setTimeout(() => {
1908
+ this._resultQueue.delete(query.idString);
1909
+ }, 6e4),
1910
+ keptInIndex: kept,
1911
+ fromQuery: (fromQuery || query) as
1912
+ | types.SearchRequest
1913
+ | types.SearchRequestIndexed
1914
+ | types.IterationRequest,
1915
+ resolveResults: resolveFlag,
1916
+ };
1917
+ // Don't keep Node alive just to GC old remote iterator state.
1918
+ prevQueued.timeout.unref?.();
1919
+ if (
1920
+ fromQuery instanceof types.IterationRequest &&
1921
+ fromQuery.pushUpdates
1922
+ ) {
1923
+ prevQueued.pushMode = fromQuery.pushUpdates;
1876
1924
  }
1877
1925
  this._resultQueue.set(query.idString, prevQueued);
1878
1926
  }
@@ -1970,17 +2018,19 @@ export class DocumentIndex<
1970
2018
  string,
1971
2019
  ReturnType<typeof setTimeout>
1972
2020
  >());
1973
- const timer = setTimeout(() => {
1974
- timers.delete(idString);
1975
- const queued = this._resultQueue.get(idString);
1976
- if (queued) {
1977
- clearTimeout(queued.timeout);
1978
- this._resultQueue.delete(idString);
1979
- }
1980
- this._resumableIterators.close({ idString });
1981
- }, delay);
1982
- timers.set(idString, timer);
1983
- }
2021
+ const timer = setTimeout(() => {
2022
+ timers.delete(idString);
2023
+ const queued = this._resultQueue.get(idString);
2024
+ if (queued) {
2025
+ clearTimeout(queued.timeout);
2026
+ this._resultQueue.delete(idString);
2027
+ }
2028
+ this._resumableIterators.close({ idString });
2029
+ }, delay);
2030
+ // This is a best-effort cleanup timer; it should not keep Node alive.
2031
+ timer.unref?.();
2032
+ timers.set(idString, timer);
2033
+ }
1984
2034
 
1985
2035
  private cancelIteratorKeepAlive(idString: string) {
1986
2036
  const timers = this.iteratorKeepAliveTimers;
@@ -2213,24 +2263,20 @@ export class DocumentIndex<
2213
2263
  queryRequest: R,
2214
2264
  options?: QueryDetailedOptions<T, I, D, boolean | undefined>,
2215
2265
  fetchFirstForRemote?: Set<string>,
2216
- ): Promise<types.Results<RT>[]> {
2217
- const local = typeof options?.local === "boolean" ? options?.local : true;
2218
- let remote:
2219
- | RemoteQueryOptions<
2220
- types.AbstractSearchRequest,
2221
- types.AbstractSearchResult,
2222
- D
2223
- >
2224
- | undefined = undefined;
2225
- if (typeof options?.remote === "boolean") {
2226
- if (options?.remote) {
2227
- remote = {};
2266
+ ): Promise<types.Results<RT>[]> {
2267
+ const local = typeof options?.local === "boolean" ? options?.local : true;
2268
+ let remote:
2269
+ | RemoteQueryOptions<
2270
+ types.AbstractSearchRequest,
2271
+ types.AbstractSearchResult,
2272
+ D
2273
+ >
2274
+ | undefined = undefined;
2275
+ if (typeof options?.remote === "boolean") {
2276
+ remote = options.remote ? {} : undefined;
2228
2277
  } else {
2229
- remote = undefined;
2278
+ remote = options?.remote || {};
2230
2279
  }
2231
- } else {
2232
- remote = options?.remote || {};
2233
- }
2234
2280
  if (remote && remote.priority == null) {
2235
2281
  // give queries higher priority than other "normal" data activities
2236
2282
  // without this, we might have a scenario that a peer joina network with large amount of data to be synced, but can not query anything before that is done
@@ -2266,19 +2312,76 @@ export class DocumentIndex<
2266
2312
  throw new Error("Unexpected");
2267
2313
  }
2268
2314
 
2269
- const replicatorGroups = options?.remote?.from
2270
- ? options?.remote?.from
2271
- : await this._log.getCover(remote.domain ?? { args: undefined }, {
2272
- roleAge: remote.minAge,
2273
- eager: remote.reach?.eager,
2274
- reachableOnly: !!remote.wait, // when we want to merge joining we can ignore pending to be online peers and instead consider them once they become online
2275
- signal: options?.signal,
2276
- });
2315
+ const coverProps = remote.domain ?? { args: undefined };
2316
+ const isDefaultDomainArgs =
2317
+ !("range" in coverProps) &&
2318
+ (!("args" in coverProps) || (coverProps as any).args == null);
2319
+
2320
+ let replicatorGroups = options?.remote?.from
2321
+ ? options?.remote?.from
2322
+ : await this._log.getCover(coverProps, {
2323
+ roleAge: remote.minAge,
2324
+ eager: remote.reach?.eager,
2325
+ reachableOnly: !!remote.wait, // when we want to merge joining we can ignore pending to be online peers and instead consider them once they become online
2326
+ signal: options?.signal,
2327
+ });
2328
+
2329
+ // Cold start: cover can be temporarily empty/self-only while replication metadata
2330
+ // converges. For remote search, it's sometimes better to at least try currently
2331
+ // connected peers, but only if we have evidence that a remote replicator exists.
2332
+ if (!options?.remote?.from && isDefaultDomainArgs) {
2333
+ const selfHash = this.node.identity.publicKey.hashcode();
2334
+ const remoteCount = replicatorGroups.filter((h) => h !== selfHash).length;
2335
+ if (remoteCount === 0) {
2336
+ const waitEnabled = Boolean(remote.wait);
2337
+ const coverIsSelfOnly =
2338
+ replicatorGroups.length === 1 && replicatorGroups[0] === selfHash;
2339
+
2340
+ // If the cover is explicitly empty (no shards), don't override it unless
2341
+ // the caller requested waiting for joins (e.g. get(waitFor)).
2342
+ if (!waitEnabled && !coverIsSelfOnly) {
2343
+ // no-op
2344
+ } else {
2345
+ let hasKnownRemoteReplicator = false;
2346
+ if (!waitEnabled) {
2347
+ try {
2348
+ const replicators = await this._log.getReplicators();
2349
+ for (const hash of replicators.keys()) {
2350
+ if (hash !== selfHash) {
2351
+ hasKnownRemoteReplicator = true;
2352
+ break;
2353
+ }
2354
+ }
2355
+ } catch {
2356
+ // Best-effort only.
2357
+ }
2358
+ }
2359
+
2360
+ if (waitEnabled || hasKnownRemoteReplicator) {
2361
+ const peerMap: Map<string, unknown> | undefined = (this.node.services
2362
+ .pubsub as any)?.peers;
2363
+ if (peerMap?.keys) {
2364
+ const extra: string[] = [];
2365
+ for (const hash of peerMap.keys()) {
2366
+ if (!hash || hash === selfHash) continue;
2367
+ extra.push(hash);
2368
+ if (extra.length >= 8) break;
2369
+ }
2370
+ if (extra.length > 0) {
2371
+ replicatorGroups = [
2372
+ ...new Set([...replicatorGroups, ...extra]),
2373
+ ];
2374
+ }
2375
+ }
2376
+ }
2377
+ }
2378
+ }
2379
+ }
2277
2380
 
2278
- if (replicatorGroups) {
2279
- const responseHandler = async (
2280
- results: {
2281
- response: types.AbstractSearchResult;
2381
+ if (replicatorGroups) {
2382
+ const responseHandler = async (
2383
+ results: {
2384
+ response: types.AbstractSearchResult;
2282
2385
  from?: PublicSignKey;
2283
2386
  }[],
2284
2387
  ) => {
@@ -2464,18 +2567,17 @@ export class DocumentIndex<
2464
2567
  options?: O,
2465
2568
  ): Promise<ValueTypeFromRequest<Resolve, T, I>[]> {
2466
2569
  // Set fetch to search size, or max value (default to max u32 (4294967295))
2467
- const coercedRequest = coerceQuery(
2468
- queryRequest,
2469
- options,
2470
- this.compatibility,
2471
- );
2472
- coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2570
+ const coercedRequest = coerceQuery(
2571
+ queryRequest,
2572
+ options,
2573
+ this.compatibility,
2574
+ );
2575
+ coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2473
2576
 
2474
- // So that the iterator is pre-fetching the right amount of entries
2475
- const iterator = this.iterate<Resolve>(coercedRequest, options);
2577
+ // Use an iterator so large results respect message size limits.
2578
+ const iterator = this.iterate<Resolve>(coercedRequest, options);
2476
2579
 
2477
- // So that this call will not do any remote requests
2478
- const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2580
+ const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2479
2581
 
2480
2582
  while (
2481
2583
  iterator.done() !== true &&
@@ -2876,37 +2978,43 @@ export class DocumentIndex<
2876
2978
 
2877
2979
  if (typeof options?.remote === "object") {
2878
2980
  let waitForTime: number | undefined = undefined;
2981
+ const waitPolicy =
2982
+ typeof options.remote.wait === "object"
2983
+ ? options.remote.wait
2984
+ : undefined;
2985
+ const waitBehavior: WaitBehavior = waitPolicy?.behavior ?? "keep-open";
2879
2986
  if (options.remote.wait) {
2880
- let t0 = +new Date();
2881
-
2882
2987
  waitForTime =
2883
2988
  typeof options.remote.wait === "boolean"
2884
2989
  ? DEFAULT_TIMEOUT
2885
2990
  : (options.remote.wait.timeout ?? DEFAULT_TIMEOUT);
2886
- let setDoneIfTimeout = false;
2887
- maybeSetDone = () => {
2888
- if (t0 + waitForTime! < +new Date()) {
2889
- cleanup();
2890
- done = true;
2891
- } else {
2892
- setDoneIfTimeout = true;
2893
- }
2894
- };
2895
- unsetDone = () => {
2896
- setDoneIfTimeout = false;
2897
- done = false;
2898
- };
2899
- let timeout = setTimeout(() => {
2900
- if (setDoneIfTimeout) {
2901
- cleanup();
2902
- done = true;
2903
- }
2904
- }, waitForTime);
2991
+ if (waitBehavior === "keep-open") {
2992
+ let t0 = +new Date();
2993
+ let setDoneIfTimeout = false;
2994
+ maybeSetDone = () => {
2995
+ if (t0 + waitForTime! < +new Date()) {
2996
+ cleanup();
2997
+ done = true;
2998
+ } else {
2999
+ setDoneIfTimeout = true;
3000
+ }
3001
+ };
3002
+ unsetDone = () => {
3003
+ setDoneIfTimeout = false;
3004
+ done = false;
3005
+ };
3006
+ let timeout = setTimeout(() => {
3007
+ if (setDoneIfTimeout) {
3008
+ cleanup();
3009
+ done = true;
3010
+ }
3011
+ }, waitForTime);
2905
3012
 
2906
- cleanup = () => {
2907
- this.clearResultsQueue(queryRequestCoerced);
2908
- clearTimeout(timeout);
2909
- };
3013
+ cleanup = () => {
3014
+ this.clearResultsQueue(queryRequestCoerced);
3015
+ clearTimeout(timeout);
3016
+ };
3017
+ }
2910
3018
  }
2911
3019
 
2912
3020
  if (options.remote.reach?.discover) {
@@ -2933,10 +3041,6 @@ export class DocumentIndex<
2933
3041
  options.remote.reach.eager = true; // include the results from the discovered peer even if it is not mature
2934
3042
  }
2935
3043
 
2936
- const waitPolicy =
2937
- typeof options.remote.wait === "object"
2938
- ? options.remote.wait
2939
- : undefined;
2940
3044
  if (
2941
3045
  waitPolicy?.behavior === "block" &&
2942
3046
  (waitPolicy.until ?? "any") === "any"
@@ -2977,14 +3081,14 @@ export class DocumentIndex<
2977
3081
  queryRequestCoerced.fetch = n;
2978
3082
  await this.queryCommence(
2979
3083
  queryRequestCoerced,
2980
- {
2981
- local: fetchOptions?.from != null ? false : options?.local,
2982
- remote:
2983
- options?.remote !== false && !skipRemoteDueToDiscovery
2984
- ? {
2985
- ...(typeof options?.remote === "object"
2986
- ? options.remote
2987
- : {}),
3084
+ {
3085
+ local: fetchOptions?.from != null ? false : options?.local,
3086
+ remote:
3087
+ options?.remote !== false && !skipRemoteDueToDiscovery
3088
+ ? {
3089
+ ...(typeof options?.remote === "object"
3090
+ ? options.remote
3091
+ : {}),
2988
3092
  from: fetchOptions?.from ?? initialRemoteTargets,
2989
3093
  }
2990
3094
  : false,
@@ -3533,32 +3637,27 @@ export class DocumentIndex<
3533
3637
  done = true;
3534
3638
  };
3535
3639
 
3536
- let close = async () => {
3537
- cleanupAndDone();
3538
-
3539
- // send close to remote
3540
- const closeRequest = new types.CloseIteratorRequest({
3541
- id: queryRequestCoerced.id,
3542
- });
3543
- const promises: Promise<any>[] = [];
3640
+ let close = async () => {
3641
+ cleanupAndDone();
3544
3642
 
3545
- for (const [peer, buffer] of peerBufferMap) {
3546
- if (buffer.kept === 0) {
3547
- peerBufferMap.delete(peer);
3548
- continue;
3549
- }
3550
- if (peer !== this.node.identity.publicKey.hashcode()) {
3551
- // Close remote
3552
- promises.push(
3643
+ // send close to remote (only peers that actually served results / had an active buffer)
3644
+ const closeRequest = new types.CloseIteratorRequest({
3645
+ id: queryRequestCoerced.id,
3646
+ });
3647
+ const selfHash = this.node.identity.publicKey.hashcode();
3648
+ const remotePeers = [...peerBufferMap.entries()]
3649
+ .filter(([peer, buffer]) => peer !== selfHash && buffer.kept > 0)
3650
+ .map(([peer]) => peer);
3651
+ peerBufferMap.clear();
3652
+ await Promise.allSettled(
3653
+ remotePeers.map((peer) =>
3553
3654
  this._query.send(closeRequest, {
3554
3655
  ...options,
3555
3656
  mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
3556
3657
  }),
3557
- );
3558
- }
3559
- }
3560
- await Promise.all(promises);
3561
- };
3658
+ ),
3659
+ );
3660
+ };
3562
3661
  options?.signal && options.signal.addEventListener("abort", close);
3563
3662
 
3564
3663
  let doneFn = () => {
@@ -4144,13 +4243,24 @@ export class DocumentIndex<
4144
4243
  };
4145
4244
  }
4146
4245
 
4147
- if (typeof options?.remote === "object" && options?.remote.wait) {
4246
+ const remoteConfig =
4247
+ options && typeof options.remote === "object" ? options.remote : undefined;
4248
+ const remoteWaitPolicy =
4249
+ remoteConfig && typeof remoteConfig.wait === "object"
4250
+ ? remoteConfig.wait
4251
+ : undefined;
4252
+ const remoteWaitBehavior: WaitBehavior =
4253
+ remoteWaitPolicy?.behavior ?? "keep-open";
4254
+ const keepRemoteWaitOpen =
4255
+ !!remoteConfig?.wait &&
4256
+ remoteWaitBehavior === "keep-open";
4257
+
4258
+ if (keepRemoteWaitOpen) {
4148
4259
  // was used to account for missed results when a peer joins; omitted in this minimal handler
4149
4260
 
4150
4261
  updateDeferred = pDefer<void>();
4151
4262
 
4152
- const waitForTime =
4153
- typeof options.remote.wait === "object" && options.remote.wait.timeout;
4263
+ const waitForTime = remoteWaitPolicy?.timeout;
4154
4264
 
4155
4265
  const prevMaybeSetDone = maybeSetDone;
4156
4266
  maybeSetDone = () => {
@@ -4167,7 +4277,7 @@ export class DocumentIndex<
4167
4277
  fetchedFirstForRemote = new Set<string>();
4168
4278
  joinListener = this.createReplicatorJoinListener({
4169
4279
  signal: ensureController().signal,
4170
- eager: options.remote.reach?.eager,
4280
+ eager: remoteConfig?.reach?.eager,
4171
4281
  onPeer: async (pk) => {
4172
4282
  if (done) return;
4173
4283
  const hash = pk.hashcode();
@@ -4240,8 +4350,7 @@ export class DocumentIndex<
4240
4350
  }
4241
4351
  };
4242
4352
  }
4243
- const remoteWaitActive =
4244
- typeof options?.remote === "object" && !!options.remote.wait;
4353
+ const remoteWaitActive = keepRemoteWaitOpen;
4245
4354
 
4246
4355
  const waitForUpdateAndResetDeferred = async () => {
4247
4356
  if (remoteWaitActive) {