@peerbit/shared-log 16.0.17 → 16.0.18

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/index.ts CHANGED
@@ -688,8 +688,13 @@ type PreparedLocalAppendCommit<R extends "u32" | "u64"> = {
688
688
  next: string[];
689
689
  wallTime: bigint;
690
690
  logical: number;
691
+ clockId?: Uint8Array;
692
+ type?: EntryType;
693
+ metaData?: Uint8Array;
691
694
  payloadSize: number;
695
+ entrySize?: number;
692
696
  metaBytes?: Uint8Array;
697
+ storageBytes?: Uint8Array;
693
698
  hashNumber?: NumberFromType<R>;
694
699
  coordinateFields?: SharedLogCoordinateNativeFields<R>;
695
700
  nativeBackboneDocumentIndexCommitted?: boolean;
@@ -708,6 +713,12 @@ type PersistedDeliveryPlanningEntry<T, R extends "u32" | "u64"> =
708
713
  | ShallowOrFullEntry<T>
709
714
  | EntryReplicated<R>;
710
715
 
716
+ type PersistedDeliveryPlanningRecord<T, R extends "u32" | "u64"> = Readonly<{
717
+ canonicalHash: string;
718
+ createDefaultPlanningSource: () => PersistedDeliveryPlanningEntry<T, R>;
719
+ createFullPlanningSource?: () => Entry<T>;
720
+ }>;
721
+
711
722
  type NativeBackboneSimpleDocumentProjectionPlan = {
712
723
  documentVariantType?: "u8" | "string";
713
724
  documentVariantValue?: string;
@@ -1733,6 +1744,20 @@ export type DeliveryOptions = {
1733
1744
  signal?: AbortSignal;
1734
1745
  };
1735
1746
 
1747
+ type PersistedDeliveryOptions = Readonly<{
1748
+ reliability: "persisted";
1749
+ minAcks: number;
1750
+ requireRecipients?: boolean;
1751
+ priority?: number;
1752
+ timeout?: number;
1753
+ signal?: AbortSignal;
1754
+ }>;
1755
+
1756
+ type PersistedAppendInvocation<T> = {
1757
+ options: SharedAppendOptions<T>;
1758
+ delivery: PersistedDeliveryOptions;
1759
+ };
1760
+
1736
1761
  type CrashSafeStorageBarrier = {
1737
1762
  readonly crashSafe: true;
1738
1763
  barrier(): MaybePromise<void>;
@@ -1859,7 +1884,10 @@ type SharedAppendBaseOptions<T> = AppendOptions<T> & {
1859
1884
 
1860
1885
  type TrustedLogAppendOptions<T> = AppendOptions<T> & {
1861
1886
  __peerbitCanAppendAlreadyValidated?: boolean;
1862
- __peerbitOnLocalCommit?: (hashes: readonly string[]) => void;
1887
+ __peerbitOnLocalCommit?: (
1888
+ hashes: readonly string[],
1889
+ entries?: readonly Entry<T>[],
1890
+ ) => void;
1863
1891
  };
1864
1892
 
1865
1893
  const attachTrustedLocalCommitEvidence = <T>(
@@ -1886,6 +1914,11 @@ export type SharedAppendOptions<T> =
1886
1914
  delivery?: false | undefined;
1887
1915
  });
1888
1916
 
1917
+ type DocumentSharedAppendOptions<T> = SharedAppendOptions<T> & {
1918
+ unique?: boolean;
1919
+ checkRemote?: boolean;
1920
+ };
1921
+
1889
1922
  type TrustedLowerLogAppendHashesSink = (
1890
1923
  hashes: string[],
1891
1924
  ) => void | Promise<void>;
@@ -2006,6 +2039,9 @@ type TrustedLowerLogNativeCommitFinalizer = {
2006
2039
  };
2007
2040
 
2008
2041
  type TrustedLowerLog<T> = {
2042
+ snapshotAppendEncryptionForTrustedCaller(
2043
+ encryption: NonNullable<AppendOptions<T>["encryption"]>,
2044
+ ): NonNullable<AppendOptions<T>["encryption"]>;
2009
2045
  appendLocallyPrepared(
2010
2046
  data: T,
2011
2047
  options?: TrustedLogAppendOptions<T>,
@@ -4492,40 +4528,369 @@ export class SharedLog<
4492
4528
  return true;
4493
4529
  }
4494
4530
 
4495
- private getPersistedDeliveryOptions(
4496
- options?: SharedAppendOptions<T>,
4497
- ): DeliveryOptions | undefined {
4498
- const delivery = options?.delivery;
4499
- if (typeof delivery !== "object" || delivery.reliability !== "persisted") {
4500
- return undefined;
4531
+ private snapshotDeliveryOptions(
4532
+ deliveryArgument: DeliveryOptions,
4533
+ reliability: DeliveryReliability | undefined,
4534
+ ): Readonly<DeliveryOptions> {
4535
+ // Keep the selected AbortSignal object live: aborting it remains effective,
4536
+ // while reassigning any caller field cannot change this invocation.
4537
+ return Object.freeze({
4538
+ reliability,
4539
+ minAcks: deliveryArgument.minAcks,
4540
+ requireRecipients: deliveryArgument.requireRecipients,
4541
+ priority: deliveryArgument.priority,
4542
+ timeout: deliveryArgument.timeout,
4543
+ signal: deliveryArgument.signal,
4544
+ });
4545
+ }
4546
+
4547
+ private snapshotAppendTrim(
4548
+ trim: NonNullable<AppendOptions<T>["trim"]>,
4549
+ ): NonNullable<AppendOptions<T>["trim"]> {
4550
+ const type = trim.type;
4551
+ const filterArgument = trim.filter;
4552
+ const filter = filterArgument
4553
+ ? Object.freeze({
4554
+ canTrim: filterArgument.canTrim,
4555
+ cacheId: filterArgument.cacheId,
4556
+ })
4557
+ : undefined;
4558
+ if (type === "time") {
4559
+ return Object.freeze({ type, maxAge: trim.maxAge, filter });
4560
+ }
4561
+ if (type === "length" || type === "bytelength") {
4562
+ return Object.freeze({
4563
+ type,
4564
+ to: trim.to,
4565
+ from: trim.from,
4566
+ filter,
4567
+ });
4568
+ }
4569
+ throw new Error("Unsupported append trim type");
4570
+ }
4571
+
4572
+ private snapshotAppendEncryption(
4573
+ encryption: NonNullable<AppendOptions<T>["encryption"]>,
4574
+ ): NonNullable<AppendOptions<T>["encryption"]> {
4575
+ return asTrustedLowerLog(this.log).snapshotAppendEncryptionForTrustedCaller(
4576
+ encryption,
4577
+ );
4578
+ }
4579
+
4580
+ private captureFullAppendNextStorageReader(
4581
+ next: Entry<any> | ShallowEntry,
4582
+ ): (() => Uint8Array) | undefined {
4583
+ if (next instanceof Entry) {
4584
+ const getStorageBytes = next.getStorageBytes;
4585
+ return () => Reflect.apply(getStorageBytes, next, []);
4586
+ }
4587
+ const candidate = next as Partial<Entry<any>>;
4588
+ const init = candidate.init;
4589
+ const getNext = candidate.getNext;
4590
+ const getClock = candidate.getClock;
4591
+ const getStorageBytes = candidate.getStorageBytes;
4592
+ const verifySignatures = candidate.verifySignatures;
4593
+ if (
4594
+ typeof init !== "function" ||
4595
+ typeof getNext !== "function" ||
4596
+ typeof getClock !== "function" ||
4597
+ typeof getStorageBytes !== "function" ||
4598
+ typeof verifySignatures !== "function"
4599
+ ) {
4600
+ return;
4601
+ }
4602
+ return () => Reflect.apply(getStorageBytes, next, []);
4603
+ }
4604
+
4605
+ private snapshotFullAppendNext(
4606
+ next: Entry<any>,
4607
+ getStorageBytes: () => Uint8Array,
4608
+ canonicalHash: string,
4609
+ ): Entry<any> | undefined {
4610
+ let entrySize: number | undefined;
4611
+ try {
4612
+ entrySize = next.size;
4613
+ } catch {
4614
+ // A valid but not-yet-sized entry still has exact owned storage bytes below.
4501
4615
  }
4502
- const parsed = this._parseDeliveryOptions(options?.delivery);
4503
- const target = (options as { target?: string } | undefined)?.target;
4616
+ const createdLocally = next.createdLocally;
4617
+ let sourceBytes = Entry.getPreparedStorageBytes(next);
4618
+ if (!sourceBytes) {
4619
+ try {
4620
+ sourceBytes = getStorageBytes();
4621
+ } catch {
4622
+ // Native commit-only entries intentionally keep payload/signature bytes
4623
+ // outside the JS Entry. They are valid sortable parents but cannot be
4624
+ // imported into another log as full blocks, so snapshot them below as
4625
+ // shallow canonical references instead.
4626
+ return;
4627
+ }
4628
+ }
4629
+ const bytes = Uint8Array.from(sourceBytes);
4630
+ const captured = deserialize(bytes, Entry) as Entry<any>;
4631
+ const decodedHash = captured.hash;
4632
+ if (decodedHash && decodedHash !== canonicalHash) {
4633
+ throw new Error(
4634
+ `Explicit append next bytes did not match captured hash ${canonicalHash}`,
4635
+ );
4636
+ }
4637
+ captured.hash = "";
4638
+ Entry.prepareMultihashBytes(captured, bytes, canonicalHash);
4639
+ captured.hash = canonicalHash;
4640
+ captured.size = entrySize ?? bytes.byteLength;
4641
+ captured.createdLocally = createdLocally;
4642
+ captured.init({
4643
+ encoding: this.log.encoding,
4644
+ keychain: this.log.keychain,
4645
+ });
4646
+ return captured;
4647
+ }
4648
+
4649
+ private snapshotAppendNext(
4650
+ next: Entry<any> | ShallowEntry,
4651
+ ): Entry<any> | ShallowEntry {
4652
+ const hash = next.hash;
4653
+ if (!hash) {
4654
+ throw new Error("Explicit append next requires a canonical hash");
4655
+ }
4656
+ const getStorageBytes = this.captureFullAppendNextStorageReader(next);
4657
+ if (getStorageBytes) {
4658
+ const captured = this.snapshotFullAppendNext(
4659
+ next as Entry<any>,
4660
+ getStorageBytes,
4661
+ hash,
4662
+ );
4663
+ if (captured) return captured;
4664
+ }
4665
+ const meta = next.meta;
4666
+ const gid = meta.gid;
4667
+ const nextHashes = [...meta.next];
4668
+ const type = meta.type;
4669
+ const sourceData = meta.data;
4670
+ const data = sourceData && Uint8Array.from(sourceData);
4671
+ const clock = meta.clock;
4672
+ const clockId = Uint8Array.from(clock.id);
4673
+ const sourceTimestamp = clock.timestamp;
4674
+ const timestamp = new Timestamp({
4675
+ wallTime: sourceTimestamp.wallTime,
4676
+ logical: sourceTimestamp.logical,
4677
+ });
4678
+ const candidate = next as ShallowEntry & { payloadByteLength?: number };
4679
+ let payloadSize = 0;
4680
+ try {
4681
+ const shallowPayloadSize = candidate.payloadSize;
4682
+ if (typeof shallowPayloadSize === "number") {
4683
+ payloadSize = shallowPayloadSize;
4684
+ } else {
4685
+ const fullPayloadSize = candidate.payloadByteLength;
4686
+ if (typeof fullPayloadSize === "number") payloadSize = fullPayloadSize;
4687
+ }
4688
+ } catch {
4689
+ // Hollow native parents need only their canonical sorting/link facts.
4690
+ }
4691
+ const candidateHead = candidate.head;
4692
+ const head = typeof candidateHead === "boolean" ? candidateHead : true;
4693
+ const captured = new ShallowEntry({
4694
+ hash,
4695
+ payloadSize,
4696
+ head,
4697
+ meta: new ShallowMeta({
4698
+ gid,
4699
+ next: nextHashes,
4700
+ type,
4701
+ data,
4702
+ clock: new LamportClock({ id: clockId, timestamp }),
4703
+ }),
4704
+ });
4705
+ Object.freeze(captured.meta.next);
4706
+ Object.freeze(captured.meta.clock.timestamp);
4707
+ Object.freeze(captured.meta.clock);
4708
+ Object.freeze(captured.meta);
4709
+ return Object.freeze(captured);
4710
+ }
4711
+
4712
+ private snapshotSupportedAppendOptions(
4713
+ options: DocumentSharedAppendOptions<T>,
4714
+ delivery: SharedAppendOptions<T>["delivery"],
4715
+ includeDocumentShape: boolean,
4716
+ validateCapturedOptions?: (options: DocumentSharedAppendOptions<T>) => void,
4717
+ ): DocumentSharedAppendOptions<T> {
4718
+ // Copy the finite public option surface, not arbitrary enumerable caller
4719
+ // properties. This pins every value that the async append/document paths
4720
+ // reread while avoiding surprising evaluation of unrelated custom getters.
4721
+ const captured = Object.create(null) as DocumentSharedAppendOptions<T>;
4722
+ const durability = options.durability;
4723
+ const deferIndexWrite = options.deferIndexWrite;
4724
+ const meta = options.meta;
4725
+ const identity = options.identity;
4726
+ const signers = options.signers;
4727
+ const trim = options.trim;
4728
+ const encryption = options.encryption;
4729
+ const onChange = options.onChange;
4730
+ const canAppend = options.canAppend;
4731
+ const replicas = options.replicas;
4732
+ const replicate = options.replicate;
4733
+ const target = options.target;
4734
+ const unique = includeDocumentShape ? options.unique : undefined;
4735
+ const checkRemote = includeDocumentShape ? options.checkRemote : undefined;
4736
+
4737
+ if (durability !== undefined) captured.durability = durability;
4738
+ if (deferIndexWrite !== undefined) {
4739
+ captured.deferIndexWrite = deferIndexWrite;
4740
+ }
4741
+ if (meta !== undefined) {
4742
+ const capturedMetaInput = Object.create(null) as NonNullable<
4743
+ SharedAppendOptions<T>["meta"]
4744
+ >;
4745
+ const type = meta.type;
4746
+ const gidSeed = meta.gidSeed;
4747
+ const hasData = "data" in meta;
4748
+ const data = meta.data;
4749
+ const timestamp = meta.timestamp;
4750
+ const next = meta.next;
4751
+ if (type !== undefined) capturedMetaInput.type = type;
4752
+ if (gidSeed !== undefined) capturedMetaInput.gidSeed = gidSeed;
4753
+ if (hasData) capturedMetaInput.data = data;
4754
+ if (timestamp !== undefined) capturedMetaInput.timestamp = timestamp;
4755
+ if (next !== undefined) capturedMetaInput.next = next;
4756
+ captured.meta = capturedMetaInput;
4757
+ }
4758
+ if (identity !== undefined) captured.identity = identity;
4759
+ if (signers !== undefined) captured.signers = signers;
4760
+ if (trim !== undefined) captured.trim = trim;
4761
+ if (encryption !== undefined) captured.encryption = encryption;
4762
+ if (onChange !== undefined) captured.onChange = onChange;
4763
+ if (canAppend !== undefined) captured.canAppend = canAppend;
4764
+ if (replicas !== undefined) captured.replicas = replicas;
4765
+ if (replicate !== undefined) captured.replicate = replicate;
4766
+ if (target !== undefined) captured.target = target;
4767
+ if (delivery !== undefined) captured.delivery = delivery;
4768
+ if (includeDocumentShape) {
4769
+ if (unique !== undefined) captured.unique = unique;
4770
+ if (checkRemote !== undefined) captured.checkRemote = checkRemote;
4771
+ }
4772
+ // Strict-native Documents validates the already-captured finite surface
4773
+ // before any accepted nested value is cloned or normalized. This keeps its
4774
+ // mode error stable even for malformed unsupported option values, without
4775
+ // rereading caller-owned top-level fields.
4776
+ validateCapturedOptions?.(captured);
4777
+ if (meta !== undefined) {
4778
+ const capturedMetaInput = captured.meta!;
4779
+ const capturedMeta = Object.create(null) as NonNullable<
4780
+ SharedAppendOptions<T>["meta"]
4781
+ >;
4782
+ if (capturedMetaInput.type !== undefined) {
4783
+ capturedMeta.type = capturedMetaInput.type;
4784
+ }
4785
+ if (capturedMetaInput.gidSeed !== undefined) {
4786
+ capturedMeta.gidSeed = Uint8Array.from(capturedMetaInput.gidSeed);
4787
+ }
4788
+ if ("data" in capturedMetaInput) {
4789
+ capturedMeta.data =
4790
+ capturedMetaInput.data && Uint8Array.from(capturedMetaInput.data);
4791
+ }
4792
+ if (capturedMetaInput.timestamp !== undefined) {
4793
+ capturedMeta.timestamp = capturedMetaInput.timestamp.clone();
4794
+ }
4795
+ if (capturedMetaInput.next !== undefined) {
4796
+ capturedMeta.next = Object.freeze(
4797
+ capturedMetaInput.next.map((entry) => this.snapshotAppendNext(entry)),
4798
+ ) as unknown as Entry<any>[] | ShallowEntry[];
4799
+ }
4800
+ captured.meta = capturedMeta;
4801
+ }
4802
+ if (signers !== undefined) captured.signers = [...signers];
4803
+ if (trim !== undefined) captured.trim = this.snapshotAppendTrim(trim);
4804
+ if (encryption !== undefined) {
4805
+ captured.encryption = this.snapshotAppendEncryption(encryption);
4806
+ }
4807
+ if (replicas !== undefined) {
4808
+ captured.replicas =
4809
+ typeof replicas === "number"
4810
+ ? replicas
4811
+ : new AbsoluteReplicas(replicas.getValue(this));
4812
+ }
4813
+ return captured;
4814
+ }
4815
+
4816
+ private validatePersistedAppendInvocation(
4817
+ capturedOptions: SharedAppendOptions<T>,
4818
+ delivery: PersistedDeliveryOptions,
4819
+ ): void {
4820
+ this._parseDeliveryOptions(delivery);
4821
+ const target = capturedOptions.target;
4504
4822
  if (target !== undefined && target !== "replicators") {
4505
4823
  throw new Error(
4506
4824
  'persisted delivery requires target="replicators" (or an omitted target)',
4507
4825
  );
4508
4826
  }
4509
4827
  if (
4510
- parsed.delivery?.timeout != null &&
4511
- (!Number.isFinite(parsed.delivery.timeout) ||
4512
- parsed.delivery.timeout <= 0 ||
4513
- parsed.delivery.timeout > MAX_PERSISTED_DELIVERY_TIMEOUT_MS)
4828
+ delivery.timeout != null &&
4829
+ (!Number.isFinite(delivery.timeout) ||
4830
+ delivery.timeout <= 0 ||
4831
+ delivery.timeout > MAX_PERSISTED_DELIVERY_TIMEOUT_MS)
4514
4832
  ) {
4515
4833
  throw new Error(
4516
4834
  `persisted delivery timeout must be a positive number no greater than ${MAX_PERSISTED_DELIVERY_TIMEOUT_MS}`,
4517
4835
  );
4518
4836
  }
4519
- if (parsed.delivery?.signal?.aborted) {
4520
- throw parsed.delivery.signal.reason ?? new AbortError();
4837
+ if (delivery.signal?.aborted) {
4838
+ throw delivery.signal.reason ?? new AbortError();
4521
4839
  }
4522
- return parsed.delivery!;
4523
4840
  }
4524
4841
 
4525
- private assertPersistedDeliveryOptions(
4842
+ private capturePersistedAppendInvocation(
4526
4843
  options?: SharedAppendOptions<T>,
4527
- ): void {
4528
- this.getPersistedDeliveryOptions(options);
4844
+ ): PersistedAppendInvocation<T> | undefined {
4845
+ const deliveryArgument = options?.delivery;
4846
+ if (typeof deliveryArgument !== "object" || deliveryArgument === null) {
4847
+ return undefined;
4848
+ }
4849
+ const reliability = deliveryArgument.reliability;
4850
+ if (reliability !== "persisted") {
4851
+ return undefined;
4852
+ }
4853
+ const delivery = this.snapshotDeliveryOptions(
4854
+ deliveryArgument,
4855
+ reliability,
4856
+ ) as PersistedDeliveryOptions;
4857
+ const capturedOptions = this.snapshotSupportedAppendOptions(
4858
+ options!,
4859
+ delivery,
4860
+ false,
4861
+ );
4862
+ this.validatePersistedAppendInvocation(capturedOptions, delivery);
4863
+ return { options: capturedOptions, delivery };
4864
+ }
4865
+
4866
+ private snapshotDocumentAppendOptions(
4867
+ options?: DocumentSharedAppendOptions<T>,
4868
+ validateCapturedOptions?: (options: DocumentSharedAppendOptions<T>) => void,
4869
+ ): DocumentSharedAppendOptions<T> | undefined {
4870
+ if (!options) return;
4871
+ const deliveryArgument = options.delivery;
4872
+ let delivery: SharedAppendOptions<T>["delivery"] = deliveryArgument;
4873
+ if (typeof deliveryArgument === "object" && deliveryArgument !== null) {
4874
+ const reliability = deliveryArgument.reliability;
4875
+ delivery = this.snapshotDeliveryOptions(deliveryArgument, reliability);
4876
+ }
4877
+ const capturedOptions = this.snapshotSupportedAppendOptions(
4878
+ options,
4879
+ delivery,
4880
+ true,
4881
+ validateCapturedOptions,
4882
+ );
4883
+ if (
4884
+ typeof delivery === "object" &&
4885
+ delivery !== null &&
4886
+ delivery.reliability === "persisted"
4887
+ ) {
4888
+ this.validatePersistedAppendInvocation(
4889
+ capturedOptions,
4890
+ delivery as PersistedDeliveryOptions,
4891
+ );
4892
+ }
4893
+ return capturedOptions;
4529
4894
  }
4530
4895
 
4531
4896
  private async _getSortedRouteHints(targetHash: string): Promise<RouteHint[]> {
@@ -5217,7 +5582,7 @@ export class SharedLog<
5217
5582
  }
5218
5583
 
5219
5584
  private persistedDeliveryTimeoutMs(
5220
- delivery: DeliveryOptions,
5585
+ delivery: PersistedDeliveryOptions,
5221
5586
  hashCount: number,
5222
5587
  ): number {
5223
5588
  return (
@@ -5263,7 +5628,7 @@ export class SharedLog<
5263
5628
  }
5264
5629
 
5265
5630
  private createPersistedDeliveryDeadline(
5266
- delivery: DeliveryOptions,
5631
+ delivery: PersistedDeliveryOptions,
5267
5632
  ownershipLifecycleController: AbortController,
5268
5633
  hashCount = 1,
5269
5634
  ): PersistedDeliveryDeadline {
@@ -5304,7 +5669,7 @@ export class SharedLog<
5304
5669
  }
5305
5670
 
5306
5671
  private async planPersistedDeliveryLeaders(
5307
- entries: PersistedDeliveryPlanningEntry<T, R>[],
5672
+ records: PersistedDeliveryPlanningRecord<T, R>[],
5308
5673
  replicas: number,
5309
5674
  ownershipLifecycleController: AbortController,
5310
5675
  ): Promise<LeaderMap[]> {
@@ -5312,7 +5677,13 @@ export class SharedLog<
5312
5677
  this.findLeadersFromEntry !== SharedLog.prototype.findLeadersFromEntry
5313
5678
  ) {
5314
5679
  const leaders: LeaderMap[] = [];
5315
- for (const entry of entries) {
5680
+ for (const record of records) {
5681
+ const entry = record.createFullPlanningSource?.();
5682
+ if (!entry) {
5683
+ throw new Error(
5684
+ `Persisted delivery requires canonical entry bytes for custom leader planning of ${record.canonicalHash}`,
5685
+ );
5686
+ }
5316
5687
  leaders.push(
5317
5688
  await this.findLeadersFromEntry(
5318
5689
  entry,
@@ -5324,8 +5695,8 @@ export class SharedLog<
5324
5695
  }
5325
5696
  return leaders;
5326
5697
  }
5327
- const items: EntryLeaderBatchItem<R>[] = entries.map((entry) => ({
5328
- entry,
5698
+ const items: EntryLeaderBatchItem<R>[] = records.map((record) => ({
5699
+ entry: record.createDefaultPlanningSource(),
5329
5700
  replicas,
5330
5701
  options: { freshLeaderPlan: true, persist: false },
5331
5702
  }));
@@ -5377,25 +5748,27 @@ export class SharedLog<
5377
5748
  }
5378
5749
 
5379
5750
  private async settlePersistedDelivery(
5380
- input: PersistedDeliveryPlanningEntry<T, R>[],
5751
+ input: PersistedDeliveryPlanningRecord<T, R>[],
5381
5752
  replicas: number,
5382
- delivery: DeliveryOptions,
5753
+ delivery: PersistedDeliveryOptions,
5383
5754
  ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
5384
5755
  persistedDeadline?: PersistedDeliveryDeadline,
5385
5756
  transferOnFirstRound = false,
5386
5757
  ): Promise<void> {
5387
5758
  const minAcks = Math.floor(delivery.minAcks!);
5388
- const entries = new Map(input.map((entry) => [entry.hash, entry]));
5389
- if (entries.size === 0) return;
5759
+ const records = new Map(
5760
+ input.map((record) => [record.canonicalHash, record]),
5761
+ );
5762
+ if (records.size === 0) return;
5390
5763
 
5391
- const committedHashes = [...entries.keys()];
5764
+ const committedHashes = [...records.keys()];
5392
5765
  const ownedDeadline = !persistedDeadline;
5393
5766
  const deadline =
5394
5767
  persistedDeadline ??
5395
5768
  this.createPersistedDeliveryDeadline(
5396
5769
  delivery,
5397
5770
  ownershipLifecycleController,
5398
- entries.size,
5771
+ records.size,
5399
5772
  );
5400
5773
  const signal = deadline.signal;
5401
5774
  let maxAttemptMs = MAX_PERSISTED_RECEIPT_ATTEMPT_MS;
@@ -5464,7 +5837,7 @@ export class SharedLog<
5464
5837
  // transport epoch. A revision/session change purges them before they can
5465
5838
  // survive an away-and-back leader transition or combine with a later peer.
5466
5839
  const hashesByPeer = new Map<string, string[]>();
5467
- const entryArray = [...entries.values()];
5840
+ const entryArray = [...records.values()];
5468
5841
  const leadersByEntry = await this.planPersistedDeliveryLeaders(
5469
5842
  entryArray,
5470
5843
  replicas,
@@ -5485,7 +5858,7 @@ export class SharedLog<
5485
5858
  }
5486
5859
  }
5487
5860
  for (let index = 0; index < entryArray.length; index++) {
5488
- const hash = entryArray[index]!.hash;
5861
+ const hash = entryArray[index]!.canonicalHash;
5489
5862
  const leaders = leadersByEntry[index]!;
5490
5863
  if (signal.aborted) {
5491
5864
  throw signal.reason ?? new AbortError();
@@ -5756,7 +6129,7 @@ export class SharedLog<
5756
6129
  }
5757
6130
  return false;
5758
6131
  }
5759
- const hash = entryArray[index]!.hash;
6132
+ const hash = entryArray[index]!.canonicalHash;
5760
6133
  const leaders = validatedLeaders[index]!;
5761
6134
  let valid = 0;
5762
6135
  const acknowledgements = carriedAcknowledgements.get(hash)!;
@@ -11503,7 +11876,9 @@ export class SharedLog<
11503
11876
  removed: ShallowOrFullEntry<T>[];
11504
11877
  }> {
11505
11878
  this.throwIfNativeDurableCommitFailed();
11506
- const persistedDelivery = this.getPersistedDeliveryOptions(options);
11879
+ const persistedInvocation = this.capturePersistedAppendInvocation(options);
11880
+ options = persistedInvocation?.options ?? options;
11881
+ const persistedDelivery = persistedInvocation?.delivery;
11507
11882
  const ownershipLifecycleController =
11508
11883
  this.captureReplicationOwnershipLifecycle();
11509
11884
  if (this._isAdaptiveReplicating) {
@@ -11515,14 +11890,32 @@ export class SharedLog<
11515
11890
  ownershipLifecycleController,
11516
11891
  );
11517
11892
  let committedHashes: readonly string[] | undefined;
11893
+ let persistedAppendCommit: PreparedLocalAppendCommit<R> | undefined;
11894
+ let persistedPlanningRecord:
11895
+ | PersistedDeliveryPlanningRecord<T, R>
11896
+ | undefined;
11518
11897
  if (persistedDelivery) {
11519
11898
  (appendOptions as TrustedLogAppendOptions<T>).__peerbitOnLocalCommit = (
11520
11899
  hashes,
11900
+ entries,
11521
11901
  ) => {
11522
11902
  // The lower log reports the exact hashes immediately after their
11523
- // irreversible local mutation and before entry initialization, trim,
11524
- // or change callbacks can reject the append.
11525
- committedHashes = hashes;
11903
+ // irreversible local mutation and before trim/change callbacks. Own the
11904
+ // canonical hash first, then detach all planning facts and storage bytes
11905
+ // synchronously so callback mutation cannot retarget the later quorum.
11906
+ committedHashes = Object.freeze([...hashes]);
11907
+ if (hashes.length !== 1 || entries?.length !== 1) {
11908
+ throw new Error(
11909
+ "Persisted delivery requires exact lower-log entry commit evidence",
11910
+ );
11911
+ }
11912
+ persistedAppendCommit = this.capturePersistedLocalAppendCommit(
11913
+ hashes[0]!,
11914
+ entries[0]!,
11915
+ );
11916
+ persistedPlanningRecord = this.createPersistedDeliveryPlanningRecord(
11917
+ persistedAppendCommit,
11918
+ );
11526
11919
  };
11527
11920
  }
11528
11921
  let persistedDeadline: PersistedDeliveryDeadline | undefined;
@@ -11533,7 +11926,11 @@ export class SharedLog<
11533
11926
  };
11534
11927
  try {
11535
11928
  const result = await this.log.append(data, appendOptions);
11536
- committedHashes ??= [result.entry.hash];
11929
+ if (persistedDelivery && (!committedHashes || !persistedPlanningRecord)) {
11930
+ throw new Error(
11931
+ "Lower log did not provide persisted-delivery commit evidence",
11932
+ );
11933
+ }
11537
11934
  persistedDeadline = persistedDelivery
11538
11935
  ? this.createPersistedDeliveryDeadline(
11539
11936
  persistedDelivery,
@@ -11545,14 +11942,17 @@ export class SharedLog<
11545
11942
  ownershipLifecycleController,
11546
11943
  );
11547
11944
  throwIfDeliveryAborted();
11548
- await this.processLocalAppend(result.entry, result.removed, options, {
11945
+ const processingEntry =
11946
+ persistedPlanningRecord?.createFullPlanningSource?.() ?? result.entry;
11947
+ await this.processLocalAppend(processingEntry, result.removed, options, {
11549
11948
  minReplicasValue,
11949
+ appendFacts: persistedAppendCommit,
11550
11950
  ownershipLifecycleController,
11551
11951
  });
11552
11952
  throwIfDeliveryAborted();
11553
11953
  if (persistedDelivery && persistedDeadline) {
11554
11954
  await this.settlePersistedDelivery(
11555
- [result.entry],
11955
+ [persistedPlanningRecord!],
11556
11956
  minReplicasValue,
11557
11957
  persistedDelivery,
11558
11958
  ownershipLifecycleController,
@@ -11576,7 +11976,7 @@ export class SharedLog<
11576
11976
  private rejectPersistedDeliveryOnTrustedLocalAppend(
11577
11977
  options?: SharedAppendOptions<T>,
11578
11978
  ): void {
11579
- if (this.getPersistedDeliveryOptions(options)) {
11979
+ if (this.capturePersistedAppendInvocation(options)) {
11580
11980
  throw new Error(
11581
11981
  "trusted local append paths require delivery=false; call deliverPersistedEntries after the local commit",
11582
11982
  );
@@ -14726,7 +15126,7 @@ export class SharedLog<
14726
15126
  removed: ShallowOrFullEntry<T>[];
14727
15127
  }> {
14728
15128
  this.throwIfNativeDurableCommitFailed();
14729
- const persistedDelivery = this.getPersistedDeliveryOptions(options);
15129
+ const persistedDelivery = this.capturePersistedAppendInvocation(options);
14730
15130
  if (data.length === 0) {
14731
15131
  return { entries: [], removed: [] };
14732
15132
  }
@@ -14818,18 +15218,22 @@ export class SharedLog<
14818
15218
  * rolls back or hides their successful local commit.
14819
15219
  */
14820
15220
  private async deliverPersistedPlanningEntries(
14821
- resolveEntries: () => PersistedDeliveryPlanningEntry<T, R>[],
14822
- committedHashes: string[],
15221
+ resolveRecords: () => PersistedDeliveryPlanningRecord<T, R>[],
15222
+ committedHashesInput: readonly string[],
14823
15223
  options: SharedAppendOptions<T>,
14824
15224
  ): Promise<void> {
15225
+ const committedHashes = Object.freeze([...committedHashesInput]);
14825
15226
  let persistedDeadline: PersistedDeliveryDeadline | undefined;
14826
15227
  try {
14827
- const delivery = this.getPersistedDeliveryOptions(options);
14828
- if (!delivery) {
15228
+ const persistedInvocation =
15229
+ this.capturePersistedAppendInvocation(options);
15230
+ if (!persistedInvocation) {
14829
15231
  throw new Error(
14830
15232
  'deliverPersistedEntries requires reliability="persisted"',
14831
15233
  );
14832
15234
  }
15235
+ options = persistedInvocation.options;
15236
+ const delivery = persistedInvocation.delivery;
14833
15237
  const ownershipLifecycleController =
14834
15238
  this.captureReplicationOwnershipLifecycle();
14835
15239
  const deadline = this.createPersistedDeliveryDeadline(
@@ -14848,16 +15252,26 @@ export class SharedLog<
14848
15252
  );
14849
15253
  }
14850
15254
  };
14851
- const entries = resolveEntries();
15255
+ const records = resolveRecords();
14852
15256
  throwIfDeliveryAborted();
14853
- if (entries.length === 0) return;
15257
+ if (records.length !== committedHashes.length) {
15258
+ throw new Error("Persisted delivery planning evidence count mismatch");
15259
+ }
15260
+ for (let index = 0; index < records.length; index++) {
15261
+ if (records[index]!.canonicalHash !== committedHashes[index]) {
15262
+ throw new Error(
15263
+ `Persisted delivery planning evidence did not match committed hash ${committedHashes[index]}`,
15264
+ );
15265
+ }
15266
+ }
15267
+ if (records.length === 0) return;
14854
15268
  const { minReplicasValue } = this.createLogAppendOptions(
14855
15269
  options,
14856
15270
  ownershipLifecycleController,
14857
15271
  );
14858
15272
  throwIfDeliveryAborted();
14859
15273
  await this.settlePersistedDelivery(
14860
- entries,
15274
+ records,
14861
15275
  minReplicasValue,
14862
15276
  delivery,
14863
15277
  ownershipLifecycleController,
@@ -14871,20 +15285,21 @@ export class SharedLog<
14871
15285
  }
14872
15286
  }
14873
15287
 
14874
- private createPersistedDeliveryPlanningEntries(
15288
+ private createPersistedDeliveryPlanningRecords(
14875
15289
  appendCommits: PreparedLocalAppendCommit<R>[],
14876
15290
  materializeEntries: () => Entry<T>[],
14877
- ): PersistedDeliveryPlanningEntry<T, R>[] {
15291
+ ): PersistedDeliveryPlanningRecord<T, R>[] {
14878
15292
  let materializedEntries: Entry<T>[] | undefined;
14879
15293
  const requiresFullEntries =
14880
15294
  this.findLeadersFromEntry !== SharedLog.prototype.findLeadersFromEntry;
14881
- const getMaterializedEntry = (
15295
+ const getCommittedEntry = (
14882
15296
  appendCommit: PreparedLocalAppendCommit<R>,
14883
15297
  index: number,
14884
15298
  ) => {
14885
15299
  materializedEntries ??= materializeEntries();
14886
15300
  const entry = materializedEntries[index];
14887
- if (!entry || entry.hash !== appendCommit.hash) {
15301
+ const materializedHash = entry?.hash;
15302
+ if (!entry || materializedHash !== appendCommit.hash) {
14888
15303
  throw new Error(
14889
15304
  `Persisted delivery materializer did not return committed entry ${appendCommit.hash}`,
14890
15305
  );
@@ -14892,12 +15307,14 @@ export class SharedLog<
14892
15307
  return entry;
14893
15308
  };
14894
15309
  return appendCommits.map((appendCommit, index) => {
14895
- if (!requiresFullEntries && appendCommit.coordinateFields) {
14896
- return this._coordinates.materializeResidentCoordinateEntry(
14897
- appendCommit.coordinateFields,
15310
+ let capturedCommit = this.snapshotPreparedLocalAppendCommit(appendCommit);
15311
+ if (requiresFullEntries && !capturedCommit.storageBytes) {
15312
+ capturedCommit = this.capturePersistedLocalAppendCommit(
15313
+ capturedCommit.hash,
15314
+ getCommittedEntry(capturedCommit, index),
14898
15315
  );
14899
15316
  }
14900
- return getMaterializedEntry(appendCommit, index);
15317
+ return this.createPersistedDeliveryPlanningRecord(capturedCommit);
14901
15318
  });
14902
15319
  }
14903
15320
 
@@ -14908,7 +15325,7 @@ export class SharedLog<
14908
15325
  ): Promise<void> {
14909
15326
  return this.deliverPersistedPlanningEntries(
14910
15327
  () =>
14911
- this.createPersistedDeliveryPlanningEntries(
15328
+ this.createPersistedDeliveryPlanningRecords(
14912
15329
  appendCommits,
14913
15330
  materializeEntries,
14914
15331
  ),
@@ -14921,9 +15338,12 @@ export class SharedLog<
14921
15338
  entries: Entry<T>[],
14922
15339
  options: SharedAppendOptions<T>,
14923
15340
  ): Promise<void> {
15341
+ const records = entries.map((entry) =>
15342
+ this.snapshotPersistedDeliveryPlanningEntry(entry),
15343
+ );
14924
15344
  return this.deliverPersistedPlanningEntries(
14925
- () => entries,
14926
- entries.map((entry) => entry.hash),
15345
+ () => records,
15346
+ records.map((record) => record.canonicalHash),
14927
15347
  options,
14928
15348
  );
14929
15349
  }
@@ -15871,7 +16291,11 @@ export class SharedLog<
15871
16291
  next: entry.meta.next,
15872
16292
  wallTime: entry.meta.clock.timestamp.wallTime,
15873
16293
  logical: entry.meta.clock.timestamp.logical,
16294
+ clockId: entry.meta.clock.id,
16295
+ type: entry.meta.type,
16296
+ metaData: entry.meta.data,
15874
16297
  payloadSize: entry.payload.byteLength,
16298
+ entrySize: entry.size,
15875
16299
  metaBytes: (entry as EntryWithMetaBytes).getMetaBytes?.(),
15876
16300
  hashNumber: nativeAppendPlan?.hashNumber,
15877
16301
  coordinateFields: nativeAppendPlan?.preparedCoordinate.fields,
@@ -15892,6 +16316,9 @@ export class SharedLog<
15892
16316
  next: appendFacts.next,
15893
16317
  wallTime: appendFacts.wallTime,
15894
16318
  logical: appendFacts.logical,
16319
+ clockId: appendFacts.clockId,
16320
+ type: appendFacts.type,
16321
+ metaData: appendFacts.metaData,
15895
16322
  payloadSize: appendFacts.payloadSize,
15896
16323
  metaBytes: appendFacts.metaBytes,
15897
16324
  hashNumber: nativeAppendPlan?.hashNumber,
@@ -15901,6 +16328,134 @@ export class SharedLog<
15901
16328
  };
15902
16329
  }
15903
16330
 
16331
+ private snapshotPreparedLocalAppendCommit(
16332
+ appendCommit: PreparedLocalAppendCommit<R>,
16333
+ ): PreparedLocalAppendCommit<R> {
16334
+ const coordinateFields = appendCommit.coordinateFields;
16335
+ return Object.freeze({
16336
+ ...appendCommit,
16337
+ next: [...appendCommit.next],
16338
+ clockId: appendCommit.clockId && Uint8Array.from(appendCommit.clockId),
16339
+ metaData: appendCommit.metaData && Uint8Array.from(appendCommit.metaData),
16340
+ metaBytes:
16341
+ appendCommit.metaBytes && Uint8Array.from(appendCommit.metaBytes),
16342
+ storageBytes:
16343
+ appendCommit.storageBytes && Uint8Array.from(appendCommit.storageBytes),
16344
+ coordinateFields: coordinateFields
16345
+ ? {
16346
+ ...coordinateFields,
16347
+ coordinates: [...coordinateFields.coordinates],
16348
+ coordinateStrings: coordinateFields.coordinateStrings && [
16349
+ ...coordinateFields.coordinateStrings,
16350
+ ],
16351
+ metaBytes: Uint8Array.from(coordinateFields.metaBytes),
16352
+ }
16353
+ : undefined,
16354
+ documentPreviousContext: appendCommit.documentPreviousContext
16355
+ ? { ...appendCommit.documentPreviousContext }
16356
+ : undefined,
16357
+ });
16358
+ }
16359
+
16360
+ private capturePersistedLocalAppendCommit(
16361
+ canonicalHash: string,
16362
+ entry: Entry<T>,
16363
+ ): PreparedLocalAppendCommit<R> {
16364
+ const sourceHash = entry.hash;
16365
+ if (sourceHash !== canonicalHash) {
16366
+ throw new Error(
16367
+ `Lower-log commit evidence entry did not match committed hash ${canonicalHash}`,
16368
+ );
16369
+ }
16370
+ const storageBytes =
16371
+ Entry.getPreparedStorageBytes(entry) ?? entry.getStorageBytes();
16372
+ return this.snapshotPreparedLocalAppendCommit({
16373
+ ...this.createPreparedLocalAppendCommit(entry),
16374
+ hash: canonicalHash,
16375
+ storageBytes,
16376
+ });
16377
+ }
16378
+
16379
+ private createPersistedDeliveryPlanningRecord(
16380
+ appendCommitInput: PreparedLocalAppendCommit<R>,
16381
+ ): PersistedDeliveryPlanningRecord<T, R> {
16382
+ // Callers hand this helper an invocation-owned snapshot. Do not duplicate
16383
+ // full entry bytes here; a fresh copy is made only when a custom planner is
16384
+ // actually invoked (and again for each replan so planner mutation cannot
16385
+ // persist between rounds).
16386
+ const appendCommit = appendCommitInput;
16387
+ const canonicalHash = appendCommit.hash;
16388
+ const coordinateFields = appendCommit.coordinateFields;
16389
+ const createDefaultPlanningSource = coordinateFields
16390
+ ? () =>
16391
+ this._coordinates.materializeResidentCoordinateEntry({
16392
+ ...coordinateFields,
16393
+ coordinates: [...coordinateFields.coordinates],
16394
+ coordinateStrings: coordinateFields.coordinateStrings && [
16395
+ ...coordinateFields.coordinateStrings,
16396
+ ],
16397
+ metaBytes: Uint8Array.from(coordinateFields.metaBytes),
16398
+ })
16399
+ : () =>
16400
+ new ShallowEntry({
16401
+ hash: canonicalHash,
16402
+ head: true,
16403
+ payloadSize: appendCommit.payloadSize,
16404
+ meta: new ShallowMeta({
16405
+ gid: appendCommit.gid,
16406
+ next: [...appendCommit.next],
16407
+ type: appendCommit.type ?? EntryType.APPEND,
16408
+ data:
16409
+ appendCommit.metaData && Uint8Array.from(appendCommit.metaData),
16410
+ clock: new LamportClock({
16411
+ id: Uint8Array.from(appendCommit.clockId ?? new Uint8Array()),
16412
+ timestamp: new Timestamp({
16413
+ wallTime: appendCommit.wallTime,
16414
+ logical: appendCommit.logical,
16415
+ }),
16416
+ }),
16417
+ }),
16418
+ });
16419
+ const storageBytes = appendCommit.storageBytes;
16420
+ const createFullPlanningSource = storageBytes
16421
+ ? () => {
16422
+ const bytes = Uint8Array.from(storageBytes);
16423
+ const entry = deserialize(bytes, Entry) as Entry<T>;
16424
+ const decodedHash = entry.hash;
16425
+ if (decodedHash && decodedHash !== canonicalHash) {
16426
+ throw new Error(
16427
+ `Persisted delivery planning bytes did not match committed hash ${canonicalHash}`,
16428
+ );
16429
+ }
16430
+ if (!decodedHash) {
16431
+ Entry.prepareMultihashBytes(entry, bytes, canonicalHash);
16432
+ entry.hash = canonicalHash;
16433
+ }
16434
+ entry.size = appendCommit.entrySize ?? bytes.byteLength;
16435
+ entry.createdLocally = true;
16436
+ entry.init({
16437
+ encoding: this.log.encoding,
16438
+ keychain: this.log.keychain,
16439
+ });
16440
+ return entry;
16441
+ }
16442
+ : undefined;
16443
+ return Object.freeze({
16444
+ canonicalHash,
16445
+ createDefaultPlanningSource,
16446
+ createFullPlanningSource,
16447
+ });
16448
+ }
16449
+
16450
+ private snapshotPersistedDeliveryPlanningEntry(
16451
+ value: Entry<T>,
16452
+ ): PersistedDeliveryPlanningRecord<T, R> {
16453
+ const canonicalHash = value.hash;
16454
+ return this.createPersistedDeliveryPlanningRecord(
16455
+ this.capturePersistedLocalAppendCommit(canonicalHash, value),
16456
+ );
16457
+ }
16458
+
15904
16459
  private createPreparedLocalAppendCommits(
15905
16460
  entries: Entry<T>[],
15906
16461
  nativeAppendPlans?: Array<NativeAppendEntryPlan<R> | undefined>,