@peerbit/document 13.0.8 → 13.0.9

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/program.ts CHANGED
@@ -43,9 +43,12 @@ import {
43
43
  type CanSearch,
44
44
  DocumentIndex,
45
45
  type GetOptions,
46
+ type IndexedContextOnly,
47
+ INDEX_CONTEXT_SHAPE,
46
48
  type PrefetchOptions,
47
49
  type ReachScope,
48
50
  type TransformOptions,
51
+ type WithContext,
49
52
  type WithIndexedContext,
50
53
  coerceWithContext,
51
54
  coerceWithIndexed,
@@ -176,6 +179,33 @@ export class Documents<
176
179
  return this._index;
177
180
  }
178
181
 
182
+ private getLocalIndexedContext(
183
+ key: indexerTypes.IdKey,
184
+ ): Promise<indexerTypes.IndexedResult<IndexedContextOnly<I>> | undefined> {
185
+ return this._index.index.get(key, {
186
+ shape: INDEX_CONTEXT_SHAPE,
187
+ }) as Promise<
188
+ indexerTypes.IndexedResult<IndexedContextOnly<I>> | undefined
189
+ >;
190
+ }
191
+
192
+ private getExistingContext(
193
+ existing:
194
+ | ResultIndexedValue<WithContext<I>>
195
+ | indexerTypes.IndexedResult<WithContext<I>>
196
+ | indexerTypes.IndexedResult<IndexedContextOnly<I>>
197
+ | null
198
+ | undefined,
199
+ ): indexerTypes.ReturnTypeFromShape<
200
+ WithContext<I>,
201
+ typeof INDEX_CONTEXT_SHAPE
202
+ >["__context"]
203
+ | undefined {
204
+ return existing instanceof ResultIndexedValue
205
+ ? existing.context
206
+ : existing?.value.__context;
207
+ }
208
+
179
209
  get changes() {
180
210
  return this.events;
181
211
  }
@@ -453,20 +483,23 @@ export class Documents<
453
483
 
454
484
  const key = indexerTypes.toId(keyValue);
455
485
 
