@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/src/codec.ts
CHANGED
|
@@ -8,18 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as errors from "@superbuilders/errors"
|
|
11
|
-
import {
|
|
11
|
+
import type { Digest32 } from "#bytes.ts"
|
|
12
|
+
import { ByteReader, ByteWriter, bytesEqual, digest32, hex32, utf8Encoder } from "#bytes.ts"
|
|
12
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
16
|
import type { Generation } from "#keys.ts"
|
|
16
17
|
import { generation } from "#keys.ts"
|
|
17
18
|
import type { TaggedRefusal, Value } from "#value.ts"
|
|
18
|
-
import { readTagged, writeTagged } from "#value.ts"
|
|
19
|
+
import { checkAgainst, readTagged, writeTagged } from "#value.ts"
|
|
19
20
|
|
|
20
21
|
const MAGIC = utf8Encoder.encode("BDBL")
|
|
21
|
-
const VERSION =
|
|
22
|
+
const VERSION = 3
|
|
22
23
|
const OP_KIND = { insert: 1, delete: 2 } as const
|
|
24
|
+
const U32_MAX = 0xffffffffn
|
|
23
25
|
|
|
24
26
|
interface Op {
|
|
25
27
|
readonly op: "insert" | "delete"
|
|
@@ -28,10 +30,25 @@ interface Op {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface BatchHeader {
|
|
31
|
-
readonly fingerprint:
|
|
33
|
+
readonly fingerprint: Digest32
|
|
32
34
|
readonly braid: Braid
|
|
33
35
|
readonly braidGen: Generation
|
|
34
|
-
readonly prev:
|
|
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
|
|
35
52
|
readonly writer: bigint
|
|
36
53
|
readonly timestamp: bigint
|
|
37
54
|
}
|
|
@@ -46,49 +63,86 @@ function braidIdOf(id: Braid): number {
|
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
/**
|
|
49
|
-
* Encodes one batch.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
52
71
|
*/
|
|
53
|
-
function encodeBatch(theory: Theory, header:
|
|
72
|
+
function encodeBatch(theory: Theory, header: EncodeHeader, ops: readonly Op[]): Uint8Array {
|
|
54
73
|
const descriptor = descriptorOf(theory)
|
|
55
|
-
|
|
56
|
-
|
|
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}`)
|
|
57
78
|
}
|
|
58
79
|
const braidId = braidIdOf(header.braid)
|
|
59
80
|
const members = descriptor.braidMembers.get(header.braid)
|
|
60
81
|
if (members === undefined) {
|
|
61
82
|
throw errors.new(`braid ${header.braid} is not derived from this descriptor`)
|
|
62
83
|
}
|
|
63
|
-
for (const op of ops) {
|
|
84
|
+
for (const [opIndex, op] of ops.entries()) {
|
|
64
85
|
const relation = descriptor.relationByName.get(op.relation)
|
|
65
86
|
if (relation === undefined) {
|
|
66
87
|
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
67
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
|
+
}
|
|
68
95
|
if (!members.includes(relation.id)) {
|
|
69
96
|
throw errors.new(`op relation ${op.relation} is outside braid ${header.braid} — a spanning batch is unencodable`)
|
|
70
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`)
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
const out = new ByteWriter(4096)
|
|
74
124
|
out.bytes(MAGIC)
|
|
75
125
|
out.u16le(VERSION)
|
|
76
126
|
out.u16le(0)
|
|
77
|
-
out.bytes(
|
|
127
|
+
out.bytes(fingerprint)
|
|
78
128
|
out.u32le(braidId)
|
|
79
129
|
out.u64le(header.braidGen)
|
|
80
|
-
out.bytes(
|
|
130
|
+
out.bytes(prev)
|
|
81
131
|
out.u64le(header.writer)
|
|
82
132
|
out.u64le(header.timestamp)
|
|
83
|
-
out.u32le(
|
|
133
|
+
out.u32le(Number(opCount))
|
|
84
134
|
for (const op of ops) {
|
|
85
135
|
const relation = descriptor.relationByName.get(op.relation)
|
|
86
136
|
if (relation === undefined) {
|
|
87
137
|
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
88
138
|
}
|
|
139
|
+
const rowCount = BigInt(op.rows.length)
|
|
140
|
+
if (rowCount > U32_MAX) {
|
|
141
|
+
throw errors.new(`encode row count ${rowCount} exceeds u32`)
|
|
142
|
+
}
|
|
89
143
|
out.u8(OP_KIND[op.op])
|
|
90
144
|
out.u32le(relation.id)
|
|
91
|
-
out.u32le(
|
|
145
|
+
out.u32le(Number(rowCount))
|
|
92
146
|
for (const row of op.rows) {
|
|
93
147
|
relation.fields.forEach(function writeCell(field, ordinal) {
|
|
94
148
|
const value = row[ordinal]
|
|
@@ -102,6 +156,34 @@ function encodeBatch(theory: Theory, header: BatchHeader, ops: readonly Op[]): U
|
|
|
102
156
|
return out.finish()
|
|
103
157
|
}
|
|
104
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
|
+
|
|
105
187
|
/** Full parse of a batch object; refusals are typed, never partial reads. */
|
|
106
188
|
function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
107
189
|
const descriptor = descriptorOf(theory)
|
|
@@ -123,10 +205,10 @@ function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
|
123
205
|
if (flags !== 0) {
|
|
124
206
|
refuse({ kind: "Flags", flags }, `batch flags ${flags} must be 0`)
|
|
125
207
|
}
|
|
126
|
-
const fingerprint =
|
|
127
|
-
if (fingerprint
|
|
208
|
+
const fingerprint = digest32(reader.bytes(32, "fingerprint"))
|
|
209
|
+
if (!bytesEqual(fingerprint, descriptor.fingerprintBytes)) {
|
|
128
210
|
refuse(
|
|
129
|
-
{ kind: "FingerprintMismatch", carried: fingerprint, expected: descriptor.fingerprint },
|
|
211
|
+
{ kind: "FingerprintMismatch", carried: hex32(fingerprint), expected: descriptor.fingerprint },
|
|
130
212
|
"batch fingerprint does not match the descriptor"
|
|
131
213
|
)
|
|
132
214
|
}
|
|
@@ -137,47 +219,46 @@ function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
|
137
219
|
refuse({ kind: "UnknownBraid", braid: braidId }, `batch braid ${braid} is not derived from this descriptor`)
|
|
138
220
|
}
|
|
139
221
|
const braidGen = generation(reader.u64le("braid generation"))
|
|
140
|
-
const prev =
|
|
222
|
+
const prev = digest32(reader.bytes(32, "prev"))
|
|
141
223
|
const writer = reader.u64le("writer")
|
|
142
224
|
const timestamp = reader.u64le("timestamp")
|
|
143
225
|
|
|
144
|
-
const opCount = reader
|
|
226
|
+
const opCount = readU32(reader, "op count")
|
|
227
|
+
refuseUnbacked(opCount, reader.remaining(), MIN_OP_BYTES, "op count")
|
|
145
228
|
const ops: Op[] = []
|
|
146
|
-
for (let opIndex =
|
|
229
|
+
for (let opIndex = 0n; opIndex < opCount; opIndex++) {
|
|
230
|
+
const op = Number(opIndex)
|
|
147
231
|
const kind = reader.u8("op kind")
|
|
148
232
|
if (kind !== OP_KIND.insert && kind !== OP_KIND.delete) {
|
|
149
233
|
refuse(
|
|
150
|
-
{ kind: "UnknownOpKind", op
|
|
151
|
-
`op ${
|
|
234
|
+
{ kind: "UnknownOpKind", op, opKind: kind },
|
|
235
|
+
`op ${op} kind ${kind} is unknown (3 was deleted with floor bumps)`
|
|
152
236
|
)
|
|
153
237
|
}
|
|
154
238
|
const relationId = reader.u32le("op relation")
|
|
155
239
|
const relation = descriptor.relations[relationId]
|
|
156
240
|
if (relation === undefined) {
|
|
157
|
-
refuse(
|
|
158
|
-
{ kind: "UnknownRelation", op: opIndex, relation: relationId },
|
|
159
|
-
`op ${opIndex} cites unknown relation ${relationId}`
|
|
160
|
-
)
|
|
241
|
+
refuse({ kind: "UnknownRelation", op, relation: relationId }, `op ${op} cites unknown relation ${relationId}`)
|
|
161
242
|
}
|
|
162
243
|
if (relation.closed) {
|
|
163
|
-
refuse(
|
|
164
|
-
{ kind: "ClosedRelation", op: opIndex, relation: relationId },
|
|
165
|
-
`op ${opIndex} writes closed relation ${relation.name}`
|
|
166
|
-
)
|
|
244
|
+
refuse({ kind: "ClosedRelation", op, relation: relationId }, `op ${op} writes closed relation ${relation.name}`)
|
|
167
245
|
}
|
|
168
246
|
if (!members.includes(relationId)) {
|
|
169
247
|
refuse(
|
|
170
|
-
{ kind: "OpRelationOutsideBraid", op
|
|
171
|
-
`op ${
|
|
248
|
+
{ kind: "OpRelationOutsideBraid", op, relation: relationId, braid },
|
|
249
|
+
`op ${op} relation ${relation.name} is outside braid ${braid}`
|
|
172
250
|
)
|
|
173
251
|
}
|
|
174
|
-
const rowCount = reader
|
|
252
|
+
const rowCount = readU32(reader, "row count")
|
|
253
|
+
const minRow = relation.fields.length === 0 ? 0n : 1n
|
|
254
|
+
refuseUnbacked(rowCount, reader.remaining(), minRow, "row count")
|
|
175
255
|
const rows: Value[][] = []
|
|
176
|
-
for (let rowIndex =
|
|
256
|
+
for (let rowIndex = 0n; rowIndex < rowCount; rowIndex++) {
|
|
257
|
+
const rowAt = Number(rowIndex)
|
|
177
258
|
const row: Value[] = []
|
|
178
259
|
relation.fields.forEach(function readCell(field) {
|
|
179
|
-
const at = { relation: relation.name, row:
|
|
180
|
-
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}`
|
|
181
262
|
const refusal: TaggedRefusal = {
|
|
182
263
|
badTag(): never {
|
|
183
264
|
refuse({ kind: "TagMismatch", ...at }, `${where}: tag does not match the layout`)
|
|
@@ -217,7 +298,7 @@ function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
|
217
298
|
|
|
218
299
|
interface ChainEntry {
|
|
219
300
|
readonly g: Generation
|
|
220
|
-
readonly prev:
|
|
301
|
+
readonly prev: Digest32
|
|
221
302
|
readonly ts: bigint
|
|
222
303
|
}
|
|
223
304
|
|
|
@@ -236,7 +317,7 @@ function verifyChain(header: BatchHeader, braid: Braid, slot: Generation, chain:
|
|
|
236
317
|
`braid ${braid}: header slot identity ${header.braid}/${header.braidGen} ≠ the fetched key's ${braid}/${slot}`
|
|
237
318
|
)
|
|
238
319
|
}
|
|
239
|
-
if (header.prev
|
|
320
|
+
if (!bytesEqual(header.prev, chain.prev)) {
|
|
240
321
|
refuseChain(
|
|
241
322
|
{ cause: "prev", braid, slot, writer: header.writer },
|
|
242
323
|
`braid ${braid} slot ${slot}: prev does not cite the predecessor`
|
|
@@ -250,5 +331,5 @@ function verifyChain(header: BatchHeader, braid: Braid, slot: Generation, chain:
|
|
|
250
331
|
}
|
|
251
332
|
}
|
|
252
333
|
|
|
253
|
-
export type { BatchHeader, ChainEntry, DecodedBatch, Op }
|
|
334
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, Digest32, EncodeHeader, Op }
|
|
254
335
|
export { decodeBatch, encodeBatch, verifyChain }
|