@bjornpagen/bumbledb-log 0.19.2 → 0.20.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/src/manifest.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  /**
2
- * The protocol's one mutable object: a v:3 binary record — version
3
- * byte 3, a branded Digest32 fingerprint, an optional branded Digest32
4
- * checkpoint. The checkpoint record is digest-keyed and immutable:
5
- * version byte 3, a count-bounded braid roster (id, g, hash, ts) in
6
- * ascending id order, catalog digest, writer, optional prev. Every
7
- * digest field is Digest32. The content address is blake3 of the
8
- * record bytes. Seed audits the catalog claim when a checkpoint is
9
- * present.
2
+ * The protocol's one mutable object and its checkpoint, as typed
3
+ * payloads. The byte grammar has one implementation
4
+ * `crates/bumbledb-log` behind the napi bridge so parse and render
5
+ * here are marshal walks: branded `Digest32` and `Braid` values in,
6
+ * bridge-tagged payloads across, never a second reader of the bytes.
7
+ * The vector algebra over checkpoint heads and the seed transition's
8
+ * catalog audit are machine logic and stay here.
10
9
  */
11
10
 
12
- import * as errors from "@superbuilders/errors"
11
+ import type { LogCheckpointDoc, LogCheckpointKind, LogCodecHandle, LogManifestKind } from "@bjornpagen/bumbledb"
12
+ import {
13
+ internalLogParseCheckpoint,
14
+ internalLogParseManifest,
15
+ internalLogRenderCheckpoint,
16
+ internalLogRenderManifest
17
+ } from "@bjornpagen/bumbledb"
13
18
  import type { Digest32 } from "#bytes.ts"
14
- import { ByteReader, ByteWriter, bytesEqual, hex32 } from "#bytes.ts"
19
+ import { bytesEqual, digest32, hex32 } from "#bytes.ts"
15
20
  import type { Braid } from "#descriptor.ts"
16
21
  import { braidHex } from "#descriptor.ts"
17
22
  import { refuse } from "#errors.ts"
@@ -19,66 +24,32 @@ import type { Generation } from "#keys.ts"
19
24
  import { generation } from "#keys.ts"
20
25
  import { Vector } from "#vector.ts"
21
26
 
22
- const DOC_VERSION = 3
23
- const U32_MAX = 0xffffffffn
24
- /** braid u32 + g u64 + hash 32 + ts u64 */
25
- const MIN_HEAD_BYTES = 52n
26
-
27
27
  interface Manifest {
28
28
  readonly fingerprint: Digest32
29
29
  readonly checkpoint: Digest32 | null
30
30
  }
31
31
 
