@bjornpagen/bumbledb-log 0.17.0 → 0.18.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 CHANGED
@@ -1,8 +1,8 @@
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 dependency, 0.17.x lockstep).
5
- The package is three things:
4
+ a thin peer of `@bjornpagen/bumbledb` (peer `^0.18.0`). The package is
5
+ three things:
6
6
 
7
7
  1. **The mirrored pure pair**, byte-exact against the Rust driver and
8
8
  pinned by cross-language goldens: `encodeBatch`/`decodeBatch` (the
@@ -10,20 +10,30 @@ The package is three things:
10
10
  `braidsOf(descriptor)` (the schema's own shard map, as data — with
11
11
  `serialAtStatementsOf` naming the degenerate-serial statements beside it).
12
12
  2. **The five-verb object store** — `get`, `getIfChanged`, `putCreate`,
13
- `putSwap`, `delete` — with `fsStore` as the tier-1 local-directory
14
- implementation (deployment case 5's production backend, not a dev
15
- double). The S3/R2/OCI store rides `aws4fetch` and is not yet in this
16
- build (the dependency was unfetchable offline).
13
+ `putSwap`, `delete` — taking a branded `StoreKey` parsed once by
14
+ `storeKey`. `fsStore` is the tier-1 local-directory implementation;
15
+ `memStore` is the same five verbs over one in-process map
16
+ (single-process only; third `Etag` producer, blake3 like `fsStore`);
17
+ `s3Store` is the five verbs over S3-compatible storage (the official
18
+ `@aws-sdk/client-s3` client signs and talks; R2 rides region `auto`).
17
19
  3. **Replica and writer** composed from the engine SDK's existing verbs:
18
20
  `openReplica` hands out the SDK's own `Db`; `openWriter` adds the
19
21
  right to create log objects; `openTenants` is an LRU of per-tenant
20
22
  replicas. No engine surface is duplicated.
21
23
 
24
+ The package `engines` and the `.ts` test runner require Node >=24.
25
+
26
+ The exported vocabulary reads as English at the call site: `Value`,
27
+ `Interval`, `Batch`, `Theory`, `Descriptor`, `Op`, `Pending`,
28
+ `ChainEntry`, plus the branded scalars `StoreKey`, `Generation`,
29
+ `Etag`, and `Braid` (`storeKey`, `generation`, `etag`, `braid` parse
30
+ at the boundary; the verbs take the proof).
31
+
22
32
  Async ⟺ network: `openReplica`, `refresh`, `waitFor`, `commit`,
23
33
  `commitSplit`, and disposal await store verbs; everything on
24
34
  `replica.db`, the `batch.*` recorders, and the pure pair are synchronous.
25
35
 
26
- ## The Vercel recipe (documented example, not framework code)
36
+ ## A Fluid host
27
37
 
28
38
  ```ts
29
39
  // lib/db.ts — module scope; Fluid shares this across the instance's requests
@@ -33,7 +43,7 @@ export const replica = await openReplica({ store: s3(env), prefix: "prod/main",
33
43
  export const writer = openWriter(replica)
34
44
 
35
45
  // route handler
36
- const out = await writer.commit((b) => b.insert(Booking, [row]))
46
+ const out = await writer.commit((batch) => batch.insert(Booking, [row]))
37
47
  if (out.tag === "accepted") ctx.waitUntil(replica.refresh(out.braid))
38
48
  ```
39
49
 
@@ -42,7 +52,7 @@ if (out.tag === "accepted") ctx.waitUntil(replica.refresh(out.braid))
42
52
  leaf-blob pattern keeps metadata stores in the tens of MB. Per-tenant
43
53
  fleets get the same gate through `openTenants({ budgetBytes, maxOpen })`.
44
54
  - **Cross-instance read-your-writes**: a commit returns
45
- `(braid, generation)`; a session token is the pointwise max of every
55
+ `{ braid, generation }`; a session token is the pointwise max of every
46
56
  pair a flow has seen; `replica.waitFor(vector)` refreshes until the
47
57
  local vector dominates it. The committing instance always reads its
48
58
  own writes without waiting. A singleton map is the single-braid form.
@@ -54,15 +64,12 @@ if (out.tag === "accepted") ctx.waitUntil(replica.refresh(out.braid))
54
64
  row — the schema idiom) or resident mode. `{ kind: "slot-race", tip }`
