@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/manifest.ts
CHANGED
|
@@ -1,113 +1,209 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The protocol's one mutable object
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The protocol's one mutable object: a v:3 binary record — version
|
|
3
|
+
* byte 3, a branded Digest32 fingerprint, an optional branded Digest32
|
|
4
|
+
* checkpoint. The checkpoint record is digest-keyed and immutable:
|
|
5
|
+
* version byte 3, a count-bounded braid roster (id, g, hash, ts) in
|
|
6
|
+
* ascending id order, catalog digest, writer, optional prev. Every
|
|
7
|
+
* digest field is Digest32. The content address is blake3 of the
|
|
8
|
+
* record bytes. Seed audits the catalog claim when a checkpoint is
|
|
9
|
+
* present.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import * as errors from "@superbuilders/errors"
|
|
10
|
-
import {
|
|
13
|
+
import type { Digest32 } from "#bytes.ts"
|
|
14
|
+
import { ByteReader, ByteWriter, bytesEqual, hex32 } from "#bytes.ts"
|
|
15
|
+
import type { Braid } from "#descriptor.ts"
|
|
16
|
+
import { braidHex } from "#descriptor.ts"
|
|
11
17
|
import { refuse } from "#errors.ts"
|
|
18
|
+
import type { Generation } from "#keys.ts"
|
|
19
|
+
import { generation } from "#keys.ts"
|
|
20
|
+
import { Vector } from "#vector.ts"
|
|
21
|
+
|
|
22
|
+
const DOC_VERSION = 3
|
|
23
|
+
const U32_MAX = 0xffffffffn
|
|
24
|
+
/** braid u32 + g u64 + hash 32 + ts u64 */
|
|
25
|
+
const MIN_HEAD_BYTES = 52n
|
|
12
26
|
|
|
13
27
|
interface Manifest {
|
|
14
|
-
readonly fingerprint:
|
|
15
|
-
readonly checkpoint:
|
|
28
|
+
readonly fingerprint: Digest32
|
|
29
|
+
readonly checkpoint: Digest32 | null
|
|
16
30
|
}
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
function readerOf(bytes: Uint8Array): ByteReader {
|
|
33
|
+
return new ByteReader(bytes, {
|
|
34
|
+
fail(what: string): never {
|
|
35
|
+
refuse({ kind: "Malformed", at: bytes.length }, `document truncated at ${what}`)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
23
38
|
}
|
|
24
39
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
29
|
-
if (decoded.error) {
|
|
30
|
-
refuse({ kind: "ManifestShape" }, "manifest is not UTF-8")
|
|
40
|
+
function finish(reader: ByteReader, at: string): void {
|
|
41
|
+
if (reader.remaining() !== 0) {
|
|
42
|
+
refuse({ kind: "Malformed", at: reader.remaining() }, `${reader.remaining()} trailing bytes after ${at}`)
|
|
31
43
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function refuseUnbacked(count: bigint, remaining: number, minItem: bigint, at: string): void {
|
|
47
|
+
if (count === 0n) {
|
|
48
|
+
return
|
|
37
49
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
refuse({ kind: "ManifestShape" }, "manifest carries no version")
|
|
50
|
+
if (minItem === 0n || BigInt(remaining) / minItem < count) {
|
|
51
|
+
refuse({ kind: "Malformed", at: remaining }, `declared ${at} ${count} outruns the remaining ${remaining} bytes`)
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeOptionalDigest(out: ByteWriter, digest: Digest32 | null): void {
|
|
56
|
+
if (digest === null) {
|
|
57
|
+
out.u8(0)
|
|
58
|
+
return
|
|
44
59
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
out.u8(1)
|
|
61
|
+
out.array32(digest)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readOptionalDigest(reader: ByteReader, at: string): Digest32 | null {
|
|
65
|
+
const tag = reader.u8(at)
|
|
66
|
+
if (tag === 0) {
|
|
67
|
+
return null
|
|
49
68
|
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
69
|
+
if (tag === 1) {
|
|
70
|
+
return reader.array32(at)
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
refuse({ kind: "Flags", flags: tag }, `${at} presence is not 0 or 1`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readVersion(reader: ByteReader, at: string): void {
|
|
76
|
+
const version = reader.u8("version")
|
|
77
|
+
if (version !== DOC_VERSION) {
|
|
78
|
+
refuse(
|
|
79
|
+
{ kind: "Version", version },
|
|
80
|
+
`${at} version ${version}, consumers refuse every version other than ${DOC_VERSION}`
|
|
81
|
+
)
|
|
57
82
|
}
|
|
58
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function braidIdOf(id: Braid): number {
|
|
86
|
+
return Number.parseInt(id.slice(1), 16)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderManifest(manifest: Manifest): Uint8Array {
|
|
90
|
+
const out = new ByteWriter(66)
|
|
91
|
+
out.u8(DOC_VERSION)
|
|
92
|
+
out.array32(manifest.fingerprint)
|
|
93
|
+
writeOptionalDigest(out, manifest.checkpoint)
|
|
94
|
+
return out.finish()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseManifest(bytes: Uint8Array): Manifest {
|
|
98
|
+
const reader = readerOf(bytes)
|
|
99
|
+
readVersion(reader, "manifest")
|
|
100
|
+
const fingerprint = reader.array32("fingerprint")
|
|
101
|
+
const checkpoint = readOptionalDigest(reader, "checkpoint")
|
|
102
|
+
finish(reader, "the manifest")
|
|
103
|
+
return { fingerprint, checkpoint }
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
interface CheckpointHead {
|
|
62
|
-
readonly g:
|
|
63
|
-
readonly hash:
|
|
107
|
+
readonly g: Generation
|
|
108
|
+
readonly hash: Digest32
|
|
64
109
|
readonly ts: bigint
|
|
65
110
|
}
|
|
66
111
|
|
|
67
112
|
interface CheckpointFacts {
|
|
68
|
-
readonly braids: ReadonlyMap<
|
|
69
|
-
readonly catalog:
|
|
113
|
+
readonly braids: ReadonlyMap<Braid, CheckpointHead>
|
|
114
|
+
readonly catalog: Digest32
|
|
70
115
|
readonly writer: bigint
|
|
71
|
-
readonly prev:
|
|
116
|
+
readonly prev: Digest32 | null
|
|
117
|
+
readonly sum: bigint
|
|
72
118
|
}
|
|
73
119
|
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
120
|
+
function vectorOfHeads(braids: ReadonlyMap<Braid, CheckpointHead>): Vector {
|
|
121
|
+
const counts = new Map<Braid, bigint>()
|
|
122
|
+
for (const [id, head] of braids) {
|
|
123
|
+
counts.set(id, head.g)
|
|
124
|
+
}
|
|
125
|
+
return Vector.from(counts)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkpointVector(facts: CheckpointFacts): Vector {
|
|
129
|
+
return vectorOfHeads(facts.braids)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderCheckpoint(facts: CheckpointFacts): Uint8Array {
|
|
133
|
+
const braids = [...facts.braids.keys()].sort()
|
|
134
|
+
const count = BigInt(braids.length)
|
|
135
|
+
if (count > U32_MAX) {
|
|
136
|
+
throw errors.new(`checkpoint braid count ${count} exceeds u32`)
|
|
80
137
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
138
|
+
const out = new ByteWriter(1 + 4 + braids.length * 52 + 32 + 8 + 1 + 32)
|
|
139
|
+
out.u8(DOC_VERSION)
|
|
140
|
+
out.u32le(Number(count))
|
|
141
|
+
for (const id of braids) {
|
|
142
|
+
const head = facts.braids.get(id)
|
|
143
|
+
if (head === undefined) {
|
|
144
|
+
throw errors.new(`checkpoint lost braid ${id}`)
|
|
145
|
+
}
|
|
146
|
+
out.u32le(braidIdOf(id))
|
|
147
|
+
out.u64le(head.g)
|
|
148
|
+
out.array32(head.hash)
|
|
149
|
+
out.u64le(head.ts)
|
|
85
150
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
151
|
+
out.array32(facts.catalog)
|
|
152
|
+
out.u64le(facts.writer)
|
|
153
|
+
writeOptionalDigest(out, facts.prev)
|
|
154
|
+
return out.finish()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseCheckpoint(bytes: Uint8Array, known?: ReadonlySet<Braid>): CheckpointFacts {
|
|
158
|
+
const reader = readerOf(bytes)
|
|
159
|
+
readVersion(reader, "checkpoint")
|
|
160
|
+
const count = BigInt(reader.u32le("braid count"))
|
|
161
|
+
refuseUnbacked(count, reader.remaining(), MIN_HEAD_BYTES, "braid count")
|
|
162
|
+
const braids = new Map<Braid, CheckpointHead>()
|
|
163
|
+
for (let i = 0n; i < count; i++) {
|
|
164
|
+
const raw = reader.u32le("braid")
|
|
165
|
+
const name = braidHex(raw)
|
|
166
|
+
if (known !== undefined && !known.has(name)) {
|
|
167
|
+
refuse({ kind: "UnknownBraid", braid: raw }, `checkpoint cites unknown braid ${name}`)
|
|
90
168
|
}
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
refuse(
|
|
169
|
+
const g = reader.u64le("generation")
|
|
170
|
+
const hash = reader.array32("hash")
|
|
171
|
+
const ts = reader.u64le("timestamp")
|
|
172
|
+
const last = [...braids.keys()].at(-1)
|
|
173
|
+
if (last !== undefined && last >= name) {
|
|
174
|
+
refuse(
|
|
175
|
+
{ kind: "Malformed", at: bytes.length - reader.remaining() },
|
|
176
|
+
"checkpoint braid roster is not strictly ascending"
|
|
177
|
+
)
|
|
97
178
|
}
|
|
98
|
-
braids.set(
|
|
179
|
+
braids.set(name, { g: generation(g), hash, ts })
|
|
180
|
+
}
|
|
181
|
+
const catalog = reader.array32("catalog")
|
|
182
|
+
const writer = reader.u64le("writer")
|
|
183
|
+
const prev = readOptionalDigest(reader, "prev")
|
|
184
|
+
finish(reader, "the checkpoint")
|
|
185
|
+
const summed = vectorOfHeads(braids).sum()
|
|
186
|
+
if (typeof summed !== "bigint") {
|
|
187
|
+
refuse({ kind: "Overflow" }, "checkpoint vector sum overflows u64")
|
|
99
188
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
189
|
+
return { braids, catalog, writer, prev, sum: summed }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The seed transition's catalog claim: a present checkpoint
|
|
193
|
+
* compares the opened store's `catalog_digest` against the document.
|
|
194
|
+
* Genesis (no checkpoint) has no claim. A mismatch names the publisher. */
|
|
195
|
+
function auditCatalog(facts: CheckpointFacts | null, computed: Digest32): void {
|
|
196
|
+
if (facts === null) {
|
|
197
|
+
return
|
|
105
198
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
199
|
+
if (bytesEqual(facts.catalog, computed)) {
|
|
200
|
+
return
|
|
108
201
|
}
|
|
109
|
-
|
|
202
|
+
refuse(
|
|
203
|
+
{ kind: "CheckpointDigest", expected: hex32(facts.catalog), computed: hex32(computed) },
|
|
204
|
+
`checkpoint catalog ${hex32(facts.catalog)} disagrees with the opened store ${hex32(computed)}; publisher ${facts.writer}`
|
|
205
|
+
)
|
|
110
206
|
}
|
|
111
207
|
|
|
112
208
|
export type { CheckpointFacts, CheckpointHead, Manifest }
|
|
113
|
-
export { parseCheckpoint, parseManifest, renderManifest }
|
|
209
|
+
export { auditCatalog, checkpointVector, DOC_VERSION, parseCheckpoint, parseManifest, renderCheckpoint, renderManifest }
|