32
- function readerOf(bytes: Uint8Array): ByteReader {
33
- return new ByteReader(bytes, {
34
- fail(what: string): never {
35
- refuse({ kind: "Malformed", at: bytes.length }, `document truncated at ${what}`)
36
- }
37
- })
38
- }
39
-
40
- function finish(reader: ByteReader, at: string): void {
41
- if (reader.remaining() !== 0) {
42
- refuse({ kind: "Malformed", at: reader.remaining() }, `${reader.remaining()} trailing bytes after ${at}`)
43
- }
44
- }
45
-
46
- function refuseUnbacked(count: bigint, remaining: number, minItem: bigint, at: string): void {
47
- if (count === 0n) {
48
- return
49
- }
50
- if (minItem === 0n || BigInt(remaining) / minItem < count) {
51
- refuse({ kind: "Malformed", at: remaining }, `declared ${at} ${count} outruns the remaining ${remaining} bytes`)
52
- }
53
- }
54
-
55
- function writeOptionalDigest(out: ByteWriter, digest: Digest32 | null): void {
56
- if (digest === null) {
57
- out.u8(0)
58
- return
59
- }
60
- out.u8(1)
61
- out.array32(digest)
62
- }
63
-
64
- function readOptionalDigest(reader: ByteReader, at: string): Digest32 | null {
65
- const tag = reader.u8(at)
66
- if (tag === 0) {
67
- return null
68
- }
69
- if (tag === 1) {
70
- return reader.array32(at)
71
- }
72
- refuse({ kind: "Flags", flags: tag }, `${at} presence is not 0 or 1`)
73
- }
74
-
75
- function readVersion(reader: ByteReader, at: string): void {
76
- const version = reader.u8("version")
77
- if (version !== DOC_VERSION) {
78
- refuse(
79
- { kind: "Version", version },
80
- `${at} version ${version}, consumers refuse every version other than ${DOC_VERSION}`
81
- )
32
+ /**
33
+ * Remints a bridge refusal row as the driver's typed refusal. The
34
+ * boundary carries `{ kind, message }` only: the kind is the log
35
+ * core's own identity string, so the cause payload holds the data this
36
+ * side owns — the document's length, its version byte (byte 0 of every
37
+ * v:3 document) — and nothing invented. The raw braid id of an
38
+ * `UnknownBraid` rides the message; the drifted set of a `BraidSet`
39
+ * rides the message the same way.
40
+ */
41
+ function refuseBridged(kind: LogManifestKind | LogCheckpointKind, message: string, bytes: Uint8Array): never {
42
+ switch (kind) {
43
+ case "Version":
44
+ return refuse({ kind: "Version", version: bytes[0] ?? 0 }, message)
45
+ case "Overflow":
46
+ return refuse({ kind: "Overflow" }, message)
47
+ case "UnknownBraid":
48
+ return refuse({ kind: "UnknownBraid" }, message)
49
+ case "BraidSet":
50
+ return refuse({ kind: "BraidSet" }, message)
51
+ case "Malformed":
52
+ return refuse({ kind: "Malformed", at: bytes.length }, message)
82
53
  }
83
54
  }
84
55
 
@@ -87,20 +58,22 @@ function braidIdOf(id: Braid): number {
87
58
  }
88
59
 
89
60
  function renderManifest(manifest: Manifest): Uint8Array {
90
- const out = new ByteWriter(66)
91
- out.u8(DOC_VERSION)
92
- out.array32(manifest.fingerprint)
93
- writeOptionalDigest(out, manifest.checkpoint)
94
- return out.finish()
61
+ return internalLogRenderManifest(
62
+ manifest.checkpoint === null
63
+ ? { fingerprint: manifest.fingerprint }
64
+ : { fingerprint: manifest.fingerprint, checkpoint: manifest.checkpoint }
65
+ )
95
66
  }
96
67
 
97
68
  function parseManifest(bytes: Uint8Array): Manifest {
98
- const reader = readerOf(bytes)
99
- readVersion(reader, "manifest")
100
- const fingerprint = reader.array32("fingerprint")
101
- const checkpoint = readOptionalDigest(reader, "checkpoint")
102
- finish(reader, "the manifest")
103
- return { fingerprint, checkpoint }
69
+ const parsed = internalLogParseManifest(bytes)
70
+ if (!parsed.ok) {
71
+ refuseBridged(parsed.kind, parsed.message, bytes)
72
+ }
73
+ return {
74
+ fingerprint: digest32(parsed.value.fingerprint),
75
+ checkpoint: parsed.value.checkpoint === undefined ? null : digest32(parsed.value.checkpoint)
76
+ }
104
77
  }
105
78
 
106
79
  interface CheckpointHead {
@@ -129,64 +102,43 @@ function checkpointVector(facts: CheckpointFacts): Vector {
129
102
  return vectorOfHeads(facts.braids)
130
103
  }
131
104
 
132
- function renderCheckpoint(facts: CheckpointFacts): Uint8Array {
133
- const braids = [...facts.braids.keys()].sort()
134
- const count = BigInt(braids.length)
135
- if (count > U32_MAX) {
136
- throw errors.new(`checkpoint braid count ${count} exceeds u32`)
137
- }
138
- const out = new ByteWriter(1 + 4 + braids.length * 52 + 32 + 8 + 1 + 32)
139
- out.u8(DOC_VERSION)
140
- out.u32le(Number(count))
141
- for (const id of braids) {
142
- const head = facts.braids.get(id)
143
- if (head === undefined) {
144
- throw errors.new(`checkpoint lost braid ${id}`)
145
- }
146
- out.u32le(braidIdOf(id))
147
- out.u64le(head.g)
148
- out.array32(head.hash)
149
- out.u64le(head.ts)
105
+ function renderCheckpoint(codec: LogCodecHandle, facts: CheckpointFacts): Uint8Array {
106
+ const braids = [...facts.braids.entries()]
107
+ .sort(function ascending(a, b) {
108
+ return braidIdOf(a[0]) - braidIdOf(b[0])
109
+ })
110
+ .map(function headOf([id, head]) {
111
+ return { braid: braidIdOf(id), g: head.g, hash: head.hash, ts: head.ts }
112
+ })
113
+ const doc: LogCheckpointDoc =
114
+ facts.prev === null
115
+ ? { braids, catalog: facts.catalog, writer: facts.writer }
116
+ : { braids, catalog: facts.catalog, writer: facts.writer, prev: facts.prev }
117
+ return internalLogRenderCheckpoint(codec, doc)
118
+ }
119
+
120
+ function parseCheckpoint(codec: LogCodecHandle, bytes: Uint8Array): CheckpointFacts {
121
+ const parsed = internalLogParseCheckpoint(codec, bytes)
122
+ if (!parsed.ok) {
123
+ refuseBridged(parsed.kind, parsed.message, bytes)
150
124
  }
151
- out.array32(facts.catalog)
152
- out.u64le(facts.writer)
153
- writeOptionalDigest(out, facts.prev)
154
- return out.finish()
155
- }
156
-
157
- function parseCheckpoint(bytes: Uint8Array, known?: ReadonlySet<Braid>): CheckpointFacts {
158
- const reader = readerOf(bytes)
159
- readVersion(reader, "checkpoint")
160
- const count = BigInt(reader.u32le("braid count"))
161
- refuseUnbacked(count, reader.remaining(), MIN_HEAD_BYTES, "braid count")
162
125
  const braids = new Map<Braid, CheckpointHead>()
163
- for (let i = 0n; i < count; i++) {
164
- const raw = reader.u32le("braid")
165
- const name = braidHex(raw)
166
- if (known !== undefined && !known.has(name)) {
167
- refuse({ kind: "UnknownBraid", braid: raw }, `checkpoint cites unknown braid ${name}`)
168
- }
169
- const g = reader.u64le("generation")
170
- const hash = reader.array32("hash")
171
- const ts = reader.u64le("timestamp")
172
- const last = [...braids.keys()].at(-1)
173
- if (last !== undefined && last >= name) {
174
- refuse(
175
- { kind: "Malformed", at: bytes.length - reader.remaining() },
176
- "checkpoint braid roster is not strictly ascending"
177
- )
178
- }
179
- braids.set(name, { g: generation(g), hash, ts })
126
+ for (const head of parsed.value.braids) {
127
+ braids.set(braidHex(head.braid), { g: generation(head.g), hash: digest32(head.hash), ts: head.ts })
180
128
  }
181
- const catalog = reader.array32("catalog")
182
- const writer = reader.u64le("writer")
183
- const prev = readOptionalDigest(reader, "prev")
184
- finish(reader, "the checkpoint")
129
+ /** The core refuses `Overflow` at parse; the narrowing here is the
130
+ * derived sum's own type gate over the same heads. */
185
131
  const summed = vectorOfHeads(braids).sum()
186
132
  if (typeof summed !== "bigint") {
187
133
  refuse({ kind: "Overflow" }, "checkpoint vector sum overflows u64")
188
134
  }
189
- return { braids, catalog, writer, prev, sum: summed }
135
+ return {
136
+ braids,
137
+ catalog: digest32(parsed.value.catalog),
138
+ writer: parsed.value.writer,
139
+ prev: parsed.value.prev === undefined ? null : digest32(parsed.value.prev),
140
+ sum: summed
141
+ }
190
142
  }
191
143
 
192
144
  /** The seed transition's catalog claim: a present checkpoint
@@ -205,5 +157,5 @@ function auditCatalog(facts: CheckpointFacts | null, computed: Digest32): void {
205
157
  )
206
158
  }
207
159
 
208
- export type { CheckpointFacts, CheckpointHead, Manifest }
209
- export { auditCatalog, checkpointVector, DOC_VERSION, parseCheckpoint, parseManifest, renderCheckpoint, renderManifest }
160
+ export type { CheckpointFacts, CheckpointHead }
161
+ export { auditCatalog, checkpointVector, parseCheckpoint, parseManifest, renderCheckpoint, renderManifest }
package/src/replica.ts CHANGED
@@ -10,17 +10,17 @@
10
10
 
11
11
  import * as fs from "node:fs/promises"
12
12
  import * as path from "node:path"
13
- import type { Db, Fact, MemberRelation, Schema, SchemaRelations, WriteOutcome } from "@bjornpagen/bumbledb"
14
- import { internalBlake3, Db as SdkDb } from "@bjornpagen/bumbledb"
13
+ import type { Db, MemberRelation, Schema, SchemaRelations, WriteOutcome } from "@bjornpagen/bumbledb"
14
+ import { factOf, internalBlake3, Db as SdkDb } from "@bjornpagen/bumbledb"
15
15
  import * as errors from "@superbuilders/errors"
16
16
  import { parse } from "#braids.ts"
17
17
  import type { Digest32 } from "#bytes.ts"
18
- import { bytesEqual, digest32, hex32, saturatingAddU64, toHex } from "#bytes.ts"
18
+ import { bytesEqual, digest32, hex32, saturatingAddU64 } from "#bytes.ts"
19
19
  import type { Chain, ChainEntry, Pending } from "#chain.ts"
20
20
  import { chainGeneration, chainSum, readSidecar, writeSidecar } from "#chain.ts"
21
21
  import type { Op } from "#codec.ts"
22
22
  import { decodeBatch, encodeBatch, verifyChain } from "#codec.ts"
23
- import type { Braid, Descriptor, RelationInfo } from "#descriptor.ts"
23
+ import type { Braid, Descriptor } from "#descriptor.ts"
24
24
  import { descriptorOf } from "#descriptor.ts"
25
25
  import { ErrRefused, ErrReplayDiverged, refuse, refuseManifestMissing, wrapStore } from "#errors.ts"
26
26
  import type { Generation } from "#keys.ts"
@@ -38,7 +38,6 @@ import {
38
38
  import type { CheckpointFacts } from "#manifest.ts"
39
39
  import { auditCatalog, parseCheckpoint, parseManifest } from "#manifest.ts"
40
40
  import type { Etag, ObjectStore } from "#store.ts"
41
- import type { Value } from "#value.ts"
42
41
  import { Vector } from "#vector.ts"
43
42
 
44
43
  const ZERO_HASH = digest32(new Uint8Array(32))
@@ -46,8 +45,9 @@ const ZERO_HASH = digest32(new Uint8Array(32))
46
45
  /** The gc-safety heartbeat cadence: every N-th pass re-reads the manifest. */
47
46
  const HEARTBEAT_PASSES = 16
48
47
 
49
- /** The re-poll cadence of waitFor, its one consumer. */
50
- const WAIT_FOR_POLL_MS = 20
48
+ /** The re-poll cadence of waitFor, its one consumer — the
49
+ * machine-constants table's `wait_for_poll_ms` fact. */
50
+ const WAIT_FOR_POLL_MS = 10
51
51
 
52
52
  type ReplicaState =
53
53
  | { readonly tag: "bootstrapped" }
@@ -66,6 +66,13 @@ type Step =
66
66
  | { readonly tag: "wedged"; readonly braid: Braid; readonly cause: string }
67
67
  | { readonly tag: "reseed"; readonly cause: string }
68
68
 
69
+ /** The full waitFor sum: a wedged braid the target needs is an
70
+ * outcome, never an infinite poll. */
71
+ type Waited =
72
+ | { readonly tag: "reached"; readonly vector: ReadonlyMap<Braid, Generation> }
73
+ | { readonly tag: "wedged"; readonly braid: Braid; readonly cause: string }
74
+ | { readonly tag: "refused"; readonly detail: string }
75
+
69
76
  type ApplyPhase = "open" | "steady"
70
77
 
71
78
  type SlotApply = { readonly tag: "applied"; readonly generation: bigint } | { readonly tag: "discard" }
@@ -81,7 +88,7 @@ interface Replica<Rels extends SchemaRelations> extends AsyncDisposable {
81
88
  readonly db: Db<Rels>
82
89
  readonly vector: ReadonlyMap<Braid, Generation>
83
90
  refresh(braid?: Braid): Promise<ReadonlyMap<Braid, Generation>>
84
- waitFor(vector: ReadonlyMap<Braid, Generation>): Promise<void>
91
+ waitFor(vector: ReadonlyMap<Braid, Generation>): Promise<Waited>
85
92
  }
86
93
 
87
94
  interface Core<Rels extends SchemaRelations> {
@@ -130,10 +137,6 @@ function withGate<Rels extends SchemaRelations, R>(core: Core<Rels>, body: () =>
130
137
  return run
131
138
  }
132
139
 
133
- function blake3Hex(bytes: Uint8Array): string {
134
- return toHex(new Uint8Array(internalBlake3(bytes)))
135
- }
136
-
137
140
  function blake3Digest(bytes: Uint8Array): Digest32 {
138
141
  return digest32(new Uint8Array(internalBlake3(bytes)))
139
142
  }
@@ -162,7 +165,7 @@ function braidOf(theory: Schema<SchemaRelations> | Schema<never>, raw: Braid): B
162
165
  const id = Number.parseInt(raw.slice(1), 16)
163
166
  const parsed = parse(theory, id)
164
167
  if (parsed === undefined) {
165
- refuse({ kind: "UnknownBraid", braid: Number.isFinite(id) ? id : 0 }, `unknown braid ${raw}`)
168
+ refuse({ kind: "UnknownBraid" }, `unknown braid ${raw}`)
166
169
  }
167
170
  return parsed
168
171
  }
@@ -194,40 +197,25 @@ function generationOf<Rels extends SchemaRelations>(core: Core<Rels>): bigint {
194
197
  })
195
198
  }
196
199
 
197
- function digestOfCatalog(raw: unknown): Digest32 {
198
- if (raw instanceof Uint8Array) {
199
- return digest32(raw)
200
- }
201
- throw errors.new(`catalogDigest is not a digest: ${typeof raw}`)
202
- }
203
-
204
- /** The opened store's catalog digest. The SDK may expose it on the
205
- * handle or on the read instance; either spelling is the computed claim. */
200
+ /** The opened store's catalog digest — the engine handle's computed claim. */
206
201
  function catalogDigestOf<Rels extends SchemaRelations>(core: Core<Rels>): Digest32 {
207
- const handle = core.db as unknown as { catalogDigest?: () => unknown }
208
- if (typeof handle.catalogDigest === "function") {
209
- return digestOfCatalog(handle.catalogDigest())
210
- }
211
- return core.db.read(function readCatalog(instance) {
212
- const carrier = instance as { catalogDigest?: unknown }
213
- if (typeof carrier.catalogDigest === "function") {
214
- return digestOfCatalog(carrier.catalogDigest())
215
- }
216
- throw errors.new("the opened store does not expose catalogDigest")
217
- })
218
- }
219
-
220
- function chainOf<Rels extends SchemaRelations>(core: Core<Rels>): Chain {
221
- return core.chain
202
+ return digest32(core.db.catalogDigest())
222
203
  }
223
204
 
224
- /** Pending-arm payload: recorded ops ride on the batch, not a third
225
- * Option beside the vector. Wire Pending is {braid,gen,bytes}; ops is
226
- * the in-memory field of that arm (§30). */
227
- type HeldBatch = Pending & { readonly ops: readonly Op[] | null }
205
+ /** Pending-arm payload: recorded ops and the batch header's timestamp
206
+ * ride on the batch, not a third Option beside the vector. Wire
207
+ * Pending is {braid,gen,bytes}; ops/ts are the in-memory fields of
208
+ * that arm (§30) null on a batch adopted as bytes the seat has not
209
+ * decoded. */
210
+ type HeldBatch = Pending & { readonly ops: readonly Op[] | null; readonly ts: bigint | null }
228
211
 
229
- function holdPending<Rels extends SchemaRelations>(core: Core<Rels>, batch: Pending, ops: readonly Op[] | null): void {
230
- const held: HeldBatch = { braid: batch.braid, gen: batch.gen, bytes: batch.bytes, ops }
212
+ function holdPending<Rels extends SchemaRelations>(
213
+ core: Core<Rels>,
214
+ batch: Pending,
215
+ ops: readonly Op[] | null,
216
+ ts: bigint | null
217
+ ): void {
218
+ const held: HeldBatch = { braid: batch.braid, slot: batch.slot, bytes: batch.bytes, ops, ts }
231
219
  core.chain = { tag: "pending", entries: entriesOf(core), batch: held }
232
220
  }
233
221
 
@@ -258,7 +246,7 @@ function atCheckpointFloor<Rels extends SchemaRelations>(core: Core<Rels>): bool
258
246
  }
259
247
 
260
248
  async function persistSidecar<Rels extends SchemaRelations>(core: Core<Rels>): Promise<void> {
261
- await writeSidecar(sidecarPath(core), chainOf(core))
249
+ await writeSidecar(core.descriptor.codec, sidecarPath(core), core.chain)
262
250
  }
263
251
 
264
252
  /** Writes the checkpoint store file and fsyncs the file and its parent
@@ -312,9 +300,10 @@ function adoptChain<Rels extends SchemaRelations>(core: Core<Rels>, chain: Chain
312
300
  if (chain.tag === "pending") {
313
301
  const held: HeldBatch = {
314
302
  braid: braidOf(core.theory, chain.batch.braid),
315
- gen: chain.batch.gen,
303
+ slot: chain.batch.slot,
316
304
  bytes: chain.batch.bytes,
317
- ops: null
305
+ ops: null,
306
+ ts: null
318
307
  }
319
308
  core.chain = { tag: "pending", entries, batch: held }
320
309
  } else {
@@ -322,47 +311,20 @@ function adoptChain<Rels extends SchemaRelations>(core: Core<Rels>, chain: Chain
322
311
  }
323
312
  }
324
313
 
325
- /** Positional decoded row the SDK's named fact, handles lifted for closed refs. */
326
- function factOf<Rels extends SchemaRelations>(
327
- core: Core<Rels>,
328
- relation: RelationInfo,
329
- row: readonly Value[]
330
- ): Record<string, unknown> {
331
- const fact: Record<string, unknown> = {}
332
- relation.fields.forEach(function liftCell(field, ordinal) {
333
- const value = row[ordinal]
334
- if (value === undefined) {
335
- throw errors.new(`relation ${relation.name}: decoded row cell ${ordinal} absent`)
336
- }
337
- if (field.closedRef !== undefined && typeof value === "bigint") {
338
- const roster = core.descriptor.relationByName.get(field.closedRef)
339
- const handle = roster?.handles[Number(value)]
340
- if (handle === undefined) {
341
- throw errors.new(
342
- `relation ${relation.name}.${field.name}: id ${value} is outside the ${field.closedRef} roster`
343
- )
344
- }
345
- fact[field.name] = handle
346
- return
347
- }
348
- fact[field.name] = value
349
- })
350
- return fact
351
- }
352
-
353
- /** One `db.write` applying ops in listed order, rows in listed order. */
314
+ /** One `db.write` applying ops in listed order, rows in listed order.
315
+ * The positional row → named fact lift is the engine's `factOf`
316
+ * closed-ref handles included. */
354
317
  function applyOps<Rels extends SchemaRelations>(core: Core<Rels>, ops: readonly Op[]): WriteOutcome<Rels, number> {
355
318
  return core.db.write(function applyBatch(tx) {
356
319
  for (const op of ops) {
357
- const info = core.descriptor.relationByName.get(op.relation)
358
320
  const member = core.theory.relations[op.relation]
359
- if (info === undefined || member === undefined) {
321
+ if (member === undefined) {
360
322
  throw errors.new(`batch op cites unknown relation ${op.relation}`)
361
323
  }
362
324
  const relation = member as MemberRelation<Rels>
363
325
  const facts = op.rows.map(function liftRow(row) {
364
- return factOf(core, info, row)
365
- }) as unknown as Iterable<Fact<MemberRelation<Rels>>>
326
+ return factOf(relation, row)
327
+ })
366
328
  if (op.op === "insert") {
367
329
  tx.insert(relation, facts)
368
330
  } else {
@@ -503,7 +465,7 @@ async function stepBraid<Rels extends SchemaRelations>(
503
465
  return { tag: "tip" }
504
466
  }
505
467
  const chain = core.chain
506
- if (chain.tag === "pending" && id === chain.batch.braid && next === chain.batch.gen) {
468
+ if (chain.tag === "pending" && id === chain.batch.braid && next === chain.batch.slot) {
507
469
  if (!bytesEqual(fetched.bytes, chain.batch.bytes)) {
508
470
  return { tag: "reseed", cause: "lost-pending-fork" }
509
471
  }
@@ -599,16 +561,9 @@ async function adoptManifest<Rels extends SchemaRelations>(
599
561
  if (facts === null) {
600
562
  throw errors.new(`manifest points at absent checkpoint ${hex32(manifest.checkpoint)}`)
601
563
  }
602
- const checkpoint = parseCheckpoint(facts.bytes)
603
- for (const id of checkpoint.braids.keys()) {
604
- braidOf(core.theory, id)
605
- }
606
- const carried = [...checkpoint.braids.keys()].sort()
607
- const derived = [...core.descriptor.braidMembers.keys()].sort()
608
- if (carried.join(",") !== derived.join(",")) {
609
- refuse({ kind: "CheckpointBraids", carried, derived }, "checkpoint braid set drifted from the derived braids")
610
- }
611
- core.checkpoint = checkpoint
564
+ // The codec-backed parseCheckpoint judges the braid set against the
565
+ // sealed handle an unknown or drifted braid refuses at parse.
566
+ core.checkpoint = parseCheckpoint(core.descriptor.codec, facts.bytes)
612
567
  core.checkpointDigest = manifest.checkpoint
613
568
  }
614
569
  // The pointer is adopted only after the checkpoint it names is in
@@ -798,7 +753,7 @@ async function resolvePendingAtOpen<Rels extends SchemaRelations>(core: Core<Rel
798
753
  generationOf(core),
799
754
  null,
800
755
  chain.batch.bytes,
801
- belowFloor(core, chain.batch.braid, chain.batch.gen)
756
+ belowFloor(core, chain.batch.braid, chain.batch.slot)
802
757
  ).tag === "below-floor"
803
758
  ) {
804
759
  await settle(core)
@@ -823,7 +778,7 @@ async function resolvePendingAtOpen<Rels extends SchemaRelations>(core: Core<Rel
823
778
  await settle(core)
824
779
  return
825
780
  }
826
- holdPending(core, chain.batch, decoded.data.ops)
781
+ holdPending(core, chain.batch, decoded.data.ops, decoded.data.header.timestamp)
827
782
  }
828
783
 
829
784
  /**
@@ -837,7 +792,7 @@ async function resolvePendingAtOpen<Rels extends SchemaRelations>(core: Core<Rel
837
792
  async function resolveColdPending<Rels extends SchemaRelations>(core: Core<Rels>, pending: Pending): Promise<void> {
838
793
  const braid = braidOf(core.theory, pending.braid)
839
794
  if (
840
- foldPending(chainSum(core.chain), generationOf(core), null, pending.bytes, belowFloor(core, braid, pending.gen))
795
+ foldPending(chainSum(core.chain), generationOf(core), null, pending.bytes, belowFloor(core, braid, pending.slot))
841
796
  .tag === "below-floor"
842
797
  ) {
843
798
  await settle(core)
@@ -859,7 +814,7 @@ async function resolveColdPending<Rels extends SchemaRelations>(core: Core<Rels>
859
814
  const before = generationOf(core)
860
815
  const outcome = applyOps(core, decoded.data.ops)
861
816
  if (outcome.tag === "accepted" && outcome.value.generation > before) {
862
- holdPending(core, { ...pending, braid }, decoded.data.ops)
817
+ holdPending(core, { ...pending, braid }, decoded.data.ops, decoded.data.header.timestamp)
863
818
  await readdressPending(core, decoded.data.ops, decoded.data.header.writer)
864
819
  }
865
820
  }
@@ -892,23 +847,16 @@ async function openCore<Rels extends SchemaRelations>(options: OpenReplicaOption
892
847
  await adoptManifest(core, fetched.bytes, fetched.etag)
893
848
 
894
849
  const existing = await newestStoreDir(core.dir)
895
- const sidecar = await readSidecar(sidecarPath(core))
850
+ const sidecar = await readSidecar(descriptor.codec, sidecarPath(core))
896
851
  let opened = false
897
852
  let coldPending: Pending | null = null
898
853
  if (sidecar.tag === "fault") {
899
854
  throw wrapStore(sidecar.io, `read sidecar ${sidecarPath(core)}`)
900
855
  }
901
- if (sidecar.tag === "read") {
902
- for (const id of sidecar.chain.entries.keys()) {
903
- braidOf(core.theory, id)
904
- }
905
- if (sidecar.chain.tag === "pending") {
906
- braidOf(core.theory, sidecar.chain.batch.braid)
907
- }
908
- }
909
- if (sidecar.tag === "corrupt") {
910
- coldPending = null
911
- } else if (existing !== null && sidecar.tag === "read") {
856
+ // The codec-backed readSidecar judges every braid against the sealed
857
+ // handle a foreign braid is a corrupt sidecar, and a corrupt
858
+ // sidecar is discarded cache (the disposable law), never adopted.
859
+ if (existing !== null && sidecar.tag === "read") {
912
860
  core.storeName = existing
913
861
  const openedDb = await errors.try(SdkDb.open(storePath(core), options.theory))
914
862
  if (openedDb.error === undefined) {
@@ -921,7 +869,6 @@ async function openCore<Rels extends SchemaRelations>(options: OpenReplicaOption
921
869
  if (!opened) {
922
870
  if (sidecar.tag === "read" && sidecar.chain.tag === "pending") {
923
871
  coldPending = sidecar.chain.batch
924
- braidOf(core.theory, coldPending.braid)
925
872
  }
926
873
  await initializeStore(core)
927
874
  }
@@ -989,7 +936,6 @@ async function readdressPending<Rels extends SchemaRelations>(
989
936
  const bytes = encodeBatch(
990
937
  core.descriptor,
991
938
  {
992
- fingerprint: digest32(core.descriptor.fingerprintBytes),
993
939
  braid,
994
940
  braidGen: generation(entry.g + 1n),
995
941
  prev: entry.prev,
@@ -998,7 +944,7 @@ async function readdressPending<Rels extends SchemaRelations>(
998
944
  },
999
945
  ops
1000
946
  )
1001
- holdPending(core, { braid, gen: generation(entry.g + 1n), bytes }, ops)
947
+ holdPending(core, { braid, slot: generation(entry.g + 1n), bytes }, ops, timestamp)
1002
948
  await persistSidecar(core)
1003
949
  }
1004
950
 
@@ -1024,19 +970,33 @@ async function refreshPass<Rels extends SchemaRelations>(core: Core<Rels>, braid
1024
970
  return outcome
1025
971
  }
1026
972
 
1027
- /** waitFor is refresh with a predicate: the same pass, then the check. */
1028
- async function refreshUntil<Rels extends SchemaRelations>(core: Core<Rels>, predicate: () => boolean): Promise<void> {
973
+ /** waitFor is refresh with a verdict: the same pass, then the full
974
+ * Waited sum. A braid the target needs that is wedged below it returns
975
+ * Wedged — no refresh will ever reach the target — and a heartbeat
976
+ * refusal returns Refused. */
977
+ async function waitForVector<Rels extends SchemaRelations>(
978
+ core: Core<Rels>,
979
+ target: ReadonlyMap<Braid, Generation>
980
+ ): Promise<Waited> {
1029
981
  for (;;) {
1030
982
  disposed(core)
1031
- if (predicate()) {
1032
- return
983
+ const have = vectorOf(core)
984
+ for (const [braid, wanted] of target) {
985
+ const cause = core.wedged.get(braid)
986
+ const at = have.get(braid)
987
+ if (cause !== undefined && at !== undefined && at < wanted) {
988
+ return { tag: "wedged", braid, cause }
989
+ }
1033
990
  }
1034
- await withGate(core, async function waitPass() {
991
+ if (Vector.from(have).dominates(Vector.from(target))) {
992
+ return { tag: "reached", vector: have }
993
+ }
994
+ const outcome = await withGate(core, async function waitPass() {
1035
995
  disposed(core)
1036
- await refreshPass(core)
996
+ return refreshPass(core)
1037
997
  })
1038
- if (predicate()) {
1039
- return
998
+ if (outcome.tag === "refused") {
999
+ return { tag: "refused", detail: outcome.detail }
1040
1000
  }
1041
1001
  await new Promise(function later(resolve) {
1042
1002
  setTimeout(resolve, WAIT_FOR_POLL_MS)
@@ -1062,12 +1022,11 @@ async function openReplica<Rels extends SchemaRelations>(options: OpenReplicaOpt
1062
1022
  })
1063
1023
  },
1064
1024
  async waitFor(vector) {
1065
- for (const braid of vector.keys()) {
1066
- chainEntry(core, braid)
1025
+ const target = new Map<Braid, Generation>()
1026
+ for (const [braid, wanted] of vector) {
1027
+ target.set(braidOf(core.theory, braid), wanted)
1067
1028
  }
1068
- await refreshUntil(core, function reached() {
1069
- return Vector.from(vectorOf(core)).dominates(Vector.from(vector))
1070
- })
1029
+ return waitForVector(core, target)
1071
1030
  },
1072
1031
  async [Symbol.asyncDispose]() {
1073
1032
  await withGate(core, async function disposeBody() {
@@ -1080,14 +1039,11 @@ async function openReplica<Rels extends SchemaRelations>(options: OpenReplicaOpt
1080
1039
  return replica
1081
1040
  }
1082
1041
 
1083
- export type { ApplyPhase, Core, OpenReplicaOptions, PendingFold, RefreshOutcome, Replica, ReplicaState, SlotApply }
1042
+ export type { Core, OpenReplicaOptions, RefreshOutcome, Replica, Waited }
1084
1043
  export {
1085
1044
  applyOps,
1086
- applySlot,
1087
1045
  belowFloor,
1088
- blake3Hex,
1089
1046
  chainEntry,
1090
- chainOf,
1091
1047
  clearPending,
1092
1048
  coreOf,
1093
1049
  discardAndReopen,
@@ -1100,8 +1056,6 @@ export {
1100
1056
  pendingOf,
1101
1057
  persistSidecar,
1102
1058
  readdressPending,
1103
- vectorOf,
1104
- wholenessHolds,
1105
1059
  withGate,
1106
1060
  ZERO_HASH
1107
1061
  }