456
- const existingDocument = (
457
- await this.index.getDetailed(key, {
458
- resolve: false,
459
- local: true,
460
- remote: this.immutable ? { strategy: "fallback" } : false,
461
- })
462
- )?.[0]?.results[0];
463
- if (existingDocument && existingDocument.context.head !== entry.hash) {
486
+ const existingDocument = this.immutable
487
+ ? (
488
+ await this.index.getDetailed(key, {
489
+ resolve: false,
490
+ local: true,
491
+ remote: { strategy: "fallback" },
492
+ })
493
+ )?.[0]?.results[0]
494
+ : await this.getLocalIndexedContext(key);
495
+ const existingContext = this.getExistingContext(existingDocument);
496
+ if (existingContext && existingContext.head !== entry.hash) {
464
497
  // econd condition can false if we reset the operation log, while not resetting the index. For example when doing .recover
465
498
  if (this.immutable) {
466
499
  // key already exist but pick the oldest entry
467
500
  // this is because we can not overwrite same id if immutable
468
501
  if (
469
- existingDocument.context.created <
502
+ existingContext.created <
470
503
  entry.meta.clock.timestamp.wallTime
471
504
  ) {
472
505
  return false;
@@ -485,7 +518,7 @@ export class Documents<
485
518
  }
486
519
 
487
520
  const prevEntry = await this.log.log.entryIndex.get(
488
- existingDocument.context.head,
521
+ existingContext.head,
489
522
  );
490
523
  if (!prevEntry) {
491
524
  logger.error(
@@ -510,19 +543,26 @@ export class Documents<
510
543
  if (entry.meta.next.length !== 1) {
511
544
  return false;
512
545
  }
513
- const existingDocument = (
514
- await this.index.getDetailed(operation.key, {
515
- resolve: false,
516
- local: true,
517
- remote: this.immutable,
518
- })
519
- )?.[0]?.results[0];
520
-
521
- if (!existingDocument) {
546
+ const existingDocument = this.immutable
547
+ ? (
548
+ await this.index.getDetailed(operation.key, {
549
+ resolve: false,
550
+ local: true,
551
+ remote: true,
552
+ })
553
+ )?.[0]?.results[0]
554
+ : await this.getLocalIndexedContext(
555
+ operation.key instanceof indexerTypes.IdKey
556
+ ? operation.key
557
+ : indexerTypes.toId(operation.key),
558
+ );
559
+ const existingHead = this.getExistingContext(existingDocument)?.head;
560
+
561
+ if (!existingHead) {
522
562
  // already deleted
523
563
  return coerceDeleteOperation(operation); // assume ok
524
564
  }
525
- let doc = await this.log.log.get(existingDocument.context.head);
565
+ let doc = await this.log.log.get(existingHead);
526
566
  if (!doc) {
527
567
  logger.error("Failed to find Document from head");
528
568
  return false;
@@ -560,7 +600,6 @@ export class Documents<
560
600
 
561
601
  // type check the key
562
602
  indexerTypes.checkId(keyValue);
563
-
564
603
  const ser = serialize(doc);
565
604
  if (ser.length > MAX_BATCH_SIZE) {
566
605
  throw new Error(
@@ -570,17 +609,18 @@ export class Documents<
570
609
  );
571
610
  }
572
611
 
573
- const existingDocument = options?.unique
612
+ const existingHead = options?.unique
574
613
  ? undefined
575
- : (
576
- await this._index.getDetailed(keyValue, {
577
- resolve: false,
578
- local: true,
579
- remote: options?.checkRemote
580
- ? { replicate: options?.replicate }
581
- : false, // only query remote if we know they exist
582
- })
583
- )?.[0]?.results[0];
614
+ : options?.checkRemote
615
+ ? (
616
+ await this._index.getDetailed(keyValue, {
617
+ resolve: false,
618
+ local: true,
619
+ remote: { replicate: options?.replicate },
620
+ })
621
+ )?.[0]?.results[0]?.context.head
622
+ : (await this.getLocalIndexedContext(indexerTypes.toId(keyValue)))
623
+ ?.value.__context.head;
584
624
 
585
625
  let operation: PutOperation | PutWithKeyOperation;
586
626
  if (this.compatibility === 6) {
@@ -601,9 +641,7 @@ export class Documents<
601
641
  const appended = await this.log.append(operation, {
602
642
  ...options,
603
643
  meta: {
604
- next: existingDocument
605
- ? [await this._resolveEntry(existingDocument.context.head)]
606
- : [],
644
+ next: existingHead ? [await this._resolveEntry(existingHead)] : [],
607
645
  ...options?.meta,
608
646
  },
609
647
  canAppend: (entry) => {
@@ -732,7 +770,7 @@ export class Documents<
732
770
  // if no casual ordering is used, use timestamps to order docs
733
771
  let existing = reference?.unique
734
772
  ? null
735
- : (await this._index.index.get(key)) || null;
773
+ : (await this.getLocalIndexedContext(key)) || null;
736
774
  if (!this.strictHistory && existing) {
737
775
  // if immutable use oldest, else use newest
738
776
  let shouldIgnoreChange = this.immutable
package/src/search.ts CHANGED
@@ -197,6 +197,7 @@ export type RemoteQueryOptions<Q, R, D> = RPCRequestAllOptions<Q, R> & {
197
197
  replicate?: boolean;
198
198
  minAge?: number;
199
199
  throwOnMissing?: boolean;
200
+ retryMissingResponses?: boolean;
200
201
  strategy?: "fallback";
201
202
  domain?:
202
203
  | {
@@ -404,6 +405,7 @@ const introduceEntries = async <
404
405
  RPCResponse<types.Results<types.ResultTypeFromRequest<R, T, I>>>[]
405
406
  > => {
406
407
  const results: RPCResponse<types.Results<any>>[] = [];
408
+ const replicatedHeads = new Set<string>();
407
409
  for (const response of responses) {
408
410
  if (!response.from) {
409
411
  logger.error("Missing from for response");
@@ -416,7 +418,27 @@ const introduceEntries = async <
416
418
  : r.init(indexedType),
417
419
  );
418
420
  if (typeof options?.remote !== "boolean" && options?.remote?.replicate) {
419
- await sync(queryRequest, response.response);
421
+ const uniqueResults = response.response.results.filter((result) => {
422
+ const head =
423
+ result instanceof types.ResultIndexedValue &&
424
+ result.entries.length > 0
425
+ ? result.entries[0]!.hash
426
+ : result.context.head;
427
+ if (replicatedHeads.has(head)) {
428
+ return false;
429
+ }
430
+ replicatedHeads.add(head);
431
+ return true;
432
+ });
433
+ if (uniqueResults.length > 0) {
434
+ await sync(
435
+ queryRequest,
436
+ new types.Results({
437
+ results: uniqueResults,
438
+ kept: response.response.kept,
439
+ }),
440
+ );
441
+ }
420
442
  }
421
443
  options?.onResponse &&
422
444
  (await options.onResponse(response.response, response.from!)); // TODO fix types
@@ -524,6 +546,19 @@ export type WithContext<I> = {
524
546
  __context: types.Context;
525
547
  } & I;
526
548
 
549
+ export const INDEX_CONTEXT_SHAPE = {
550
+ __context: {
551
+ created: true,
552
+ modified: true,
553
+ head: true,
554
+ gid: true,
555
+ size: true,
556
+ },
557
+ } as const;
558
+
559
+ export type IndexedContextOnly<I extends Record<string, any>> =
560
+ indexerTypes.ReturnTypeFromShape<WithContext<I>, typeof INDEX_CONTEXT_SHAPE>;
561
+
527
562
  export type WithIndexed<T, I> = {
528
563
  // experimental, used to quickly get the indexed representation
529
564
  __indexed: I;
@@ -1534,10 +1569,18 @@ export class DocumentIndex<
1534
1569
  value: T,
1535
1570
  id: indexerTypes.IdKey,
1536
1571
  entry: Entry<Operation>,
1537
- existing: indexerTypes.IndexedResult<WithContext<I>> | null | undefined,
1572
+ existing:
1573
+ | indexerTypes.IndexedResult<WithContext<I>>
1574
+ | indexerTypes.IndexedResult<IndexedContextOnly<I>>
1575
+ | null
1576
+ | undefined,
1538
1577
  ): Promise<{ context: types.Context; indexable: I }> {
1539
1578
  const existingDefined =
1540
- existing === undefined ? await this.index.get(id) : existing;
1579
+ existing === undefined
1580
+ ? await this.index.get(id, {
1581
+ shape: INDEX_CONTEXT_SHAPE,
1582
+ })
1583
+ : existing;
1541
1584
  const context = new types.Context({
1542
1585
  created:
1543
1586
  existingDefined?.value.__context.created ||
@@ -1547,13 +1590,16 @@ export class DocumentIndex<
1547
1590
  gid: entry.meta.gid,
1548
1591
  size: entry.payload.byteLength,
1549
1592
  });
1550
- return this.putWithContext(value, id, context);
1593
+ return this.putWithContext(value, id, context, {
1594
+ replace: existingDefined != null,
1595
+ });
1551
1596
  }
1552
1597
 
1553
1598
  public async putWithContext(
1554
1599
  value: T,
1555
1600
  id: indexerTypes.IdKey,
1556
1601
  context: types.Context,
1602
+ options?: { replace?: boolean },
1557
1603
  ): Promise<{ context: types.Context; indexable: I }> {
1558
1604
  const idString = id.primitive;
1559
1605
  if (
@@ -1582,7 +1628,7 @@ export class DocumentIndex<
1582
1628
 
1583
1629
  coerceWithContext(value, context);
1584
1630
 
1585
- await this.index.put(wrappedValueToIndex);
1631
+ await this.index.put(wrappedValueToIndex, undefined, options);
1586
1632
  return { context, indexable: valueToIndex };
1587
1633
  }
1588
1634
 
@@ -2580,17 +2626,29 @@ export class DocumentIndex<
2580
2626
  options?: O,
2581
2627
  ): Promise<ValueTypeFromRequest<Resolve, T, I>[]> {
2582
2628
  // Set fetch to search size, or max value (default to max u32 (4294967295))
2583
- const coercedRequest = coerceQuery(
2584
- queryRequest,
2585
- options,
2586
- this.compatibility,
2587
- );
2588
- coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2629
+ const coercedRequest = coerceQuery(
2630
+ queryRequest,
2631
+ options,
2632
+ this.compatibility,
2633
+ );
2634
+ coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2635
+
2636
+ const searchOptions =
2637
+ typeof options?.remote === "object" &&
2638
+ options.remote.retryMissingResponses === undefined
2639
+ ? ({
2640
+ ...options,
2641
+ remote: {
2642
+ ...options.remote,
2643
+ retryMissingResponses: false,
2644
+ },
2645
+ } as O)
2646
+ : options;
2589
2647
 
2590
- // Use an iterator so large results respect message size limits.
2591
- const iterator = this.iterate<Resolve>(coercedRequest, options);
2648
+ // Use an iterator so large results respect message size limits.
2649
+ const iterator = this.iterate<Resolve>(coercedRequest, searchOptions);
2592
2650
 
2593
- const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2651
+ const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2594
2652
 
2595
2653
  while (
2596
2654
  iterator.done() !== true &&
@@ -2870,6 +2928,10 @@ export class DocumentIndex<
2870
2928
  ) {
2871
2929
  queryRequestCoerced.replicate = true;
2872
2930
  }
2931
+ const retryMissingResponseGroups =
2932
+ typeof options?.remote === "object"
2933
+ ? options.remote.retryMissingResponses ?? true
2934
+ : true;
2873
2935
 
2874
2936
  indexIteratorLogger.trace("Iterate with options", {
2875
2937
  query: queryRequestCoerced,
@@ -3216,6 +3278,9 @@ export class DocumentIndex<
3216
3278
  },
3217
3279
  onMissingResponses: (error) => {
3218
3280
  missingResponses = true;
3281
+ if (!retryMissingResponseGroups) {
3282
+ return;
3283
+ }
3219
3284
  const missingGroups = (error as MissingResponsesError & {
3220
3285
  missingGroups?: string[][];
3221
3286
  }).missingGroups;
@@ -3242,7 +3307,7 @@ export class DocumentIndex<
3242
3307
  fetchOptions?.fetchedFirstForRemote,
3243
3308
  );
3244
3309
 
3245
- if (missingResponses) {
3310
+ if (missingResponses && retryMissingResponseGroups) {
3246
3311
  hasMore = true;
3247
3312
  unsetDone();
3248
3313
  }