@peerbit/document 15.0.32 → 15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/document",
3
- "version": "15.0.32",
3
+ "version": "15.1.0",
4
4
  "description": "Document store implementation",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -61,20 +61,20 @@
61
61
  "p-defer": "^4.0.0",
62
62
  "uint8arrays": "^5.1.0",
63
63
  "@peerbit/cache": "3.1.1",
64
- "@peerbit/crypto": "3.1.6",
65
- "@peerbit/indexer-interface": "3.0.13",
64
+ "@peerbit/document-interface": "3.2.77",
66
65
  "@peerbit/indexer-cache": "0.2.16",
67
- "@peerbit/document-interface": "3.2.76",
68
66
  "@peerbit/indexer-simple": "1.2.17",
67
+ "@peerbit/indexer-interface": "3.0.13",
69
68
  "@peerbit/indexer-sqlite3": "3.0.20",
70
- "@peerbit/log": "6.2.32",
69
+ "@peerbit/log": "6.2.33",
70
+ "@peerbit/crypto": "3.1.6",
71
71
  "@peerbit/logger": "2.0.2",
72
- "@peerbit/program": "6.0.59",
73
- "@peerbit/pubsub": "5.4.5",
74
- "@peerbit/rpc": "6.1.27",
75
- "@peerbit/shared-log": "16.0.29",
76
- "@peerbit/time": "3.0.1",
77
- "@peerbit/stream-interface": "6.0.16"
72
+ "@peerbit/program": "6.0.60",
73
+ "@peerbit/shared-log": "16.0.30",
74
+ "@peerbit/pubsub": "5.4.6",
75
+ "@peerbit/rpc": "6.1.28",
76
+ "@peerbit/stream-interface": "6.0.16",
77
+ "@peerbit/time": "3.0.1"
78
78
  },
79
79
  "optionalDependencies": {
80
80
  "@peerbit/document-rust": "0.1.1"
@@ -86,9 +86,9 @@
86
86
  "pidusage": "^4.0.1",
87
87
  "uuid": "^11.1.1",
88
88
  "@peerbit/log-rust": "1.1.4",
89
- "@peerbit/test-utils": "3.1.40",
90
- "@peerbit/native-backbone": "0.2.14",
91
- "peerbit": "5.4.1"
89
+ "@peerbit/native-backbone": "0.2.15",
90
+ "peerbit": "5.4.2",
91
+ "@peerbit/test-utils": "3.1.41"
92
92
  },
