@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 +22 -15
- package/package.json +3 -2
- package/src/braids.ts +5 -8
- package/src/bytes.ts +2 -2
- package/src/chain.ts +16 -17
- package/src/codec.ts +21 -23
- package/src/descriptor.ts +245 -276
- package/src/errors.ts +8 -6
- package/src/index.ts +10 -8
- package/src/keys.ts +69 -15
- package/src/manifest.ts +11 -7
- package/src/replica.ts +42 -44
- package/src/store-s3.ts +222 -0
- package/src/store.ts +76 -31
- package/src/value.ts +16 -16
- package/src/writer.ts +37 -34
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
|
|
5
|
-
|
|
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` —
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
##
|
|
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((
|
|
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
|
-
`
|
|
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.
|
|
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
|
-
##
|
|
69
|
+
## A local fleet
|
|
63
70
|
|
|
64
71
|
```ts
|
|
65
|
-
// one process per scope loop; all processes share one
|
|
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.
|
|
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.
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
|
3
|
-
*
|
|
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
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
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<
|
|
30
|
-
readonly pending:
|
|
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<
|
|
61
|
-
for (const [
|
|
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,
|
|
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 {
|
|
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 {
|
|
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
|
|
24
|
+
interface Op {
|
|
23
25
|
readonly op: "insert" | "delete"
|
|
24
26
|
readonly relation: string
|
|
25
|
-
readonly rows: ReadonlyArray<readonly
|
|
27
|
+
readonly rows: ReadonlyArray<readonly Value[]>
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface BatchHeader {
|
|
29
31
|
readonly fingerprint: string
|
|
30
|
-
readonly braid:
|
|
31
|
-
readonly braidGen:
|
|
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
|
|
41
|
+
readonly ops: readonly Op[]
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
function braidIdOf(
|
|
43
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
175
|
+
const rows: Value[][] = []
|
|
178
176
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
179
|
-
const row:
|
|
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
|
|
221
|
-
readonly g:
|
|
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:
|
|
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,
|
|
253
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, Op }
|
|
256
254
|
export { decodeBatch, encodeBatch, verifyChain }
|