55
65
  means the terminal losses were accepted but out-raced: an operational
56
66
  signal to shard the theory into more braids or move the hot braid to
57
- a resident Rust writer, whose group commit batches the queue. This
58
- package ships no group commit of its own; the recorded reopen trigger
59
- is a measured TS deployment at Turso-density write rates where a
60
- deliberate batching delay would amortize many writers into one PUT.
67
+ a resident Rust writer, whose group commit batches the queue.
61
68
 
62
- ## The local-fleet recipe (deployment case 5)
69
+ ## A local fleet
63
70
 
64
71
  ```ts
65
- // one process per scope loop; all processes share one FsStore prefix
72
+ // one process per scope loop; all processes share one fsStore prefix
66
73
  import { fsStore, openReplica, openWriter } from "@bjornpagen/bumbledb-log"
67
74
 
68
75
  const replica = await openReplica({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjornpagen/bumbledb-log",
3
- "version": "0.17.0",
3
+ "version": "0.18.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": {
@@ -32,10 +32,11 @@
32
32
  },
33
33
  "homepage": "https://github.com/bjornpagen/bumbledb#readme",
34
34
  "dependencies": {
35
+ "@aws-sdk/client-s3": "^3.1116.0",
35
36
  "@superbuilders/errors": "^4.0.2"
36
37
  },
37
38
  "peerDependencies": {
38
- "@bjornpagen/bumbledb": "^0.17.1"
39
+ "@bjornpagen/bumbledb": "^0.18.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@biomejs/biome": "2.5.4",
package/src/braids.ts CHANGED
@@ -5,14 +5,11 @@
5
5
  * the descriptor, pinned cross-language by the codec goldens.
6
6
  */
7
7
 
8
- import type { LogTheory, SerialStatement } from "#descriptor.ts"
9
- import { descriptorOf } from "#descriptor.ts"
10
-
11
- /** Braid id: `c{smallest RelationId:08x}`, scoped to the schema fingerprint. */
12
- type Braid = string
8
+ import type { Braid, SerialStatement, Theory } from "#descriptor.ts"
9
+ import { braid, descriptorOf } from "#descriptor.ts"
13
10
 
14
11
  /** The schema's own shard map: ordinary relation name → braid id. */
15
- function braidsOf(theory: LogTheory): ReadonlyMap<string, Braid> {
12
+ function braidsOf(theory: Theory): ReadonlyMap<string, Braid> {
16
13
  const descriptor = descriptorOf(theory)
17
14
  const out = new Map<string, Braid>()
18
15
  for (const relation of descriptor.relations) {
@@ -30,9 +27,9 @@ function braidsOf(theory: LogTheory): ReadonlyMap<string, Braid> {
30
27
  * serializes at that statement. Typed data beside the braid map, one
31
28
  * question per verb.
32
29
  */
33
- function serialAtStatementsOf(theory: LogTheory): readonly SerialStatement[] {
30
+ function serialAtStatementsOf(theory: Theory): readonly SerialStatement[] {
34
31
  return descriptorOf(theory).serialAtStatements
35
32
  }
36
33
 
37
34
  export type { Braid }
38
- export { braidsOf, serialAtStatementsOf }
35
+ export { braid, braidsOf, serialAtStatementsOf }
package/src/bytes.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Little-endian byte primitives shared by the codec, the footprint keys,
3
- * and the fingerprint mirror. Every multi-byte integer on the batch wire
2
+ * Little-endian byte primitives shared by the codec and the
3
+ * fingerprint mirror. Every multi-byte integer on the batch wire
4
4
  * is little-endian; the fingerprint's canonical literal encoding is the
5
5
  * engine's big-endian order-preserving form — both live here so no third
6
6
  * spelling can appear.
package/src/chain.ts CHANGED
@@ -12,22 +12,21 @@ import * as fs from "node:fs/promises"
12
12
  import * as path from "node:path"
13
13
  import * as errors from "@superbuilders/errors"
14
14
  import { utf8StrictDecoder } from "#bytes.ts"
15
+ import type { ChainEntry } from "#codec.ts"
16
+ import type { Braid } from "#descriptor.ts"
17
+ import { braid } from "#descriptor.ts"
18
+ import type { Generation } from "#keys.ts"
19
+ import { generation } from "#keys.ts"
15
20
 
16
- interface ChainEntry {
17
- readonly g: bigint
18
- readonly prev: string
19
- readonly ts: bigint
20
- }
21
-
22
- interface PendingBatch {
23
- readonly braid: string
24
- readonly gen: bigint
21
+ interface Pending {
22
+ readonly braid: Braid
23
+ readonly gen: Generation
25
24
  readonly bytes: Uint8Array
26
25
  }
27
26
 
28
27
  interface Sidecar {
29
- readonly chain: ReadonlyMap<string, ChainEntry>
30
- readonly pending: PendingBatch | null
28
+ readonly chain: ReadonlyMap<Braid, ChainEntry>
29
+ readonly pending: Pending | null
31
30
  }
32
31
 
33
32
  function renderSidecar(sidecar: Sidecar): string {
@@ -57,16 +56,16 @@ function parseSidecar(text: string): Sidecar {
57
56
  if (parsed.v !== 2 || typeof parsed.chain !== "object" || parsed.chain === null) {
58
57
  throw errors.new("sidecar is not a v2 chain file")
59
58
  }
60
- const chain = new Map<string, ChainEntry>()
61
- for (const [braid, entry] of Object.entries(parsed.chain)) {
62
- chain.set(braid, { g: BigInt(entry.g), prev: entry.prev, ts: BigInt(entry.ts) })
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) })
63
62
  }
64
63
  const pending =
65
64
  parsed.pending === null
66
65
  ? null
67
66
  : {
68
- braid: parsed.pending.braid,
69
- gen: BigInt(parsed.pending.gen),
67
+ braid: braid(parsed.pending.braid),
68
+ gen: generation(BigInt(parsed.pending.gen)),
70
69
  bytes: new Uint8Array(Buffer.from(parsed.pending.bytes, "base64"))
71
70
  }
72
71
  return { chain, pending }
@@ -105,5 +104,5 @@ async function writeSidecar(file: string, sidecar: Sidecar): Promise<void> {
105
104
  }
106
105
  }
107
106
 
108
- export type { ChainEntry, PendingBatch, Sidecar }
107
+ export type { ChainEntry, Pending, Sidecar }
109
108
  export { readSidecar, renderSidecar, writeSidecar }
package/src/codec.ts CHANGED
@@ -9,26 +9,28 @@
9
9
 
10
10
  import * as errors from "@superbuilders/errors"
11
11
  import { ByteReader, ByteWriter, bytesEqual, fromHex, toHex, utf8Encoder } from "#bytes.ts"
12
- import type { LogTheory } from "#descriptor.ts"
12
+ import type { Braid, Theory } from "#descriptor.ts"
13
13
  import { braidHex, descriptorOf } from "#descriptor.ts"
14
14
  import { refuse, refuseChain } from "#errors.ts"
15
- import type { LogValue, TaggedRefusal } from "#value.ts"
15
+ import type { Generation } from "#keys.ts"
16
+ import { generation } from "#keys.ts"
17
+ import type { TaggedRefusal, Value } from "#value.ts"
16
18
  import { readTagged, writeTagged } from "#value.ts"
17
19
 
18
20
  const MAGIC = utf8Encoder.encode("BDBL")
19
21
  const VERSION = 2
20
22
  const OP_KIND = { insert: 1, delete: 2 } as const
21
23
 
22
- interface BatchOp {
24
+ interface Op {
23
25
  readonly op: "insert" | "delete"
24
26
  readonly relation: string
25
- readonly rows: ReadonlyArray<readonly LogValue[]>
27
+ readonly rows: ReadonlyArray<readonly Value[]>
26
28
  }
27
29
 
28
30
  interface BatchHeader {
29
31
  readonly fingerprint: string
30
- readonly braid: string
31
- readonly braidGen: bigint
32
+ readonly braid: Braid
33
+ readonly braidGen: Generation
32
34
  readonly prev: string
33
35
  readonly writer: bigint
34
36
  readonly timestamp: bigint
@@ -36,15 +38,11 @@ interface BatchHeader {
36
38
 
37
39
  interface DecodedBatch {
38
40
  readonly header: BatchHeader
39
- readonly ops: readonly BatchOp[]
41
+ readonly ops: readonly Op[]
40
42
  }
41
43
 
42
- function braidIdOf(braid: string): number {
43
- const match = /^c([0-9a-f]{8})$/.exec(braid)
44
- if (match === null || match[1] === undefined) {
45
- throw errors.new(`not a braid id: ${braid}`)
46
- }
47
- return Number.parseInt(match[1], 16)
44
+ function braidIdOf(id: Braid): number {
45
+ return Number.parseInt(id.slice(1), 16)
48
46
  }
49
47
 
50
48
  /**
@@ -52,7 +50,7 @@ function braidIdOf(braid: string): number {
52
50
  * the object is published under; every op relation must belong to the
53
51
  * header's braid — a spanning batch is unencodable.
54
52
  */
55
- function encodeBatch(theory: LogTheory, header: BatchHeader, ops: readonly BatchOp[]): Uint8Array {
53
+ function encodeBatch(theory: Theory, header: BatchHeader, ops: readonly Op[]): Uint8Array {
56
54
  const descriptor = descriptorOf(theory)
57
55
  if (header.fingerprint !== descriptor.fingerprint) {
58
56
  throw errors.new(`encode fingerprint ${header.fingerprint} is not the descriptor's ${descriptor.fingerprint}`)
