@bjornpagen/bumbledb-log 0.18.0 → 0.19.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/README.md +5 -4
- package/package.json +4 -3
- package/src/braids.ts +17 -2
- package/src/bytes.ts +78 -10
- package/src/chain.ts +173 -56
- package/src/codec.ts +122 -41
- package/src/descriptor.ts +182 -107
- package/src/errors.ts +133 -5
- package/src/index.ts +15 -2
- package/src/keys.ts +161 -18
- package/src/manifest.ts +165 -73
- package/src/replica.ts +577 -264
- package/src/store-s3.ts +191 -36
- package/src/store.ts +459 -85
- package/src/tenants.ts +322 -26
- package/src/value.ts +65 -14
- package/src/vector.ts +173 -0
- package/src/writer.ts +453 -92
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @bjornpagen/bumbledb-log
|
|
2
2
|
|
|
3
3
|
Braided object-store replication for [bumbledb](https://github.com/bjornpagen/bumbledb):
|
|
4
|
-
a thin peer of `@bjornpagen/bumbledb` (peer `^0.
|
|
4
|
+
a thin peer of `@bjornpagen/bumbledb` (peer `^0.19.0`). The package is
|
|
5
5
|
three things:
|
|
6
6
|
|
|
7
7
|
1. **The mirrored pure pair**, byte-exact against the Rust driver and
|
|
@@ -40,7 +40,7 @@ Async ⟺ network: `openReplica`, `refresh`, `waitFor`, `commit`,
|
|
|
40
40
|
import { fsStore, openReplica, openWriter } from "@bjornpagen/bumbledb-log"
|
|
41
41
|
|
|
42
42
|
export const replica = await openReplica({ store: s3(env), prefix: "prod/main", dir: "/tmp/store", theory: Ledger })
|
|
43
|
-
export const writer = openWriter(replica)
|
|
43
|
+
export const writer = await openWriter(replica)
|
|
44
44
|
|
|
45
45
|
// route handler
|
|
46
46
|
const out = await writer.commit((batch) => batch.insert(Booking, [row]))
|
|
@@ -78,7 +78,7 @@ const replica = await openReplica({
|
|
|
78
78
|
dir: `/data/primer/replicas/${scopeName}`, // per-process local dir — never shared
|
|
79
79
|
theory: Explanation
|
|
80
80
|
})
|
|
81
|
-
const writer = openWriter(replica)
|
|
81
|
+
const writer = await openWriter(replica)
|
|
82
82
|
|
|
83
83
|
// one pass = refresh, render, emit, lower, one commit
|
|
84
84
|
await replica.refresh()
|
|
@@ -116,7 +116,8 @@ of the object file and its parent directory.
|
|
|
116
116
|
Exported sentinel values on the SDK idiom, checked with `errors.is`,
|
|
117
117
|
never by message strings: `ErrRefused` (typed per cause — batch shape,
|
|
118
118
|
version, fingerprint, manifest shape, checkpoint braid-set drift),
|
|
119
|
-
`
|
|
119
|
+
`ErrManifestMissing` (a replica found no manifest; only the writer
|
|
120
|
+
births a store), `ErrSpanningCommit`, `ErrGapDetected`, `ErrReplayDiverged`,
|
|
120
121
|
`ErrChainMismatch` (cause `"prev" | "slot" |
|
|
121
122
|
"timestamp"`), `ErrContention` (cause `hot-key` or `slot-race`),
|
|
122
123
|
`ErrStore` (the vendor channel, present in every wrapped store
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bjornpagen/bumbledb-log",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": {
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
"homepage": "https://github.com/bjornpagen/bumbledb#readme",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@aws-sdk/client-s3": "^3.1116.0",
|
|
36
|
-
"@superbuilders/errors": "^4.0.2"
|
|
36
|
+
"@superbuilders/errors": "^4.0.2",
|
|
37
|
+
"arkregex": "^0.0.8"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"@bjornpagen/bumbledb": "^0.
|
|
40
|
+
"@bjornpagen/bumbledb": "^0.19.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@biomejs/biome": "2.5.4",
|
package/src/braids.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Braid, SerialStatement, Theory } from "#descriptor.ts"
|
|
9
|
-
import { braid, descriptorOf } from "#descriptor.ts"
|
|
9
|
+
import { braid, braidHex, descriptorOf } from "#descriptor.ts"
|
|
10
|
+
|
|
11
|
+
const U32_MAX = 0xffffffff
|
|
10
12
|
|
|
11
13
|
/** The schema's own shard map: ordinary relation name → braid id. */
|
|
12
14
|
function braidsOf(theory: Theory): ReadonlyMap<string, Braid> {
|
|
@@ -31,5 +33,18 @@ function serialAtStatementsOf(theory: Theory): readonly SerialStatement[] {
|
|
|
31
33
|
return descriptorOf(theory).serialAtStatements
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Parses a wire u32 into a braid id: valid only when the relation it
|
|
38
|
+
* names is the smallest in its own component. An unknown, closed, or
|
|
39
|
+
* non-head id is not a braid — the caller refuses, it is not ignored.
|
|
40
|
+
*/
|
|
41
|
+
function parse(theory: Theory, raw: number): Braid | undefined {
|
|
42
|
+
if (!Number.isInteger(raw) || raw < 0 || raw > U32_MAX) {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
const name = braidHex(raw)
|
|
46
|
+
return descriptorOf(theory).braidMembers.has(name) ? name : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
export type { Braid }
|
|
35
|
-
export { braid, braidsOf, serialAtStatementsOf }
|
|
50
|
+
export { braid, braidsOf, parse, serialAtStatementsOf }
|
package/src/bytes.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Little-endian byte primitives shared by the codec and the
|
|
3
|
-
* fingerprint mirror
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import * as errors from "@superbuilders/errors"
|
|
@@ -13,6 +15,13 @@ const I64_MIN = -0x8000000000000000n
|
|
|
13
15
|
const I64_MAX = 0x7fffffffffffffffn
|
|
14
16
|
const I64_SIGN_BIT = 0x8000000000000000n
|
|
15
17
|
|
|
18
|
+
const utf8Encoder = new TextEncoder()
|
|
19
|
+
/** Fatal UTF-8. ignoreBOM is true: a leading U+FEFF is a character, not a stripped BOM. */
|
|
20
|
+
const utf8StrictDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true })
|
|
21
|
+
|
|
22
|
+
declare const digest32Brand: unique symbol
|
|
23
|
+
type Digest32 = Uint8Array & { readonly [digest32Brand]: typeof digest32Brand }
|
|
24
|
+
|
|
16
25
|
class ByteWriter {
|
|
17
26
|
private buf: Uint8Array
|
|
18
27
|
private len = 0
|
|
@@ -46,6 +55,10 @@ class ByteWriter {
|
|
|
46
55
|
this.len += raw.length
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
array32(value: Digest32): void {
|
|
59
|
+
this.bytes(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
u16le(value: number): void {
|
|
50
63
|
this.grow(2)
|
|
51
64
|
this.buf[this.len] = value & 0xff
|
|
@@ -148,6 +161,10 @@ class ByteReader {
|
|
|
148
161
|
return new Uint8Array(this.take(count, what))
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
array32(what: string): Digest32 {
|
|
165
|
+
return digest32(this.take(32, what))
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
u16le(what: string): number {
|
|
152
169
|
const raw = this.take(2, what)
|
|
153
170
|
return (raw[0] ?? 0) | ((raw[1] ?? 0) << 8)
|
|
@@ -198,6 +215,16 @@ function bytesCompare(a: Uint8Array, b: Uint8Array): number {
|
|
|
198
215
|
|
|
199
216
|
const HEX_DIGITS = "0123456789abcdef"
|
|
200
217
|
|
|
218
|
+
function hexNibble(byte: number): number | undefined {
|
|
219
|
+
if (byte >= 0x30 && byte <= 0x39) {
|
|
220
|
+
return byte - 0x30
|
|
221
|
+
}
|
|
222
|
+
if (byte >= 0x61 && byte <= 0x66) {
|
|
223
|
+
return byte - 0x61 + 10
|
|
224
|
+
}
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
function toHex(bytes: Uint8Array): string {
|
|
202
229
|
let out = ""
|
|
203
230
|
for (const byte of bytes) {
|
|
@@ -208,27 +235,68 @@ function toHex(bytes: Uint8Array): string {
|
|
|
208
235
|
}
|
|
209
236
|
|
|
210
237
|
function fromHex(hex: string): Uint8Array {
|
|
211
|
-
|
|
238
|
+
const raw = utf8Encoder.encode(hex)
|
|
239
|
+
if (raw.length % 2 !== 0) {
|
|
212
240
|
throw errors.new(`not lowercase hex: ${hex}`)
|
|
213
241
|
}
|
|
214
|
-
const out = new Uint8Array(
|
|
215
|
-
for (let i = 0; i <
|
|
216
|
-
|
|
242
|
+
const out = new Uint8Array(raw.length / 2)
|
|
243
|
+
for (let i = 0, j = 0; i < raw.length; i += 2, j++) {
|
|
244
|
+
const hiByte = raw[i]
|
|
245
|
+
const loByte = raw[i + 1]
|
|
246
|
+
if (hiByte === undefined || loByte === undefined) {
|
|
247
|
+
throw errors.new(`not lowercase hex: ${hex}`)
|
|
248
|
+
}
|
|
249
|
+
const hi = hexNibble(hiByte)
|
|
250
|
+
const lo = hexNibble(loByte)
|
|
251
|
+
if (hi === undefined || lo === undefined) {
|
|
252
|
+
throw errors.new(`not lowercase hex: ${hex}`)
|
|
253
|
+
}
|
|
254
|
+
out[j] = (hi << 4) | lo
|
|
217
255
|
}
|
|
218
256
|
return out
|
|
219
257
|
}
|
|
220
258
|
|
|
221
|
-
|
|
222
|
-
|
|
259
|
+
function digest32(bytes: Uint8Array): Digest32 {
|
|
260
|
+
if (bytes.length !== 32) {
|
|
261
|
+
throw errors.new(`digest is not 32 bytes: ${bytes.length}`)
|
|
262
|
+
}
|
|
263
|
+
const out = new Uint8Array(32)
|
|
264
|
+
out.set(bytes)
|
|
265
|
+
return out as Digest32
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function digest32FromHex(hex: string): Digest32 {
|
|
269
|
+
return digest32(fromHex(hex))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function hex32(bytes: Digest32): string {
|
|
273
|
+
return toHex(bytes)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function saturatingAddU64(a: bigint, b: bigint): bigint {
|
|
277
|
+
const sum = a + b
|
|
278
|
+
return sum > U64_MAX ? U64_MAX : sum
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function checkedAddU64(a: bigint, b: bigint): bigint | undefined {
|
|
282
|
+
const sum = a + b
|
|
283
|
+
return sum > U64_MAX ? undefined : sum
|
|
284
|
+
}
|
|
223
285
|
|
|
286
|
+
export type { Digest32 }
|
|
224
287
|
export {
|
|
225
288
|
ByteReader,
|
|
226
289
|
ByteWriter,
|
|
227
290
|
bytesCompare,
|
|
228
291
|
bytesEqual,
|
|
292
|
+
checkedAddU64,
|
|
293
|
+
digest32,
|
|
294
|
+
digest32FromHex,
|
|
229
295
|
fromHex,
|
|
296
|
+
hex32,
|
|
230
297
|
I64_MAX,
|
|
231
298
|
I64_MIN,
|
|
299
|
+
saturatingAddU64,
|
|
232
300
|
toHex,
|
|
233
301
|
U64_MAX,
|
|
234
302
|
utf8Encoder,
|
package/src/chain.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chain sidecar
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import * as crypto from "node:crypto"
|
|
11
12
|
import * as fs from "node:fs/promises"
|
|
12
13
|
import * as path from "node:path"
|
|
13
14
|
import * as errors from "@superbuilders/errors"
|
|
14
|
-
import {
|
|
15
|
+
import { ByteReader, ByteWriter, saturatingAddU64, U64_MAX } from "#bytes.ts"
|
|
15
16
|
import type { ChainEntry } from "#codec.ts"
|
|
16
17
|
import type { Braid } from "#descriptor.ts"
|
|
17
|
-
import {
|
|
18
|
+
import { braidHex } from "#descriptor.ts"
|
|
19
|
+
import { refuse } from "#errors.ts"
|
|
18
20
|
import type { Generation } from "#keys.ts"
|
|
19
21
|
import { generation } from "#keys.ts"
|
|
22
|
+
import { Vector } from "#vector.ts"
|
|
23
|
+
|
|
24
|
+
/** The sidecar's file name inside a replica directory. */
|
|
25
|
+
const CHAIN_FILE = "chain"
|
|
26
|
+
|
|
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
|
|
20
32
|
|
|
21
33
|
interface Pending {
|
|
22
34
|
readonly braid: Braid
|
|
@@ -24,69 +36,174 @@ interface Pending {
|
|
|
24
36
|
readonly bytes: Uint8Array
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
readonly
|
|
29
|
-
readonly pending: Pending
|
|
39
|
+
type Chain =
|
|
40
|
+
| { readonly tag: "settled"; readonly entries: ReadonlyMap<Braid, ChainEntry> }
|
|
41
|
+
| { readonly tag: "pending"; readonly entries: ReadonlyMap<Braid, ChainEntry>; readonly batch: Pending }
|
|
42
|
+
|
|
43
|
+
type SidecarRead =
|
|
44
|
+
| { readonly tag: "absent" }
|
|
45
|
+
| { readonly tag: "fault"; readonly io: Error }
|
|
46
|
+
| { readonly tag: "corrupt"; readonly parse: Error }
|
|
47
|
+
| { readonly tag: "read"; readonly chain: Chain }
|
|
48
|
+
|
|
49
|
+
function codeOf(error: Error): string | undefined {
|
|
50
|
+
return (error as NodeJS.ErrnoException).code
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function braidIdOf(id: Braid): number {
|
|
54
|
+
return Number.parseInt(id.slice(1), 16)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function vectorOf(entries: ReadonlyMap<Braid, { readonly g: bigint }>): Vector {
|
|
58
|
+
const counts = new Map<Braid, bigint>()
|
|
59
|
+
for (const [braid, entry] of entries) {
|
|
60
|
+
counts.set(braid, entry.g)
|
|
61
|
+
}
|
|
62
|
+
return Vector.from(counts)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function chainSum(chain: Chain): bigint {
|
|
66
|
+
const sum = vectorOf(chain.entries).sum()
|
|
67
|
+
return typeof sum === "bigint" ? sum : U64_MAX
|
|
30
68
|
}
|
|
31
69
|
|
|
32
|
-
function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
.map(function renderEntry(braid) {
|
|
36
|
-
const entry = sidecar.chain.get(braid)
|
|
37
|
-
if (entry === undefined) {
|
|
38
|
-
throw errors.new(`sidecar chain lost braid ${braid}`)
|
|
39
|
-
}
|
|
40
|
-
return `"${braid}":{"g":${entry.g},"prev":"${entry.prev}","ts":${entry.ts}}`
|
|
41
|
-
})
|
|
42
|
-
.join(",")
|
|
43
|
-
const pending =
|
|
44
|
-
sidecar.pending === null
|
|
45
|
-
? "null"
|
|
46
|
-
: `{"braid":"${sidecar.pending.braid}","gen":${sidecar.pending.gen},"bytes":"${Buffer.from(sidecar.pending.bytes).toString("base64")}"}`
|
|
47
|
-
return `{"v":2,"chain":{${chain}},"pending":${pending}}`
|
|
70
|
+
function chainGeneration(chain: Chain): bigint {
|
|
71
|
+
const sum = chainSum(chain)
|
|
72
|
+
return chain.tag === "settled" ? sum : saturatingAddU64(sum, 1n)
|
|
48
73
|
}
|
|
49
74
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const chain = new Map<Braid, ChainEntry>()
|
|
60
|
-
for (const [name, entry] of Object.entries(parsed.chain)) {
|
|
61
|
-
chain.set(braid(name), { g: generation(BigInt(entry.g)), prev: entry.prev, ts: BigInt(entry.ts) })
|
|
62
|
-
}
|
|
63
|
-
const pending =
|
|
64
|
-
parsed.pending === null
|
|
65
|
-
? null
|
|
66
|
-
: {
|
|
67
|
-
braid: braid(parsed.pending.braid),
|
|
68
|
-
gen: generation(BigInt(parsed.pending.gen)),
|
|
69
|
-
bytes: new Uint8Array(Buffer.from(parsed.pending.bytes, "base64"))
|
|
70
|
-
}
|
|
71
|
-
return { chain, pending }
|
|
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`)
|
|
83
|
+
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
|
|
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()
|
|
117
|
+
}
|
|
118
|
+
|
|
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}`)
|
|
131
|
+
}
|
|
132
|
+
const count = BigInt(reader.u32le("chain count"))
|
|
133
|
+
refuseUnbacked(count, reader.remaining(), ENTRY_BYTES, "chain count")
|
|
134
|
+
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")
|
|
153
|
+
}
|
|
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
|
+
}
|
|
159
|
+
return { tag: "settled", entries }
|
|
160
|
+
}
|
|
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
|
+
return {
|
|
176
|
+
tag: "pending",
|
|
177
|
+
entries,
|
|
178
|
+
batch: { braid: name, gen: generation(slot), bytes: body }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readSidecar(file: string, known?: ReadonlySet<Braid>): Promise<SidecarRead> {
|
|
75
183
|
const read = await errors.try(fs.readFile(file))
|
|
76
184
|
if (read.error) {
|
|
77
|
-
|
|
185
|
+
if (codeOf(read.error) === "ENOENT") {
|
|
186
|
+
return { tag: "absent" }
|
|
187
|
+
}
|
|
188
|
+
return { tag: "fault", io: read.error }
|
|
189
|
+
}
|
|
190
|
+
const parsed = errors.trySync(function parse() {
|
|
191
|
+
return parseSidecar(read.data, known)
|
|
192
|
+
})
|
|
193
|
+
if (parsed.error) {
|
|
194
|
+
return { tag: "corrupt", parse: parsed.error }
|
|
78
195
|
}
|
|
79
|
-
return
|
|
196
|
+
return { tag: "read", chain: parsed.data }
|
|
80
197
|
}
|
|
81
198
|
|
|
82
|
-
async function writeSidecar(file: string,
|
|
199
|
+
async function writeSidecar(file: string, chain: Chain): Promise<void> {
|
|
83
200
|
const dir = path.dirname(file)
|
|
84
201
|
await fs.mkdir(dir, { recursive: true })
|
|
85
202
|
const temp = path.join(dir, `.chain-${process.pid}-${crypto.randomBytes(4).toString("hex")}`)
|
|
86
203
|
const handle = await fs.open(temp, "wx")
|
|
87
204
|
const written = await errors.try(
|
|
88
205
|
(async function writeAll() {
|
|
89
|
-
await handle.writeFile(renderSidecar(
|
|
206
|
+
await handle.writeFile(renderSidecar(chain))
|
|
90
207
|
await handle.sync()
|
|
91
208
|
})()
|
|
92
209
|
)
|
|
@@ -104,5 +221,5 @@ async function writeSidecar(file: string, sidecar: Sidecar): Promise<void> {
|
|
|
104
221
|
}
|
|
105
222
|
}
|
|
106
223
|
|
|
107
|
-
export type { ChainEntry, Pending,
|
|
108
|
-
export { readSidecar, renderSidecar, writeSidecar }
|
|
224
|
+
export type { Chain, ChainEntry, Pending, SidecarRead }
|
|
225
|
+
export { CHAIN_FILE, chainGeneration, chainSum, parseSidecar, readSidecar, renderSidecar, writeSidecar }
|