@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjornpagen/bumbledb-log",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "Braided object-store replication for bumbledb: the command codec, theory-derived braids, and replica/writer over five store verbs",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,7 +37,7 @@
37
37
  "arkregex": "^0.0.8"
38
38
  },
39
39
  "peerDependencies": {
40
- "@bjornpagen/bumbledb": "^0.19.2"
40
+ "@bjornpagen/bumbledb": "^0.20.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@biomejs/biome": "2.5.4",
package/src/braids.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Braid derivation as data (10): connected components of the statement
3
- * graph over ordinary relations, the braid id the smallest RelationId in
4
- * the component rendered `c{id:08x}`. Assignment is a pure function of
5
- * the descriptor, pinned cross-language by the codec goldens.
2
+ * Braid derivation as data (10/20): connected components of the
3
+ * statement graph over ordinary relations, the braid id the smallest
4
+ * RelationId in the component rendered `c{id:08x}`. The one derivation
5
+ * lives in `crates/bumbledb-log` and reaches the descriptor parse
6
+ * through the engine bridge (`internalLogBraidsOf`); this façade names
7
+ * that derivation in the driver's vocabulary.
6
8
  */
7
9
 
8
10
  import type { Braid, SerialStatement, Theory } from "#descriptor.ts"
@@ -26,8 +28,8 @@ function braidsOf(theory: Theory): ReadonlyMap<string, Braid> {
26
28
  /**
27
29
  * The degenerate-serial roster (15): key or capacity statements whose
28
30
  * determinant projection is empty name one global group, so their braid
29
- * serializes at that statement. Typed data beside the braid map, one
30
- * question per verb.
31
+ * serializes at that statement. The statement ids are the log core's
32
+ * own roster, read off the descriptor's derivation.
31
33
  */
32
34
  function serialAtStatementsOf(theory: Theory): readonly SerialStatement[] {
33
35
  return descriptorOf(theory).serialAtStatements
package/src/bytes.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  /**
2
- * Little-endian byte primitives shared by the codec and the
3
- * fingerprint mirror, plus the hex grammar every document digest
4
- * walks. Every multi-byte integer on the batch wire is little-endian;
5
- * the fingerprint's canonical literal encoding is the engine's
6
- * big-endian order-preserving form; a digest is 32 bytes, rendered as
7
- * 64 lowercase hex characters. Integer order and hex width live here
8
- * so no third spelling can appear.
2
+ * Byte primitives beside the one grammar: the hex spelling every
3
+ * document digest walks (32 bytes, 64 lowercase hex characters), the
4
+ * `Digest32` brand, byte equality, and the u64 arithmetic the vector
5
+ * and lease algebras share. The wire's own cursors live in
6
+ * `crates/bumbledb-log`; nothing here reads or writes protocol bytes.
9
7
  */
10
8
 
11
9
  import * as errors from "@superbuilders/errors"
12
10
 
13
11
  const U64_MAX = 0xffffffffffffffffn
14
- const I64_MIN = -0x8000000000000000n
15
- const I64_MAX = 0x7fffffffffffffffn
16
- const I64_SIGN_BIT = 0x8000000000000000n
17
12
 
18
13
  const utf8Encoder = new TextEncoder()
19
14
  /** Fatal UTF-8. ignoreBOM is true: a leading U+FEFF is a character, not a stripped BOM. */
@@ -22,174 +17,6 @@ const utf8StrictDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: tru
22
17
  declare const digest32Brand: unique symbol
23
18
  type Digest32 = Uint8Array & { readonly [digest32Brand]: typeof digest32Brand }
24
19
 
25
- class ByteWriter {
26
- private buf: Uint8Array
27
- private len = 0
28
-
29
- constructor(capacity = 256) {
30
- this.buf = new Uint8Array(capacity)
31
- }
32
-
33
- private grow(need: number): void {
34
- if (this.len + need <= this.buf.length) {
35
- return
36
- }
37
- let capacity = this.buf.length * 2
38
- while (capacity < this.len + need) {
39
- capacity *= 2
40
- }
41
- const next = new Uint8Array(capacity)
42
- next.set(this.buf.subarray(0, this.len))
43
- this.buf = next
44
- }
45
-
46
- u8(value: number): void {
47
- this.grow(1)
48
- this.buf[this.len] = value
49
- this.len += 1
50
- }
51
-
52
- bytes(raw: Uint8Array): void {
53
- this.grow(raw.length)
54
- this.buf.set(raw, this.len)
55
- this.len += raw.length
56
- }
57
-
58
- array32(value: Digest32): void {
59
- this.bytes(value)
60
- }
61
-
62
- u16le(value: number): void {
63
- this.grow(2)
64
- this.buf[this.len] = value & 0xff
65
- this.buf[this.len + 1] = (value >>> 8) & 0xff
66
- this.len += 2
67
- }
68
-
69
- u32le(value: number): void {
70
- this.grow(4)
71
- this.buf[this.len] = value & 0xff
72
- this.buf[this.len + 1] = (value >>> 8) & 0xff
73
- this.buf[this.len + 2] = (value >>> 16) & 0xff
74
- this.buf[this.len + 3] = (value >>> 24) & 0xff
75
- this.len += 4
76
- }
77
-
78
- u64le(value: bigint): void {
79
- if (value < 0n || value > U64_MAX) {
80
- throw errors.new(`u64 out of range: ${value}`)
81
- }
82
- this.grow(8)
83
- let v = value
84
- for (let i = 0; i < 8; i++) {
85
- this.buf[this.len + i] = Number(v & 0xffn)
86
- v >>= 8n
87
- }
88
- this.len += 8
89
- }
90
-
91
- i64le(value: bigint): void {
92
- if (value < I64_MIN || value > I64_MAX) {
93
- throw errors.new(`i64 out of range: ${value}`)
94
- }
95
- this.u64le(value & U64_MAX)
96
- }
97
-
98
- u64be(value: bigint): void {
99
- if (value < 0n || value > U64_MAX) {
100
- throw errors.new(`u64 out of range: ${value}`)
101
- }
102
- this.grow(8)
103
- let v = value
104
- for (let i = 7; i >= 0; i--) {
105
- this.buf[this.len + i] = Number(v & 0xffn)
106
- v >>= 8n
107
- }
108
- this.len += 8
109
- }
110
-
111
- /** The engine's sign-flipped big-endian i64 (lexicographic = numeric). */
112
- i64beFlipped(value: bigint): void {
113
- if (value < I64_MIN || value > I64_MAX) {
114
- throw errors.new(`i64 out of range: ${value}`)
115
- }
116
- this.u64be((value & U64_MAX) ^ I64_SIGN_BIT)
117
- }
118
-
119
- finish(): Uint8Array {
120
- return this.buf.slice(0, this.len)
121
- }
122
- }
123
-
124
- interface ReadFailure {
125
- fail(what: string): never
126
- }
127
-
128
- class ByteReader {
129
- private readonly buf: Uint8Array
130
- private pos = 0
131
- private readonly refusal: ReadFailure
132
-
133
- constructor(buf: Uint8Array, refusal: ReadFailure) {
134
- this.buf = buf
135
- this.refusal = refusal
136
- }
137
-
138
- remaining(): number {
139
- return this.buf.length - this.pos
140
- }
141
-
142
- private take(count: number, what: string): Uint8Array {
143
- if (this.pos + count > this.buf.length) {
144
- this.refusal.fail(what)
145
- }
146
- const out = this.buf.subarray(this.pos, this.pos + count)
147
- this.pos += count
148
- return out
149
- }
150
-
151
- u8(what: string): number {
152
- const raw = this.take(1, what)
153
- const byte = raw[0]
154
- if (byte === undefined) {
155
- this.refusal.fail(what)
156
- }
157
- return byte
158
- }
159
-
160
- bytes(count: number, what: string): Uint8Array {
161
- return new Uint8Array(this.take(count, what))
162
- }
163
-
164
- array32(what: string): Digest32 {
165
- return digest32(this.take(32, what))
166
- }
167
-
168
- u16le(what: string): number {
169
- const raw = this.take(2, what)
170
- return (raw[0] ?? 0) | ((raw[1] ?? 0) << 8)
171
- }
172
-
173
- u32le(what: string): number {
174
- const raw = this.take(4, what)
175
- return (((raw[0] ?? 0) | ((raw[1] ?? 0) << 8) | ((raw[2] ?? 0) << 16)) + (raw[3] ?? 0) * 0x1000000) >>> 0
176
- }
177
-
178
- u64le(what: string): bigint {
179
- const raw = this.take(8, what)
180
- let value = 0n
181
- for (let i = 7; i >= 0; i--) {
182
- value = (value << 8n) | BigInt(raw[i] ?? 0)
183
- }
184
- return value
185
- }
186
-
187
- i64le(what: string): bigint {
188
- const unsigned = this.u64le(what)
189
- return unsigned > I64_MAX ? unsigned - (U64_MAX + 1n) : unsigned
190
- }
191
- }
192
-
193
20
  function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
194
21
  if (a.length !== b.length) {
195
22
  return false
@@ -202,17 +29,6 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
202
29
  return true
203
30
  }
204
31
 
205
- function bytesCompare(a: Uint8Array, b: Uint8Array): number {
206
- const shared = Math.min(a.length, b.length)
207
- for (let i = 0; i < shared; i++) {
208
- const delta = (a[i] ?? 0) - (b[i] ?? 0)
209
- if (delta !== 0) {
210
- return delta
211
- }
212
- }
213
- return a.length - b.length
214
- }
215
-
216
32
  const HEX_DIGITS = "0123456789abcdef"
217
33
 
218
34
  function hexNibble(byte: number): number | undefined {
@@ -285,17 +101,12 @@ function checkedAddU64(a: bigint, b: bigint): bigint | undefined {
285
101
 
286
102
  export type { Digest32 }
287
103
  export {
288
- ByteReader,
289
- ByteWriter,
290
- bytesCompare,
291
104
  bytesEqual,
292
105
  checkedAddU64,
293
106
  digest32,
294
107
  digest32FromHex,
295
108
  fromHex,
296
109
  hex32,
297
- I64_MAX,
298
- I64_MIN,
299
110
  saturatingAddU64,
300
111
  toHex,
301
112
  U64_MAX,
package/src/chain.ts CHANGED
@@ -1,19 +1,21 @@
1
1
  /**
2
- * The chain sidecar in `dir/chain`: a floor cache of chain
3
- * position, written atomically (temp + rename, fsync). The chain is
4
- * Settled or Pending — generation is the vector sum, plus one exactly
5
- * when the value is Pending. The document is a binary v:3 record:
6
- * version byte 3, counted roster of braid / g / prev / ts, pending
7
- * tag. Wire pending is the batch bytes. The content address is blake3
8
- * of those bytes. Every integer is little-endian.
2
+ * The chain sidecar in `dir/chain`: a floor cache of chain position,
3
+ * written atomically (temp + rename, fsync). The chain is Settled or
4
+ * Pending — generation is the vector sum, plus one exactly when the
5
+ * value is Pending. The byte grammar has one implementation
6
+ * `crates/bumbledb-log` behind the napi bridge so parse and render
7
+ * are marshal walks over the sealed codec handle; the file IO half
8
+ * lives here. The content address is blake3 of the rendered bytes.
9
9
  */
10
10
 
11
11
  import * as crypto from "node:crypto"
12
12
  import * as fs from "node:fs/promises"
13
13
  import * as path from "node:path"
14
+ import type { LogChain, LogCodecHandle, LogSidecarKind } from "@bjornpagen/bumbledb"
15
+ import { internalLogParseSidecar, internalLogRenderSidecar } from "@bjornpagen/bumbledb"
14
16
  import * as errors from "@superbuilders/errors"
15
- import { ByteReader, ByteWriter, saturatingAddU64, U64_MAX } from "#bytes.ts"
16
- import type { ChainEntry } from "#codec.ts"
17
+ import type { Digest32 } from "#bytes.ts"
18
+ import { digest32, saturatingAddU64, U64_MAX } from "#bytes.ts"
17
19
  import type { Braid } from "#descriptor.ts"
18
20
  import { braidHex } from "#descriptor.ts"
19
21
  import { refuse } from "#errors.ts"
@@ -24,15 +26,17 @@ import { Vector } from "#vector.ts"
24
26
  /** The sidecar's file name inside a replica directory. */
25
27
  const CHAIN_FILE = "chain"
26
28
 
27
- const VERSION = 3
28
- const SETTLED = 0
29
- const PENDING = 1
30
- /** u32 braid + u64 g + 32 prev + u64 ts. */
31
- const ENTRY_BYTES = 52n
29
+ /** One braid's chain coordinate: the applied count, the applied
30
+ * batch's content address, its timestamp. */
31
+ interface ChainEntry {
32
+ readonly g: Generation
33
+ readonly prev: Digest32
34
+ readonly ts: bigint
35
+ }
32
36
 
33
37
  interface Pending {
34
38
  readonly braid: Braid
35
- readonly gen: Generation
39
+ readonly slot: Generation
36
40
  readonly bytes: Uint8Array
37
41
  }
38
42
 
@@ -72,114 +76,66 @@ function chainGeneration(chain: Chain): bigint {
72
76
  return chain.tag === "settled" ? sum : saturatingAddU64(sum, 1n)
73
77
  }
74
78
 
75
- /** A declared count the remaining bytes cannot open is Malformed
76
- * before the loop. */
77
- function refuseUnbacked(count: bigint, remaining: number, minItem: bigint, at: string): void {
78
- if (count === 0n) {
79
- return
80
- }
81
- if (minItem === 0n || BigInt(remaining) / minItem < count) {
82
- refuse({ kind: "Malformed", at: remaining }, `declared ${at} ${count} outruns the remaining ${remaining} bytes`)
79
+ /**
80
+ * Remints a bridge refusal row as the driver's typed refusal. The
81
+ * boundary carries `{ kind, message }` only: the kind is the log
82
+ * core's own identity string, so the cause payload holds the data this
83
+ * side owns — the document's length, its version byte (byte 0 of every
84
+ * v:3 document). The raw braid id of an `UnknownBraid` rides the
85
+ * message.
86
+ */
87
+ function refuseBridged(kind: LogSidecarKind, message: string, bytes: Uint8Array): never {
88
+ switch (kind) {
89
+ case "Version":
90
+ return refuse({ kind: "Version", version: bytes[0] ?? 0 }, message)
91
+ case "Overflow":
92
+ return refuse({ kind: "Overflow" }, message)
93
+ case "UnknownBraid":
94
+ return refuse({ kind: "UnknownBraid" }, message)
95
+ case "Malformed":
96
+ return refuse({ kind: "Malformed", at: bytes.length }, message)
83
97
  }
84
98
  }
85
99
 
86
- function renderSidecar(chain: Chain): Uint8Array {
87
- const out = new ByteWriter(64)
88
- out.u8(VERSION)
89
- const braids = [...chain.entries.keys()].sort()
90
- if (braids.length > 0xffffffff) {
91
- throw errors.new("sidecar chain count exceeds u32")
92
- }
93
- out.u32le(braids.length)
94
- for (const id of braids) {
95
- const entry = chain.entries.get(id)
96
- if (entry === undefined) {
97
- throw errors.new(`sidecar chain lost braid ${id}`)
98
- }
99
- out.u32le(braidIdOf(id))
100
- out.u64le(entry.g)
101
- out.bytes(entry.prev)
102
- out.u64le(entry.ts)
103
- }
104
- if (chain.tag === "settled") {
105
- out.u8(SETTLED)
106
- return out.finish()
107
- }
108
- out.u8(PENDING)
109
- out.u32le(braidIdOf(chain.batch.braid))
110
- out.u64le(chain.batch.gen)
111
- if (chain.batch.bytes.length > 0xffffffff) {
112
- throw errors.new("sidecar pending exceeds u32 length")
113
- }
114
- out.u32le(chain.batch.bytes.length)
115
- out.bytes(chain.batch.bytes)
116
- return out.finish()
100
+ function renderSidecar(codec: LogCodecHandle, chain: Chain): Uint8Array {
101
+ const entries = [...chain.entries.entries()]
102
+ .sort(function ascending(a, b) {
103
+ return braidIdOf(a[0]) - braidIdOf(b[0])
104
+ })
105
+ .map(function entryOf([id, entry]) {
106
+ return { braid: braidIdOf(id), g: entry.g, prev: entry.prev, ts: entry.ts }
107
+ })
108
+ const doc: LogChain =
109
+ chain.tag === "settled"
110
+ ? { entries }
111
+ : {
112
+ entries,
113
+ pending: { braid: braidIdOf(chain.batch.braid), slot: chain.batch.slot, bytes: chain.batch.bytes }
114
+ }
115
+ return internalLogRenderSidecar(codec, doc)
117
116
  }
118
117
 
119
- function parseSidecar(bytes: Uint8Array, known?: ReadonlySet<Braid>): Chain {
120
- const reader = new ByteReader(bytes, {
121
- fail(what: string): never {
122
- refuse({ kind: "Malformed", at: bytes.length }, `sidecar truncated at ${what}`)
123
- }
124
- })
125
- const at = function offset(): number {
126
- return bytes.length - reader.remaining()
127
- }
128
- const version = reader.u8("version")
129
- if (version !== VERSION) {
130
- refuse({ kind: "Version", version }, `sidecar version ${version}, consumers refuse ≠ ${VERSION}`)
118
+ function parseSidecar(codec: LogCodecHandle, bytes: Uint8Array): Chain {
119
+ const parsed = internalLogParseSidecar(codec, bytes)
120
+ if (!parsed.ok) {
121
+ refuseBridged(parsed.kind, parsed.message, bytes)
131
122
  }
132
- const count = BigInt(reader.u32le("chain count"))
133
- refuseUnbacked(count, reader.remaining(), ENTRY_BYTES, "chain count")
134
123
  const entries = new Map<Braid, ChainEntry>()
135
- let last: Braid | undefined
136
- for (let i = 0n; i < count; i++) {
137
- const raw = reader.u32le("braid")
138
- const name = braidHex(raw)
139
- if (known !== undefined && !known.has(name)) {
140
- refuse({ kind: "UnknownBraid", braid: raw }, `sidecar cites unknown braid ${name}`)
141
- }
142
- const g = reader.u64le("g")
143
- const prev = reader.array32("prev")
144
- const ts = reader.u64le("ts")
145
- if (last !== undefined && last >= name) {
146
- refuse({ kind: "Malformed", at: at() }, "sidecar chain is not strictly ascending")
147
- }
148
- entries.set(name, { g: generation(g), prev, ts })
149
- last = name
150
- }
151
- if (typeof vectorOf(entries).sum() !== "bigint") {
152
- refuse({ kind: "Overflow" }, "sidecar chain sum overflows u64")
124
+ for (const entry of parsed.value.entries) {
125
+ entries.set(braidHex(entry.braid), { g: generation(entry.g), prev: digest32(entry.prev), ts: entry.ts })
153
126
  }
154
- const tag = reader.u8("pending")
155
- if (tag === SETTLED) {
156
- if (reader.remaining() !== 0) {
157
- refuse({ kind: "Malformed", at: reader.remaining() }, `${reader.remaining()} trailing bytes after the sidecar`)
158
- }
127
+ const pending = parsed.value.pending
128
+ if (pending === undefined) {
159
129
  return { tag: "settled", entries }
160
130
  }
161
- if (tag !== PENDING) {
162
- refuse({ kind: "Malformed", at: at() - 1 }, `sidecar pending tag ${tag}`)
163
- }
164
- const raw = reader.u32le("pending braid")
165
- const name = braidHex(raw)
166
- if (known !== undefined && !known.has(name)) {
167
- refuse({ kind: "UnknownBraid", braid: raw }, `sidecar pending cites unknown braid ${name}`)
168
- }
169
- const slot = reader.u64le("pending generation")
170
- const length = reader.u32le("pending length")
171
- const body = reader.bytes(length, "pending bytes")
172
- if (reader.remaining() !== 0) {
173
- refuse({ kind: "Malformed", at: reader.remaining() }, `${reader.remaining()} trailing bytes after the sidecar`)
174
- }
175
131
  return {
176
132
  tag: "pending",
177
133
  entries,
178
- batch: { braid: name, gen: generation(slot), bytes: body }
134
+ batch: { braid: braidHex(pending.braid), slot: generation(pending.slot), bytes: pending.bytes }
179
135
  }
180
136
  }
181
137
 
182
- async function readSidecar(file: string, known?: ReadonlySet<Braid>): Promise<SidecarRead> {
138
+ async function readSidecar(codec: LogCodecHandle, file: string): Promise<SidecarRead> {
183
139
  const read = await errors.try(fs.readFile(file))
184
140
  if (read.error) {
185
141
  if (codeOf(read.error) === "ENOENT") {
@@ -188,7 +144,7 @@ async function readSidecar(file: string, known?: ReadonlySet<Braid>): Promise<Si
188
144
  return { tag: "fault", io: read.error }
189
145
  }
190
146
  const parsed = errors.trySync(function parse() {
191
- return parseSidecar(read.data, known)
147
+ return parseSidecar(codec, read.data)
192
148
  })
193
149
  if (parsed.error) {
194
150
  return { tag: "corrupt", parse: parsed.error }
@@ -196,14 +152,14 @@ async function readSidecar(file: string, known?: ReadonlySet<Braid>): Promise<Si
196
152
  return { tag: "read", chain: parsed.data }
197
153
  }
198
154
 
199
- async function writeSidecar(file: string, chain: Chain): Promise<void> {
155
+ async function writeSidecar(codec: LogCodecHandle, file: string, chain: Chain): Promise<void> {
200
156
  const dir = path.dirname(file)
201
157
  await fs.mkdir(dir, { recursive: true })
202
158
  const temp = path.join(dir, `.chain-${process.pid}-${crypto.randomBytes(4).toString("hex")}`)
203
159
  const handle = await fs.open(temp, "wx")
204
160
  const written = await errors.try(
205
161
  (async function writeAll() {
206
- await handle.writeFile(renderSidecar(chain))
162
+ await handle.writeFile(renderSidecar(codec, chain))
207
163
  await handle.sync()
208
164
  })()
209
165
  )
@@ -221,5 +177,5 @@ async function writeSidecar(file: string, chain: Chain): Promise<void> {
221
177
  }
222
178
  }
223
179
 
224
- export type { Chain, ChainEntry, Pending, SidecarRead }
180
+ export type { Chain, ChainEntry, Pending }
225
181
  export { CHAIN_FILE, chainGeneration, chainSum, parseSidecar, readSidecar, renderSidecar, writeSidecar }