@@ -105,7 +103,7 @@ function encodeBatch(theory: LogTheory, header: BatchHeader, ops: readonly Batch
105
103
  }
106
104
 
107
105
  /** Full parse of a batch object; refusals are typed, never partial reads. */
108
- function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
106
+ function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
109
107
  const descriptor = descriptorOf(theory)
110
108
  const reader = new ByteReader(bytes, {
111
109
  fail(what: string): never {
@@ -138,13 +136,13 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
138
136
  if (members === undefined) {
139
137
  refuse({ kind: "UnknownBraid", braid: braidId }, `batch braid ${braid} is not derived from this descriptor`)
140
138
  }
141
- const braidGen = reader.u64le("braid generation")
139
+ const braidGen = generation(reader.u64le("braid generation"))
142
140
  const prev = toHex(reader.bytes(32, "prev"))
143
141
  const writer = reader.u64le("writer")
144
142
  const timestamp = reader.u64le("timestamp")
145
143
 
146
144
  const opCount = reader.u32le("op count")
147
- const ops: BatchOp[] = []
145
+ const ops: Op[] = []
148
146
  for (let opIndex = 0; opIndex < opCount; opIndex++) {
149
147
  const kind = reader.u8("op kind")
150
148
  if (kind !== OP_KIND.insert && kind !== OP_KIND.delete) {
@@ -174,9 +172,9 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
174
172
  )
175
173
  }
176
174
  const rowCount = reader.u32le("row count")
177
- const rows: LogValue[][] = []
175
+ const rows: Value[][] = []
178
176
  for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
179
- const row: LogValue[] = []
177
+ const row: Value[] = []
180
178
  relation.fields.forEach(function readCell(field) {
181
179
  const at = { relation: relation.name, row: rowIndex, field: field.name }
182
180
  const where = `relation ${relation.name} row ${rowIndex} field ${field.name}`
@@ -217,8 +215,8 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
217
215
  }
218
216
  }
219
217
 
220
- interface ChainPosition {
221
- readonly g: bigint
218
+ interface ChainEntry {
219
+ readonly g: Generation
222
220
  readonly prev: string
223
221
  readonly ts: bigint
224
222
  }
@@ -231,7 +229,7 @@ interface ChainPosition {
231
229
  * names the misbehaving writer, and the refusal data names the fetched
232
230
  * braid.
233
231
  */
234
- function verifyChain(header: BatchHeader, braid: string, slot: bigint, chain: ChainPosition): void {
232
+ function verifyChain(header: BatchHeader, braid: Braid, slot: Generation, chain: ChainEntry): void {
235
233
  if (header.braid !== braid || header.braidGen !== slot) {
236
234
  refuseChain(
237
235
  { cause: "slot", braid, slot, writer: header.writer },
@@ -252,5 +250,5 @@ function verifyChain(header: BatchHeader, braid: string, slot: bigint, chain: Ch
252
250
  }
253
251
  }
254
252
 
255
- export type { BatchHeader, BatchOp, ChainPosition, DecodedBatch }
253
+ export type { BatchHeader, ChainEntry, DecodedBatch, Op }
256
254
  export { decodeBatch, encodeBatch, verifyChain }