93
93
  "repository": {
94
94
  "type": "git",
@@ -0,0 +1,45 @@
1
+ export type DocumentBatchCommittedItem = Readonly<{
2
+ /** Position in the input array captured when putMany was invoked. */
3
+ index: number;
4
+ hash: string;
5
+ }>;
6
+
7
+ export type DocumentBatchLocalCommit =
8
+ | "not-started"
9
+ | "committed"
10
+ | "indeterminate";
11
+
12
+ /**
13
+ * Failure of a putMany invocation with batching: "required". Local append
14
+ * evidence does not prove remote receipt, document projection completion, or
15
+ * an atomic storage transaction. An indeterminate outcome requires recovery
16
+ * and must never be treated as an empty committed set that is safe to retry.
17
+ */
18
+ export class DocumentBatchCommitError extends Error {
19
+ readonly cause: unknown;
20
+ readonly localCommit: DocumentBatchLocalCommit;
21
+ readonly committedItems: readonly DocumentBatchCommittedItem[];
22
+ /** Safety of replaying this local append only; not application side effects. */
23
+ readonly retrySafe: boolean;
24
+ /** Unresolved local append outcome; false does not rule out projection repair. */
25
+ readonly recoveryRequired: boolean;
26
+
27
+ constructor(
28
+ cause: unknown,
29
+ localCommit: DocumentBatchLocalCommit,
30
+ committedItems: readonly DocumentBatchCommittedItem[],
31
+ ) {
32
+ super(
33
+ `Required document batch failed (${localCommit}): ${cause instanceof Error ? cause.message : String(cause)}`,
34
+ );
35
+ this.name = "DocumentBatchCommitError";
36
+ this.cause = cause;
37
+ this.localCommit = localCommit;
38
+ this.committedItems = Object.freeze(
39
+ committedItems.map(({ index, hash }) => Object.freeze({ index, hash })),
40
+ );
41
+ this.retrySafe = localCommit === "not-started";
42
+ this.recoveryRequired = localCommit === "indeterminate";
43
+ Object.freeze(this);
44
+ }
45
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "@peerbit/indexer-interface";
2
2
  export * from "@peerbit/document-interface";
3
3
  export * from "./program.js";
4
+ export * from "./batch-error.js";
4
5
  export type {
5
6
  CanRead,
6
7
  CanSearch,
package/src/program.ts CHANGED
@@ -30,12 +30,14 @@ import { logger as loggerFn } from "@peerbit/logger";
30
30
  import { Program, type ProgramEvents } from "@peerbit/program";
31
31
  import {
32
32
  type EntryReplicated,
33
+ NativeDurableCommitError,
33
34
  PersistedDeliveryError,
34
35
  type ReplicationDomain,
35
36
  type SharedAppendOptions,
36
37
  SharedLog,
37
38
  type SharedLogOptions,
38
39
  } from "@peerbit/shared-log";
40
+ import { DocumentBatchCommitError } from "./batch-error.js";
39
41
  import {
40
42
  detachCanPerformCallbackProperties,
41
43
  detachEntryForCallback,
@@ -240,6 +242,16 @@ type DocumentPutOptions = SharedAppendOptions<Operation> & {
240
242
  checkRemote?: boolean;
241
243
  };
242
244
 
245
+ export type DocumentPutManyOptions = DocumentPutOptions & {
246
+ /** Require a supported independent batch; never fall back to sequential puts. */
247
+ batching?: "required";
248
+ };
249
+
250
+ type RequiredDocumentBatch<T> = TrustedLocalCommitEvidence & {
251
+ prepared: PreparedPlainPut<T>[];
252
+ appendStarted: boolean;
253
+ };
254
+
243
255
  type TrustedLocalCommitEvidence = {
244
256
  committedHashes: Set<string>;
245
257
  };
@@ -363,6 +375,7 @@ interface DocumentBackend<T> {
363
375
  putMany(
364
376
  docs: T[],
365
377
  options?: DocumentPutOptions,
378
+ batch?: RequiredDocumentBatch<T>,
366
379
  ): MaybePromise<DocumentPutManyResult>;
367
380
  del(
368
381
  id: indexerTypes.Ideable | indexerTypes.IdKey,
@@ -439,6 +452,7 @@ type DocumentBackendPut<T> = (
439
452
  type DocumentBackendPutMany<T> = (
440
453
  docs: T[],
441
454
  options?: DocumentPutOptions,
455
+ batch?: RequiredDocumentBatch<T>,
442
456
  ) => MaybePromise<DocumentPutManyResult>;
443
457
  type DocumentBackendDelete = (
444
458
  id: indexerTypes.Ideable | indexerTypes.IdKey,
@@ -459,8 +473,9 @@ class CompatDocumentBackend<T> implements DocumentBackend<T> {
459
473
  putMany(
460
474
  docs: T[],
461
475
  options?: DocumentPutOptions,
476
+ batch?: RequiredDocumentBatch<T>,
462
477
  ): MaybePromise<DocumentPutManyResult> {
463
- return this.putManyImpl(docs, options);
478
+ return this.putManyImpl(docs, options, batch);
464
479
  }
465
480
 
466
481
  del(
@@ -589,6 +604,7 @@ class NativeDocumentBackend<T, I extends Record<string, any>>
589
604
  async putMany(
590
605
  docs: T[],
591
606
  options?: DocumentPutOptions,
607
+ batch?: RequiredDocumentBatch<T>,
592
608
  ): Promise<DocumentPutManyResult> {
593
609
  if (docs.length === 0) {
594
610
  return { entries: [], removed: [] };
@@ -597,8 +613,13 @@ class NativeDocumentBackend<T, I extends Record<string, any>>
597
613
  for (const doc of docs) {
598
614
  this.context.assertPlainPutSupported(doc, putOptions);
599
615
  }
600
- const prepared = docs.map((doc) => this.context.preparePlainPut(doc));
616
+ const prepared =
617
+ batch?.prepared ?? docs.map((doc) => this.context.preparePlainPut(doc));
601
618
  if (this.context.hasDuplicatePreparedPutKeys(prepared)) {
619
+ if (batch)
620
+ throw new Error(
621
+ "Required putMany batching requires distinct document keys",
622
+ );
602
623
  if (hasPersistedDelivery(options)) {
603
624
  throw this.context.nativeModeError(
604
625
  "requires distinct document keys for persisted putMany",
@@ -700,6 +721,7 @@ class NativeDocumentBackend<T, I extends Record<string, any>>
700
721
  resolveTrimmedEntries: this.context.shouldResolveTrimmedEntries(),
701
722
  options: putOptions,
702
723
  useNativeExistingDocumentContext,
724
+ batch,
703
725
  }),
704
726
  (documentAppendCommit) => {
705
727
  if (!documentAppendCommit) {
@@ -1113,6 +1135,7 @@ type NativeDocumentAppendManyCommitInput<T, I extends Record<string, any>> = {
1113
1135
  resolveTrimmedEntries: boolean;
1114
1136
  options?: DocumentPutOptions;
1115
1137
  useNativeExistingDocumentContext?: boolean;
1138
+ batch?: RequiredDocumentBatch<T>;
1116
1139
  };
1117
1140
 
1118
1141
  type DocumentAppendManyCommitFacts<T, I extends Record<string, any>> = {
@@ -3373,8 +3396,18 @@ export class Documents<
3373
3396
  };
3374
3397
  }
3375
3398
 
3376
- private preparePlainPut(doc: T): PreparedPlainPut<T> {
3377
- const keyValue = this.idResolver(doc);
3399
+ private preparePlainPut(doc: T, capture = false): PreparedPlainPut<T> {
3400
+ const resolvedKey = this.idResolver(doc);
3401
+ const keyValue =
3402
+ capture && ArrayBuffer.isView(resolvedKey)
3403
+ ? new Uint8Array(
3404
+ new Uint8Array(
3405
+ resolvedKey.buffer,
3406
+ resolvedKey.byteOffset,
3407
+ resolvedKey.byteLength,
3408
+ ),
3409
+ )
3410
+ : resolvedKey;
3378
3411
  indexerTypes.checkId(keyValue);
3379
3412
  const documentBytes = serialize(doc);
3380
3413
  if (documentBytes.length > MAX_BATCH_SIZE) {
@@ -3386,7 +3419,9 @@ export class Documents<
3386
3419
  }
3387
3420
  const operationPayloadBytes = encodePutOperationPayload(documentBytes);
3388
3421
  return {
3389
- document: doc,
3422
+ document: capture
3423
+ ? this._index.valueEncoding.decoder(documentBytes)
3424
+ : doc,
3390
3425
  encodedDocument: operationPayloadBytes.subarray(
3391
3426
  PUT_OPERATION_PREFIX_LENGTH,
3392
3427
  ),
@@ -3542,56 +3577,106 @@ export class Documents<
3542
3577
 
3543
3578
  public async putMany(
3544
3579
  docs: T[],
3545
- options?: DocumentPutOptions,
3580
+ options?: DocumentPutManyOptions,
3546
3581
  ): Promise<DocumentPutManyResult> {
3547
- options = asTrustedDocumentSharedLog(
3548
- this.log,
3549
- ).snapshotDocumentAppendOptions(
3550
- options,
3551
- this.isNativeMode() && docs.length > 0
3552
- ? (capturedOptions) => {
3553
- if (capturedOptions.encryption) {
3554
- this.assertNativeModePlainPutSupported(docs[0]!, capturedOptions);
3555
- }
3556
- }
3557
- : undefined,
3558
- );
3559
- const persistedRequested = hasPersistedDelivery(options);
3560
- const result = await this._documentBackend.putMany(docs, options);
3561
- if (!persistedRequested) {
3562
- return result;
3582
+ const batching = options?.batching;
3583
+ if (batching !== undefined && batching !== "required") {
3584
+ throw new Error('Unsupported putMany batching mode; expected "required"');
3563
3585
  }
3564
- const appendDelivery = persistedDocumentAppendDelivery.get(result);
3565
- if (appendDelivery) {
3566
- persistedDocumentAppendDelivery.delete(result);
3567
- await this.deliverPersistedDocumentAppendCommits(
3568
- appendDelivery,
3569
- options!,
3586
+ const batch: RequiredDocumentBatch<T> | undefined =
3587
+ batching === "required"
3588
+ ? { prepared: [], committedHashes: new Set(), appendStarted: false }
3589
+ : undefined;
3590
+ try {
3591
+ if (batch) docs = docs.slice();
3592
+ options = asTrustedDocumentSharedLog(
3593
+ this.log,
3594
+ ).snapshotDocumentAppendOptions(
3595
+ options,
3596
+ this.isNativeMode() && docs.length > 0
3597
+ ? (capturedOptions) => {
3598
+ if (capturedOptions.encryption) {
3599
+ this.assertNativeModePlainPutSupported(
3600
+ docs[0]!,
3601
+ capturedOptions,
3602
+ );
3603
+ }
3604
+ }
3605
+ : undefined,
3606
+ );
3607
+ if (batch) {
3608
+ if (Program.isPrototypeOf(this._clazz)) {
3609
+ throw new Error(
3610
+ "Required putMany batching does not support program-valued documents",
3611
+ );
3612
+ }
3613
+ // Capture every encoded value and key before the first asynchronous
3614
+ // operation, then reuse these bytes in the ordinary native batch path.
3615
+ batch.prepared = docs.map((doc) => this.preparePlainPut(doc, true));
3616
+ docs = batch.prepared.map(({ document }) => document);
3617
+ }
3618
+ const persistedRequested = hasPersistedDelivery(options);
3619
+ const result = await this._documentBackend.putMany(docs, options, batch);
3620
+ if (!persistedRequested) {
3621
+ return result;
3622
+ }
3623
+ const appendDelivery = persistedDocumentAppendDelivery.get(result);
3624
+ if (appendDelivery) {
3625
+ persistedDocumentAppendDelivery.delete(result);
3626
+ await this.deliverPersistedDocumentAppendCommits(
3627
+ appendDelivery,
3628
+ options!,
3629
+ );
3630
+ let entries: Entry<Operation>[] | undefined;
3631
+ return {
3632
+ get entries() {
3633
+ return (entries ??= result.entries);
3634
+ },
3635
+ removed: result.removed,
3636
+ };
3637
+ }
3638
+ const entries = result.entries;
3639
+ if (entries.length === 0) {
3640
+ return result;
3641
+ }
3642
+ await this.deliverPersistedDocumentEntries(entries, options!);
3643
+ return { entries, removed: result.removed };
3644
+ } catch (error) {
3645
+ if (!batch) throw error;
3646
+ const hashes = [...batch.committedHashes];
3647
+ // The trusted independent-batch seam emits all hashes in input order.
3648
+ // Never invent indexes from incomplete or contradictory batch evidence.
3649
+ const complete =
3650
+ hashes.length === batch.prepared.length && hashes.length > 0;
3651
+ const cause =
3652
+ error instanceof PersistedDeliveryError ? error.cause : error;
3653
+ const localCommit =
3654
+ complete && !(cause instanceof NativeDurableCommitError)
3655
+ ? "committed"
3656
+ : batch.appendStarted || hashes.length > 0
3657
+ ? "indeterminate"
3658
+ : "not-started";
3659
+ throw new DocumentBatchCommitError(
3660
+ error,
3661
+ localCommit,
3662
+ complete ? hashes.map((hash, index) => ({ index, hash })) : [],
3570
3663
  );
3571
- let entries: Entry<Operation>[] | undefined;
3572
- return {
3573
- get entries() {
3574
- return (entries ??= result.entries);
3575
- },
3576
- removed: result.removed,
3577
- };
3578
- }
3579
- const entries = result.entries;
3580
- if (entries.length === 0) {
3581
- return result;
3582
3664
  }
3583
- await this.deliverPersistedDocumentEntries(entries, options!);
3584
- return { entries, removed: result.removed };
3585
3665
  }
3586
3666
 
3587
3667
  private async putManyCompatDocumentBackend(
3588
3668
  docs: T[],
3589
3669
  options?: DocumentPutOptions,
3670
+ batch?: RequiredDocumentBatch<T>,
3590
3671
  ): Promise<DocumentPutManyResult> {
3591
3672
  if (docs.length === 0) {
3592
3673
  return { entries: [], removed: [] };
3593
3674
  }
3594
3675
  if (!this.canUsePlainPutManyFastPath(docs, options)) {
3676
+ if (batch)
3677
+ throw new Error(
3678
+ "Required putMany batching requires the independent batched document path",
3679
+ );
3595
3680
  if (hasPersistedDelivery(options)) {
3596
3681
  throw new Error(
3597
3682
  "persisted putMany requires the independent batched document path",
@@ -3600,8 +3685,13 @@ export class Documents<
3600
3685
  return this.putManySequential(docs, options);
3601
3686
  }
3602
3687
 
3603
- const prepared = docs.map((doc) => this.preparePlainPut(doc));
3688
+ const prepared =
3689
+ batch?.prepared ?? docs.map((doc) => this.preparePlainPut(doc));
3604
3690
  if (this.hasDuplicatePreparedPutKeys(prepared)) {
3691
+ if (batch)
3692
+ throw new Error(
3693
+ "Required putMany batching requires distinct document keys",
3694
+ );
3605
3695
  if (hasPersistedDelivery(options)) {
3606
3696
  throw new Error("persisted putMany requires distinct document keys");
3607
3697
  }
@@ -3619,8 +3709,13 @@ export class Documents<
3619
3709
  })),
3620
3710
  resolveTrimmedEntries: !this._index.canGetIdentityIndexedByHead(),
3621
3711
  options,
3712
+ batch,
3622
3713
  });
3623
3714
  if (!documentAppendCommit) {
3715
+ if (batch)
3716
+ throw new Error(
3717
+ "Required putMany batching requires native batched payload append support",
3718
+ );
3624
3719
  if (hasPersistedDelivery(options)) {
3625
3720
  throw new Error(
3626
3721
  "persisted putMany requires native batched payload append support",
@@ -4132,6 +4227,12 @@ export class Documents<
4132
4227
  private async commitNativeDocumentAppendMany(
4133
4228
  input: NativeDocumentAppendManyCommitInput<T, I>,
4134
4229
  ): Promise<DocumentAppendManyCommitFacts<T, I> | undefined> {
4230
+ if (input.batch) {
4231
+ return this.commitNativeDocumentAppendManyWithEvidence(
4232
+ input,
4233
+ input.batch,
4234
+ );
4235
+ }
4135
4236
  if (!hasPersistedDelivery(input.options)) {
4136
4237
  return this.commitNativeDocumentAppendManyWithEvidence(input, undefined);
4137
4238
  }
@@ -4180,10 +4281,13 @@ export class Documents<
4180
4281
  }
4181
4282
  return [next];
4182
4283
  });
4284
+ const payloads = input.puts.map((put) => put.operationPayloadBytes);
4285
+ const appendOptions = withoutPersistedDelivery(input.options);
4286
+ if (input.batch) input.batch.appendStarted = true;
4183
4287
  const appended =
4184
4288
  await trustedLog.appendLocallyPreparedPayloadsManyIndependent(
4185
- input.puts.map((put) => put.operationPayloadBytes),
4186
- withoutPersistedDelivery(input.options),
4289
+ payloads,
4290
+ appendOptions,
4187
4291
  {
4188
4292
  resolveTrimmedEntries: input.resolveTrimmedEntries,
4189
4293
  nexts,
@@ -4193,6 +4297,8 @@ export class Documents<
4193
4297
  },
4194
4298
  );
4195
4299
  if (!appended) {
4300
+ // An unsupported lower append can already have prepared native state.
4301
+ // Absence of success evidence cannot prove that replay is safe.
4196
4302
  if (this.isNativeMode()) {
4197
4303
  throw this.nativeModeError(
4198
4304
  "requires native batched payload append support",