@bjornpagen/bumbledb-log 0.17.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 +26 -18
- package/package.json +5 -3
- package/src/braids.ts +19 -7
- package/src/bytes.ts +79 -11
- package/src/chain.ts +179 -63
- package/src/codec.ts +141 -62
- package/src/descriptor.ts +392 -348
- package/src/errors.ts +140 -10
- package/src/index.ts +23 -8
- package/src/keys.ts +212 -15
- package/src/manifest.ts +171 -75
- package/src/replica.ts +606 -295
- package/src/store-s3.ts +377 -0
- package/src/store.ts +524 -105
- package/src/tenants.ts +322 -26
- package/src/value.ts +80 -29
- package/src/vector.ts +173 -0
- package/src/writer.ts +484 -120
package/src/codec.ts
CHANGED
|
@@ -8,89 +8,141 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as errors from "@superbuilders/errors"
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
11
|
+
import type { Digest32 } from "#bytes.ts"
|
|
12
|
+
import { ByteReader, ByteWriter, bytesEqual, digest32, hex32, utf8Encoder } from "#bytes.ts"
|
|
13
|
+
import type { Braid, Theory } from "#descriptor.ts"
|
|
13
14
|
import { braidHex, descriptorOf } from "#descriptor.ts"
|
|
14
15
|
import { refuse, refuseChain } from "#errors.ts"
|
|
15
|
-
import type {
|
|
16
|
-
import {
|
|
16
|
+
import type { Generation } from "#keys.ts"
|
|
17
|
+
import { generation } from "#keys.ts"
|
|
18
|
+
import type { TaggedRefusal, Value } from "#value.ts"
|
|
19
|
+
import { checkAgainst, readTagged, writeTagged } from "#value.ts"
|
|
17
20
|
|
|
18
21
|
const MAGIC = utf8Encoder.encode("BDBL")
|
|
19
|
-
const VERSION =
|
|
22
|
+
const VERSION = 3
|
|
20
23
|
const OP_KIND = { insert: 1, delete: 2 } as const
|
|
24
|
+
const U32_MAX = 0xffffffffn
|
|
21
25
|
|
|
22
|
-
interface
|
|
26
|
+
interface Op {
|
|
23
27
|
readonly op: "insert" | "delete"
|
|
24
28
|
readonly relation: string
|
|
25
|
-
readonly rows: ReadonlyArray<readonly
|
|
29
|
+
readonly rows: ReadonlyArray<readonly Value[]>
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
interface BatchHeader {
|
|
29
|
-
readonly fingerprint:
|
|
30
|
-
readonly braid:
|
|
31
|
-
readonly braidGen:
|
|
32
|
-
readonly prev:
|
|
33
|
+
readonly fingerprint: Digest32
|
|
34
|
+
readonly braid: Braid
|
|
35
|
+
readonly braidGen: Generation
|
|
36
|
+
readonly prev: Digest32
|
|
37
|
+
readonly writer: bigint
|
|
38
|
+
readonly timestamp: bigint
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Encode input. Digest fields are raw bytes so a short `prev` reaches
|
|
43
|
+
* the named `DigestWidth` refuse at this gate instead of dying at
|
|
44
|
+
* `Digest32` construction. Decode still produces a branded
|
|
45
|
+
* `BatchHeader`.
|
|
46
|
+
*/
|
|
47
|
+
interface EncodeHeader {
|
|
48
|
+
readonly fingerprint: Uint8Array
|
|
49
|
+
readonly braid: Braid
|
|
50
|
+
readonly braidGen: Generation
|
|
51
|
+
readonly prev: Uint8Array
|
|
33
52
|
readonly writer: bigint
|
|
34
53
|
readonly timestamp: bigint
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
interface DecodedBatch {
|
|
38
57
|
readonly header: BatchHeader
|
|
39
|
-
readonly ops: readonly
|
|
58
|
+
readonly ops: readonly Op[]
|
|
40
59
|
}
|
|
41
60
|
|
|
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)
|
|
61
|
+
function braidIdOf(id: Braid): number {
|
|
62
|
+
return Number.parseInt(id.slice(1), 16)
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
/**
|
|
51
|
-
* Encodes one batch.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
66
|
+
* Encodes one batch. Digests are branded here: a `prev` or fingerprint
|
|
67
|
+
* that is not 32 bytes is `DigestWidth`. The header's `braid_gen` must
|
|
68
|
+
* equal the slot number the object is published under; every op
|
|
69
|
+
* relation must belong to the header's braid — a spanning batch is
|
|
70
|
+
* unencodable.
|
|
54
71
|
*/
|
|
55
|
-
function encodeBatch(theory:
|
|
72
|
+
function encodeBatch(theory: Theory, header: EncodeHeader, ops: readonly Op[]): Uint8Array {
|
|
56
73
|
const descriptor = descriptorOf(theory)
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
const fingerprint = asDigest(header.fingerprint, "fingerprint")
|
|
75
|
+
const prev = asDigest(header.prev, "prev")
|
|
76
|
+
if (!bytesEqual(fingerprint, descriptor.fingerprintBytes)) {
|
|
77
|
+
throw errors.new(`encode fingerprint ${hex32(fingerprint)} is not the descriptor's ${descriptor.fingerprint}`)
|
|
59
78
|
}
|
|
60
79
|
const braidId = braidIdOf(header.braid)
|
|
61
80
|
const members = descriptor.braidMembers.get(header.braid)
|
|
62
81
|
if (members === undefined) {
|
|
63
82
|
throw errors.new(`braid ${header.braid} is not derived from this descriptor`)
|
|
64
83
|
}
|
|
65
|
-
for (const op of ops) {
|
|
84
|
+
for (const [opIndex, op] of ops.entries()) {
|
|
66
85
|
const relation = descriptor.relationByName.get(op.relation)
|
|
67
86
|
if (relation === undefined) {
|
|
68
87
|
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
69
88
|
}
|
|
89
|
+
if (relation.closed) {
|
|
90
|
+
refuse(
|
|
91
|
+
{ kind: "ClosedRelation", op: opIndex, relation: relation.id },
|
|
92
|
+
`op ${opIndex} writes closed relation ${relation.name}`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
70
95
|
if (!members.includes(relation.id)) {
|
|
71
96
|
throw errors.new(`op relation ${op.relation} is outside braid ${header.braid} — a spanning batch is unencodable`)
|
|
72
97
|
}
|
|
98
|
+
for (const [rowIndex, row] of op.rows.entries()) {
|
|
99
|
+
if (row.length !== relation.fields.length) {
|
|
100
|
+
refuse(
|
|
101
|
+
{ kind: "Arity", op: opIndex, relation: relation.name, row: rowIndex },
|
|
102
|
+
`op ${opIndex} relation ${relation.name} row ${rowIndex} arity ${row.length} ≠ ${relation.fields.length}`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
relation.fields.forEach(function gateCell(field, ordinal) {
|
|
106
|
+
const value = row[ordinal]
|
|
107
|
+
if (value === undefined) {
|
|
108
|
+
refuse(
|
|
109
|
+
{ kind: "Arity", op: opIndex, relation: relation.name, row: rowIndex },
|
|
110
|
+
`op ${opIndex} relation ${relation.name} row ${rowIndex} cell ${ordinal} absent`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
checkAgainst(`relation ${relation.name} field ${field.name}`, field.type, value)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const opCount = BigInt(ops.length)
|
|
119
|
+
if (opCount > U32_MAX) {
|
|
120
|
+
throw errors.new(`encode op count ${opCount} exceeds u32`)
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
const out = new ByteWriter(4096)
|
|
76
124
|
out.bytes(MAGIC)
|
|
77
125
|
out.u16le(VERSION)
|
|
78
126
|
out.u16le(0)
|
|
79
|
-
out.bytes(
|
|
127
|
+
out.bytes(fingerprint)
|
|
80
128
|
out.u32le(braidId)
|
|
81
129
|
out.u64le(header.braidGen)
|
|
82
|
-
out.bytes(
|
|
130
|
+
out.bytes(prev)
|
|
83
131
|
out.u64le(header.writer)
|
|
84
132
|
out.u64le(header.timestamp)
|
|
85
|
-
out.u32le(
|
|
133
|
+
out.u32le(Number(opCount))
|
|
86
134
|
for (const op of ops) {
|
|
87
135
|
const relation = descriptor.relationByName.get(op.relation)
|
|
88
136
|
if (relation === undefined) {
|
|
89
137
|
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
90
138
|
}
|
|
139
|
+
const rowCount = BigInt(op.rows.length)
|
|
140
|
+
if (rowCount > U32_MAX) {
|
|
141
|
+
throw errors.new(`encode row count ${rowCount} exceeds u32`)
|
|
142
|
+
}
|
|
91
143
|
out.u8(OP_KIND[op.op])
|
|
92
144
|
out.u32le(relation.id)
|
|
93
|
-
out.u32le(
|
|
145
|
+
out.u32le(Number(rowCount))
|
|
94
146
|
for (const row of op.rows) {
|
|
95
147
|
relation.fields.forEach(function writeCell(field, ordinal) {
|
|
96
148
|
const value = row[ordinal]
|
|
@@ -104,8 +156,36 @@ function encodeBatch(theory: LogTheory, header: BatchHeader, ops: readonly Batch
|
|
|
104
156
|
return out.finish()
|
|
105
157
|
}
|
|
106
158
|
|
|
159
|
+
function asDigest(bytes: Uint8Array, at: string): Digest32 {
|
|
160
|
+
if (bytes.length !== 32) {
|
|
161
|
+
refuse({ kind: "DigestWidth" }, `${at} is not 32 bytes`)
|
|
162
|
+
}
|
|
163
|
+
return digest32(bytes)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readU32(reader: ByteReader, what: string): bigint {
|
|
167
|
+
return BigInt(reader.u32le(what))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Kind + relation id + row count: the shortest op the grammar admits. */
|
|
171
|
+
const MIN_OP_BYTES = 9n
|
|
172
|
+
|
|
173
|
+
/** A declared count the remaining bytes cannot open is Truncated
|
|
174
|
+
* before the loop. Counts are exact bigint so a u32::MAX row vector
|
|
175
|
+
* cannot wrap a JavaScript number. A zero-field relation has no row
|
|
176
|
+
* bytes. A nonempty layout uses one tag byte so a first-cell typed
|
|
177
|
+
* refusal is not swallowed. */
|
|
178
|
+
function refuseUnbacked(count: bigint, remaining: number, minItem: bigint, at: string): void {
|
|
179
|
+
if (count === 0n) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (minItem === 0n || BigInt(remaining) / minItem < count) {
|
|
183
|
+
refuse({ kind: "Truncated", at }, `declared ${at} ${count} outruns the remaining ${remaining} bytes`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
107
187
|
/** Full parse of a batch object; refusals are typed, never partial reads. */
|
|
108
|
-
function decodeBatch(theory:
|
|
188
|
+
function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
109
189
|
const descriptor = descriptorOf(theory)
|
|
110
190
|
const reader = new ByteReader(bytes, {
|
|
111
191
|
fail(what: string): never {
|
|
@@ -125,10 +205,10 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
|
|
|
125
205
|
if (flags !== 0) {
|
|
126
206
|
refuse({ kind: "Flags", flags }, `batch flags ${flags} must be 0`)
|
|
127
207
|
}
|
|
128
|
-
const fingerprint =
|
|
129
|
-
if (fingerprint
|
|
208
|
+
const fingerprint = digest32(reader.bytes(32, "fingerprint"))
|
|
209
|
+
if (!bytesEqual(fingerprint, descriptor.fingerprintBytes)) {
|
|
130
210
|
refuse(
|
|
131
|
-
{ kind: "FingerprintMismatch", carried: fingerprint, expected: descriptor.fingerprint },
|
|
211
|
+
{ kind: "FingerprintMismatch", carried: hex32(fingerprint), expected: descriptor.fingerprint },
|
|
132
212
|
"batch fingerprint does not match the descriptor"
|
|
133
213
|
)
|
|
134
214
|
}
|
|
@@ -138,48 +218,47 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
|
|
|
138
218
|
if (members === undefined) {
|
|
139
219
|
refuse({ kind: "UnknownBraid", braid: braidId }, `batch braid ${braid} is not derived from this descriptor`)
|
|
140
220
|
}
|
|
141
|
-
const braidGen = reader.u64le("braid generation")
|
|
142
|
-
const prev =
|
|
221
|
+
const braidGen = generation(reader.u64le("braid generation"))
|
|
222
|
+
const prev = digest32(reader.bytes(32, "prev"))
|
|
143
223
|
const writer = reader.u64le("writer")
|
|
144
224
|
const timestamp = reader.u64le("timestamp")
|
|
145
225
|
|
|
146
|
-
const opCount = reader
|
|
147
|
-
|
|
148
|
-
|
|
226
|
+
const opCount = readU32(reader, "op count")
|
|
227
|
+
refuseUnbacked(opCount, reader.remaining(), MIN_OP_BYTES, "op count")
|
|
228
|
+
const ops: Op[] = []
|
|
229
|
+
for (let opIndex = 0n; opIndex < opCount; opIndex++) {
|
|
230
|
+
const op = Number(opIndex)
|
|
149
231
|
const kind = reader.u8("op kind")
|
|
150
232
|
if (kind !== OP_KIND.insert && kind !== OP_KIND.delete) {
|
|
151
233
|
refuse(
|
|
152
|
-
{ kind: "UnknownOpKind", op
|
|
153
|
-
`op ${
|
|
234
|
+
{ kind: "UnknownOpKind", op, opKind: kind },
|
|
235
|
+
`op ${op} kind ${kind} is unknown (3 was deleted with floor bumps)`
|
|
154
236
|
)
|
|
155
237
|
}
|
|
156
238
|
const relationId = reader.u32le("op relation")
|
|
157
239
|
const relation = descriptor.relations[relationId]
|
|
158
240
|
if (relation === undefined) {
|
|
159
|
-
refuse(
|
|
160
|
-
{ kind: "UnknownRelation", op: opIndex, relation: relationId },
|
|
161
|
-
`op ${opIndex} cites unknown relation ${relationId}`
|
|
162
|
-
)
|
|
241
|
+
refuse({ kind: "UnknownRelation", op, relation: relationId }, `op ${op} cites unknown relation ${relationId}`)
|
|
163
242
|
}
|
|
164
243
|
if (relation.closed) {
|
|
165
|
-
refuse(
|
|
166
|
-
{ kind: "ClosedRelation", op: opIndex, relation: relationId },
|
|
167
|
-
`op ${opIndex} writes closed relation ${relation.name}`
|
|
168
|
-
)
|
|
244
|
+
refuse({ kind: "ClosedRelation", op, relation: relationId }, `op ${op} writes closed relation ${relation.name}`)
|
|
169
245
|
}
|
|
170
246
|
if (!members.includes(relationId)) {
|
|
171
247
|
refuse(
|
|
172
|
-
{ kind: "OpRelationOutsideBraid", op
|
|
173
|
-
`op ${
|
|
248
|
+
{ kind: "OpRelationOutsideBraid", op, relation: relationId, braid },
|
|
249
|
+
`op ${op} relation ${relation.name} is outside braid ${braid}`
|
|
174
250
|
)
|
|
175
251
|
}
|
|
176
|
-
const rowCount = reader
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
252
|
+
const rowCount = readU32(reader, "row count")
|
|
253
|
+
const minRow = relation.fields.length === 0 ? 0n : 1n
|
|
254
|
+
refuseUnbacked(rowCount, reader.remaining(), minRow, "row count")
|
|
255
|
+
const rows: Value[][] = []
|
|
256
|
+
for (let rowIndex = 0n; rowIndex < rowCount; rowIndex++) {
|
|
257
|
+
const rowAt = Number(rowIndex)
|
|
258
|
+
const row: Value[] = []
|
|
180
259
|
relation.fields.forEach(function readCell(field) {
|
|
181
|
-
const at = { relation: relation.name, row:
|
|
182
|
-
const where = `relation ${relation.name} row ${
|
|
260
|
+
const at = { relation: relation.name, row: rowAt, field: field.name }
|
|
261
|
+
const where = `relation ${relation.name} row ${rowAt} field ${field.name}`
|
|
183
262
|
const refusal: TaggedRefusal = {
|
|
184
263
|
badTag(): never {
|
|
185
264
|
refuse({ kind: "TagMismatch", ...at }, `${where}: tag does not match the layout`)
|
|
@@ -217,9 +296,9 @@ function decodeBatch(theory: LogTheory, bytes: Uint8Array): DecodedBatch {
|
|
|
217
296
|
}
|
|
218
297
|
}
|
|
219
298
|
|
|
220
|
-
interface
|
|
221
|
-
readonly g:
|
|
222
|
-
readonly prev:
|
|
299
|
+
interface ChainEntry {
|
|
300
|
+
readonly g: Generation
|
|
301
|
+
readonly prev: Digest32
|
|
223
302
|
readonly ts: bigint
|
|
224
303
|
}
|
|
225
304
|
|
|
@@ -231,14 +310,14 @@ interface ChainPosition {
|
|
|
231
310
|
* names the misbehaving writer, and the refusal data names the fetched
|
|
232
311
|
* braid.
|
|
233
312
|
*/
|
|
234
|
-
function verifyChain(header: BatchHeader, braid:
|
|
313
|
+
function verifyChain(header: BatchHeader, braid: Braid, slot: Generation, chain: ChainEntry): void {
|
|
235
314
|
if (header.braid !== braid || header.braidGen !== slot) {
|
|
236
315
|
refuseChain(
|
|
237
316
|
{ cause: "slot", braid, slot, writer: header.writer },
|
|
238
317
|
`braid ${braid}: header slot identity ${header.braid}/${header.braidGen} ≠ the fetched key's ${braid}/${slot}`
|
|
239
318
|
)
|
|
240
319
|
}
|
|
241
|
-
if (header.prev
|
|
320
|
+
if (!bytesEqual(header.prev, chain.prev)) {
|
|
242
321
|
refuseChain(
|
|
243
322
|
{ cause: "prev", braid, slot, writer: header.writer },
|
|
244
323
|
`braid ${braid} slot ${slot}: prev does not cite the predecessor`
|
|
@@ -252,5 +331,5 @@ function verifyChain(header: BatchHeader, braid: string, slot: bigint, chain: Ch
|
|
|
252
331
|
}
|
|
253
332
|
}
|
|
254
333
|
|
|
255
|
-
export type { BatchHeader,
|
|
334
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, Digest32, EncodeHeader, Op }
|
|
256
335
|
export { decodeBatch, encodeBatch, verifyChain }
|