@bjornpagen/bumbledb-log 0.17.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/errors.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The driver's error identities, on the SDK's `@superbuilders/errors`
3
+ * idiom: exported sentinel values checked with `errors.is`, structured
4
+ * causes carried as data properties read back with the `*Of` accessors —
5
+ * never by message-string matching. There is deliberately no
6
+ * `ErrAlreadyApplied`: the state it would name is absorbed by idempotent
7
+ * replay (L10) and never surfaces.
8
+ */
9
+
10
+ import * as errors from "@superbuilders/errors"
11
+
12
+ /**
13
+ * Typed refusal of a protocol object before any apply: batch shape,
14
+ * version, fingerprint, manifest shape, checkpoint braid-set drift —
15
+ * one identity, cause data per site.
16
+ */
17
+ const ErrRefused = errors.new("bumbledb-log refused")
18
+
19
+ /** `commit` recorded ops in more than one braid; `commitSplit` is the verb. */
20
+ const ErrSpanningCommit = errors.new(
21
+ "bumbledb-log spanningCommit: the recorded ops span braids — commitSplit is the explicit verb"
22
+ )
23
+
24
+ /** 404 at or below the current checkpoint's vector: the tail was gc'd. */
25
+ const ErrGapDetected = errors.new("bumbledb-log gapDetected: the log tail below the checkpoint vector was collected")
26
+
27
+ /** A rejected replay on a store that passed the wholeness check. */
28
+ const ErrReplayDiverged = errors.new(
29
+ "bumbledb-log replayDiverged: a published batch rejected during steady-state replay"
30
+ )
31
+
32
+ /** The chain discipline, one identity with three proved causes. */
33
+ const ErrChainMismatch = errors.new("bumbledb-log chainMismatch: the batch violates the braid's chain discipline")
34
+
35
+ /** Bounded live-tip losses exhausted — an operational signal, not an outcome arm. */
36
+ const ErrContention = errors.new("bumbledb-log contention: consecutive live-tip losses exhausted the bound")
37
+
38
+ /** The vendor channel: I/O and store infrastructure failures. */
39
+ const ErrStore = errors.new("bumbledb-log store failure")
40
+
41
+ /**
42
+ * Decode refusal kinds carry the cross-implementation identity names the
43
+ * Rust driver's `DecodeError::identity` pins — the conformance corpus
44
+ * compares them string for string. The tail kinds are this driver's own
45
+ * replica-boundary refusals.
46
+ */
47
+ type RefusalCause =
48
+ | { readonly kind: "Truncated"; readonly at: string }
49
+ | { readonly kind: "BadMagic" }
50
+ | { readonly kind: "Version"; readonly version: number }
51
+ | { readonly kind: "Flags"; readonly flags: number }
52
+ | { readonly kind: "FingerprintMismatch"; readonly carried: string; readonly expected: string }
53
+ | { readonly kind: "UnknownBraid"; readonly braid: number }
54
+ | { readonly kind: "UnknownOpKind"; readonly op: number; readonly opKind: number }
55
+ | { readonly kind: "UnknownRelation"; readonly op: number; readonly relation: number }
56
+ | { readonly kind: "ClosedRelation"; readonly op: number; readonly relation: number }
57
+ | { readonly kind: "OpRelationOutsideBraid"; readonly op: number; readonly relation: number; readonly braid: string }
58
+ | { readonly kind: "TagMismatch"; readonly relation: string; readonly row: number; readonly field: string }
59
+ | { readonly kind: "BoolByte"; readonly relation: string; readonly row: number; readonly field: string }
60
+ | { readonly kind: "InvalidUtf8"; readonly relation: string; readonly row: number; readonly field: string }
61
+ | { readonly kind: "EmptyInterval"; readonly relation: string; readonly row: number; readonly field: string }
62
+ | { readonly kind: "IntervalOverflow"; readonly relation: string; readonly row: number; readonly field: string }
63
+ | { readonly kind: "TrailingBytes"; readonly bytes: number }
64
+ | { readonly kind: "ManifestShape" }
65
+ | { readonly kind: "ManifestVersion"; readonly version: number }
66
+ | { readonly kind: "CheckpointShape" }
67
+ | { readonly kind: "CheckpointBraids"; readonly carried: readonly string[]; readonly derived: readonly string[] }
68
+ | { readonly kind: "CheckpointDigest"; readonly expected: string; readonly computed: string }
69
+ | { readonly kind: "NoOpSlot"; readonly braid: string; readonly slot: bigint; readonly writer: bigint }
70
+
71
+ type ChainCause = "prev" | "slot" | "timestamp"
72
+
73
+ interface ChainMismatchData {
74
+ readonly cause: ChainCause
75
+ readonly braid: string
76
+ readonly slot: bigint
77
+ readonly writer: bigint
78
+ }
79
+
80
+ type ContentionCause =
81
+ | {
82
+ readonly kind: "hot-key"
83
+ readonly statement: string
84
+ readonly determinants: ReadonlyArray<Readonly<Record<string, unknown>>>
85
+ }
86
+ | { readonly kind: "slot-race"; readonly tip: bigint }
87
+
88
+ interface ContentionData {
89
+ readonly braid: string
90
+ readonly cause: ContentionCause
91
+ }
92
+
93
+ const refusalData = new WeakMap<Error, RefusalCause>()
94
+ const chainData = new WeakMap<Error, ChainMismatchData>()
95
+ const contentionData = new WeakMap<Error, ContentionData>()
96
+
97
+ function refuse(cause: RefusalCause, detail: string): never {
98
+ const error = errors.wrap(ErrRefused, detail)
99
+ refusalData.set(error, cause)
100
+ throw error
101
+ }
102
+
103
+ function refuseChain(data: ChainMismatchData, detail: string): never {
104
+ const error = errors.wrap(ErrChainMismatch, detail)
105
+ chainData.set(error, data)
106
+ throw error
107
+ }
108
+
109
+ function throwContention(data: ContentionData, detail: string): never {
110
+ const error = errors.wrap(ErrContention, detail)
111
+ contentionData.set(error, data)
112
+ throw error
113
+ }
114
+
115
+ function refusalOf(error: Error): RefusalCause | undefined {
116
+ return refusalData.get(error)
117
+ }
118
+
119
+ function chainMismatchOf(error: Error): ChainMismatchData | undefined {
120
+ return chainData.get(error)
121
+ }
122
+
123
+ function contentionOf(error: Error): ContentionData | undefined {
124
+ return contentionData.get(error)
125
+ }
126
+
127
+ /** Every store failure wraps the exported sentinel itself, so
128
+ * `errors.is(e, ErrStore)` matches by identity; the vendor error's
129
+ * message rides the detail verbatim. */
130
+ function wrapStore(inner: Error, detail: string): Error {
131
+ return errors.wrap(ErrStore, `${detail}: ${inner.message}`)
132
+ }
133
+
134
+ export type { ChainCause, ChainMismatchData, ContentionCause, ContentionData, RefusalCause }
135
+ export {
136
+ chainMismatchOf,
137
+ contentionOf,
138
+ ErrChainMismatch,
139
+ ErrContention,
140
+ ErrGapDetected,
141
+ ErrRefused,
142
+ ErrReplayDiverged,
143
+ ErrSpanningCommit,
144
+ ErrStore,
145
+ refusalOf,
146
+ refuse,
147
+ refuseChain,
148
+ throwContention,
149
+ wrapStore
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @bjornpagen/bumbledb-log — braided object-store replication for
3
+ * bumbledb, a thin peer of the engine SDK. Public surface: the pure
4
+ * protocol pair (`encodeBatch`/`decodeBatch`, `braidsOf`) mirrored
5
+ * byte-exactly against the Rust driver, the five-verb object store
6
+ * with its tier-1 `fsStore`, and `openReplica`/`openWriter`/
7
+ * `openTenants` composed from the engine SDK's own verbs — the replica
8
+ * hands out the SDK's `Db`, and no engine surface is duplicated.
9
+ */
10
+
11
+ export type { Braid } from "#braids.ts"
12
+ export { braidsOf, serialAtStatementsOf } from "#braids.ts"
13
+ export type { BatchHeader, BatchOp, ChainPosition, DecodedBatch } from "#codec.ts"
14
+ export { decodeBatch, encodeBatch, verifyChain } from "#codec.ts"
15
+ export type { LogDescriptor, LogTheory, SerialStatement } from "#descriptor.ts"
16
+ export { descriptorOf } from "#descriptor.ts"
17
+ export type { ChainCause, ChainMismatchData, ContentionCause, ContentionData, RefusalCause } from "#errors.ts"
18
+ export {
19
+ chainMismatchOf,
20
+ contentionOf,
21
+ ErrChainMismatch,
22
+ ErrContention,
23
+ ErrGapDetected,
24
+ ErrRefused,
25
+ ErrReplayDiverged,
26
+ ErrSpanningCommit,
27
+ ErrStore,
28
+ refusalOf
29
+ } from "#errors.ts"
30
+ export type { OpenReplicaOptions, Replica } from "#replica.ts"
31
+ export { openReplica } from "#replica.ts"
32
+ export type { Create, Fetched, ObjectStore, Poll, Swap } from "#store.ts"
33
+ export { fsStore } from "#store.ts"
34
+ export type { OpenTenantsOptions, Tenants } from "#tenants.ts"
35
+ export { openTenants } from "#tenants.ts"
36
+ export type { LogInterval, LogValue } from "#value.ts"
37
+ export type { BraidOutcome, Commit, CommitSplit, Durability, LogBatch, Writer } from "#writer.ts"
38
+ export { openWriter } from "#writer.ts"
package/src/keys.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The key layout (10): generation numbers zero-padded lowercase hex,
3
+ * 16 chars; a prefix is a store; a tenant is a prefix under `t/`.
4
+ */
5
+
6
+ function hex16(g: bigint): string {
7
+ return g.toString(16).padStart(16, "0")
8
+ }
9
+
10
+ function manifestKey(prefix: string): string {
11
+ return `${prefix}/manifest.json`
12
+ }
13
+
14
+ function logKey(prefix: string, braid: string, g: bigint): string {
15
+ return `${prefix}/log/${braid}/${hex16(g)}`
16
+ }
17
+
18
+ function checkpointMdbKey(prefix: string, digest: string): string {
19
+ return `${prefix}/ckpt/${digest}.mdb`
20
+ }
21
+
22
+ function checkpointJsonKey(prefix: string, digest: string): string {
23
+ return `${prefix}/ckpt/${digest}.json`
24
+ }
25
+
26
+ function idsKey(prefix: string, relation: number, field: number): string {
27
+ return `${prefix}/ids/${relation.toString(16).padStart(8, "0")}/${field.toString(16).padStart(4, "0")}`
28
+ }
29
+
30
+ function tenantPrefix(root: string, tenant: string): string {
31
+ return `${root}/t/${tenant}`
32
+ }
33
+
34
+ export { checkpointJsonKey, checkpointMdbKey, hex16, idsKey, logKey, manifestKey, tenantPrefix }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The protocol's one mutable object (10): canonical single-line UTF-8
3
+ * JSON, strict parse, field order fixed — strictness is proved by
4
+ * re-rendering the parse and demanding byte equality, so a
5
+ * non-canonical manifest is a typed refusal, never a tolerated variant.
6
+ * The checkpoint json beside it is immutable and digest-keyed.
7
+ */
8
+
9
+ import * as errors from "@superbuilders/errors"
10
+ import { utf8Encoder, utf8StrictDecoder } from "#bytes.ts"
11
+ import { refuse } from "#errors.ts"
12
+
13
+ interface Manifest {
14
+ readonly fingerprint: string
15
+ readonly checkpoint: string | null
16
+ }
17
+
18
+ const HEX64 = /^[0-9a-f]{64}$/
19
+
20
+ function renderManifest(manifest: Manifest): Uint8Array {
21
+ const checkpoint = manifest.checkpoint === null ? "null" : `"${manifest.checkpoint}"`
22
+ return utf8Encoder.encode(`{"v":2,"fingerprint":"${manifest.fingerprint}","checkpoint":${checkpoint}}`)
23
+ }
24
+
25
+ function parseManifest(bytes: Uint8Array): Manifest {
26
+ const decoded = errors.trySync(function decodeText() {
27
+ return utf8StrictDecoder.decode(bytes)
28
+ })
29
+ if (decoded.error) {
30
+ refuse({ kind: "ManifestShape" }, "manifest is not UTF-8")
31
+ }
32
+ const parsed = errors.trySync(function parseJson() {
33
+ return JSON.parse(decoded.data) as unknown
34
+ })
35
+ if (parsed.error || typeof parsed.data !== "object" || parsed.data === null) {
36
+ refuse({ kind: "ManifestShape" }, "manifest is not a JSON object")
37
+ }
38
+ const record = parsed.data as Record<string, unknown>
39
+ if (typeof record.v !== "number") {
40
+ refuse({ kind: "ManifestShape" }, "manifest carries no version")
41
+ }
42
+ if (record.v !== 2) {
43
+ refuse({ kind: "ManifestVersion", version: record.v }, `manifest version ${record.v}, consumers refuse ≠ 2`)
44
+ }
45
+ const fingerprint = record.fingerprint
46
+ const checkpoint = record.checkpoint
47
+ if (typeof fingerprint !== "string" || !HEX64.test(fingerprint)) {
48
+ refuse({ kind: "ManifestShape" }, "manifest fingerprint is not 64 hex")
49
+ }
50
+ if (checkpoint !== null && (typeof checkpoint !== "string" || !HEX64.test(checkpoint))) {
51
+ refuse({ kind: "ManifestShape" }, "manifest checkpoint is neither null nor 64 hex")
52
+ }
53
+ const manifest: Manifest = { fingerprint, checkpoint: checkpoint === null ? null : checkpoint }
54
+ const canonical = renderManifest(manifest)
55
+ if (canonical.length !== bytes.length || utf8StrictDecoder.decode(canonical) !== decoded.data) {
56
+ refuse({ kind: "ManifestShape" }, "manifest is not the canonical single-line rendering")
57
+ }
58
+ return manifest
59
+ }
60
+
61
+ interface CheckpointHead {
62
+ readonly g: bigint
63
+ readonly hash: string
64
+ readonly ts: bigint
65
+ }
66
+
67
+ interface CheckpointFacts {
68
+ readonly braids: ReadonlyMap<string, CheckpointHead>
69
+ readonly catalog: string
70
+ readonly writer: bigint
71
+ readonly prev: string | null
72
+ }
73
+
74
+ function parseCheckpoint(bytes: Uint8Array): CheckpointFacts {
75
+ const parsed = errors.trySync(function parseJson() {
76
+ return JSON.parse(utf8StrictDecoder.decode(bytes)) as unknown
77
+ })
78
+ if (parsed.error || typeof parsed.data !== "object" || parsed.data === null) {
79
+ refuse({ kind: "CheckpointShape" }, "checkpoint json is not an object")
80
+ }
81
+ const record = parsed.data as Record<string, unknown>
82
+ const rawBraids = record.braids
83
+ if (typeof rawBraids !== "object" || rawBraids === null) {
84
+ refuse({ kind: "CheckpointShape" }, "checkpoint json carries no braids map")
85
+ }
86
+ const braids = new Map<string, CheckpointHead>()
87
+ for (const [braid, head] of Object.entries(rawBraids as Record<string, unknown>)) {
88
+ if (typeof head !== "object" || head === null) {
89
+ refuse({ kind: "CheckpointShape" }, `checkpoint braid ${braid} head is not an object`)
90
+ }
91
+ const headRecord = head as Record<string, unknown>
92
+ const g = headRecord.g
93
+ const hash = headRecord.hash
94
+ const ts = headRecord.ts
95
+ if (typeof g !== "number" || typeof ts !== "number" || typeof hash !== "string" || !HEX64.test(hash)) {
96
+ refuse({ kind: "CheckpointShape" }, `checkpoint braid ${braid} head is malformed`)
97
+ }
98
+ braids.set(braid, { g: BigInt(g), hash, ts: BigInt(ts) })
99
+ }
100
+ const catalog = record.catalog
101
+ const writer = record.writer
102
+ const prev = record.prev
103
+ if (typeof catalog !== "string" || !HEX64.test(catalog) || typeof writer !== "number") {
104
+ refuse({ kind: "CheckpointShape" }, "checkpoint json catalog or writer is malformed")
105
+ }
106
+ if (prev !== null && (typeof prev !== "string" || !HEX64.test(prev))) {
107
+ refuse({ kind: "CheckpointShape" }, "checkpoint json prev is neither null nor a digest")
108
+ }
109
+ return { braids, catalog, writer: BigInt(writer), prev: prev === null ? null : prev }
110
+ }
111
+
112
+ export type { CheckpointFacts, CheckpointHead, Manifest }
113
+ export { parseCheckpoint, parseManifest, renderManifest }