@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/package.json +2 -2
- package/src/braids.ts +8 -6
- package/src/bytes.ts +5 -194
- package/src/chain.ts +67 -111
- package/src/codec.ts +150 -233
- package/src/descriptor.ts +67 -612
- package/src/errors.ts +91 -62
- package/src/index.ts +25 -21
- package/src/keys.ts +49 -62
- package/src/manifest.ts +81 -129
- package/src/replica.ts +82 -128
- package/src/store-s3.ts +1 -1
- package/src/store.ts +245 -229
- package/src/tenants.ts +19 -41
- package/src/vector.ts +5 -75
- package/src/writer.ts +275 -168
- package/src/value.ts +0 -336
package/src/manifest.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The protocol's one mutable object
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 (
|
|
164
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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 {
|
|
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
|
|
209
|
-
export { auditCatalog, checkpointVector,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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<
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
225
|
-
* Option beside the vector. Wire
|
|
226
|
-
* the in-memory
|
|
227
|
-
|
|
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>(
|
|
230
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
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 (
|
|
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(
|
|
365
|
-
})
|
|
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.
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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,
|
|
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
|
|
1028
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
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
|
-
|
|
996
|
+
return refreshPass(core)
|
|
1037
997
|
})
|
|
1038
|
-
if (
|
|
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
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|