@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/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
export type { Braid } from "#braids.ts"
|
|
12
12
|
export { braid, braidsOf, serialAtStatementsOf } from "#braids.ts"
|
|
13
|
-
export type { BatchHeader, ChainEntry, DecodedBatch, Op } from "#codec.ts"
|
|
13
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, EncodeHeader, Op } from "#codec.ts"
|
|
14
14
|
export { decodeBatch, encodeBatch, verifyChain } from "#codec.ts"
|
|
15
15
|
export type { Descriptor, SerialStatement, Theory } from "#descriptor.ts"
|
|
16
16
|
export { descriptorOf } from "#descriptor.ts"
|
|
@@ -21,14 +21,25 @@ export {
|
|
|
21
21
|
ErrChainMismatch,
|
|
22
22
|
ErrContention,
|
|
23
23
|
ErrGapDetected,
|
|
24
|
+
ErrManifestMissing,
|
|
24
25
|
ErrRefused,
|
|
25
26
|
ErrReplayDiverged,
|
|
26
27
|
ErrSpanningCommit,
|
|
27
28
|
ErrStore,
|
|
29
|
+
isManifestMissing,
|
|
28
30
|
refusalOf
|
|
29
31
|
} from "#errors.ts"
|
|
30
32
|
export type { Generation, StoreKey } from "#keys.ts"
|
|
31
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
CKPT_SCRATCH_LEASE,
|
|
35
|
+
encodeCkptScratch,
|
|
36
|
+
generation,
|
|
37
|
+
LEASE_NAMESPACE,
|
|
38
|
+
parseCkptScratch,
|
|
39
|
+
scratchCkptDigest,
|
|
40
|
+
scratchCkptName,
|
|
41
|
+
storeKey
|
|
42
|
+
} from "#keys.ts"
|
|
32
43
|
export type { OpenReplicaOptions, Replica } from "#replica.ts"
|
|
33
44
|
export { openReplica } from "#replica.ts"
|
|
34
45
|
export type { Create, Etag, Fetched, ObjectStore, Poll, S3Config, S3Credentials, Swap } from "#store.ts"
|
|
@@ -36,5 +47,7 @@ export { etag, fsStore, memStore, s3Store } from "#store.ts"
|
|
|
36
47
|
export type { OpenTenantsOptions, Tenants } from "#tenants.ts"
|
|
37
48
|
export { openTenants } from "#tenants.ts"
|
|
38
49
|
export type { Interval, Value } from "#value.ts"
|
|
50
|
+
export type { CheckpointOrder, VectorParse } from "#vector.ts"
|
|
51
|
+
export { Overflow, Vector } from "#vector.ts"
|
|
39
52
|
export type { Batch, BraidOutcome, Commit, CommitSplit, Durability, Writer } from "#writer.ts"
|
|
40
53
|
export { openWriter } from "#writer.ts"
|
package/src/keys.ts
CHANGED
|
@@ -3,37 +3,164 @@
|
|
|
3
3
|
* chars; a prefix is a store; a tenant is a prefix under `t/`. A
|
|
4
4
|
* StoreKey is parsed here, once — the verbs take the proof and never
|
|
5
5
|
* re-check. An empty prefix joins as the rest alone, so a leading
|
|
6
|
-
* slash is unrepresentable.
|
|
6
|
+
* slash is unrepresentable. Temps and leases live under a reserved
|
|
7
|
+
* tilde-family first segment no StoreKey can spell — ASCII `~` and
|
|
8
|
+
* its lookalikes — so those namespaces stay disjoint from every
|
|
9
|
+
* honest key. Format characters, line and paragraph separators, and
|
|
10
|
+
* space separators cannot hide a reserved prefix or a `.lock` suffix.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import * as errors from "@superbuilders/errors"
|
|
10
|
-
import {
|
|
14
|
+
import { regex } from "arkregex"
|
|
15
|
+
import type { Digest32 } from "#bytes.ts"
|
|
16
|
+
import { digest32, hex32, U64_MAX } from "#bytes.ts"
|
|
11
17
|
import type { Braid } from "#descriptor.ts"
|
|
12
18
|
|
|
13
|
-
/**
|
|
19
|
+
/** A segment wearing this suffix — after format characters are stripped — is not a key. */
|
|
14
20
|
const LOCK_SUFFIX = ".lock"
|
|
15
21
|
|
|
22
|
+
/** Reserved first-segment names no StoreKey can spell. Temps and leases live here. */
|
|
23
|
+
const TEMP_NAMESPACE = "~tmp"
|
|
24
|
+
const LEASE_NAMESPACE = "~lease"
|
|
25
|
+
|
|
26
|
+
/** ASCII tilde and lookalikes that can spell a reserved first segment. */
|
|
27
|
+
const TILDE_FAMILY = new Set([
|
|
28
|
+
"~",
|
|
29
|
+
"\u02DC",
|
|
30
|
+
"\u02F7",
|
|
31
|
+
"\u1FC0",
|
|
32
|
+
"\u2053",
|
|
33
|
+
"\u223C",
|
|
34
|
+
"\u223D",
|
|
35
|
+
"\u223F",
|
|
36
|
+
"\u2E1E",
|
|
37
|
+
"\u2E1F",
|
|
38
|
+
"\u301C",
|
|
39
|
+
"\u3030",
|
|
40
|
+
"\uFE4B",
|
|
41
|
+
"\uFE4F",
|
|
42
|
+
"\uFF5E"
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
const FORMAT_OR_SEPARATOR = regex("[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Zs}]", "u")
|
|
46
|
+
const FORMAT_CHARS = regex("\\p{Cf}", "gu")
|
|
47
|
+
const SLASH_TRIM = regex("^/+|/+$", "g")
|
|
48
|
+
|
|
16
49
|
declare const storeKeyBrand: unique symbol
|
|
17
50
|
type StoreKey = string & { readonly [storeKeyBrand]: typeof storeKeyBrand }
|
|
18
51
|
|
|
19
52
|
declare const generationBrand: unique symbol
|
|
20
53
|
type Generation = bigint & { readonly [generationBrand]: typeof generationBrand }
|
|
21
54
|
|
|
55
|
+
function firstCodePoint(seg: string): string | undefined {
|
|
56
|
+
for (const ch of seg) {
|
|
57
|
+
return ch
|
|
58
|
+
}
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tildeFamilyPrefix(seg: string): boolean {
|
|
63
|
+
const first = firstCodePoint(seg)
|
|
64
|
+
if (first === undefined) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
return TILDE_FAMILY.has(first) || first.normalize("NFKC") === "~"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stripFormat(seg: string): string {
|
|
71
|
+
return seg.replace(FORMAT_CHARS, "")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One path segment of a key or a tenant id: the same grammar. */
|
|
75
|
+
function segmentOk(seg: string): boolean {
|
|
76
|
+
if (seg.length === 0 || seg.includes("/") || seg === "." || seg === "..") {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
if (FORMAT_OR_SEPARATOR.test(seg)) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
const stripped = stripFormat(seg)
|
|
83
|
+
if (stripped.length === 0 || stripped === "." || stripped === "..") {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
return !tildeFamilyPrefix(stripped) && !stripped.endsWith(LOCK_SUFFIX)
|
|
87
|
+
}
|
|
88
|
+
|
|
22
89
|
function storeKey(raw: string): StoreKey {
|
|
23
|
-
|
|
90
|
+
const wellFormed = raw.length > 0 && !raw.startsWith("/") && !raw.endsWith("/") && raw.split("/").every(segmentOk)
|
|
91
|
+
if (!wellFormed) {
|
|
24
92
|
throw errors.new(`store key is not a slash path: ${raw}`)
|
|
25
93
|
}
|
|
26
|
-
for (const segment of raw.split("/")) {
|
|
27
|
-
if (segment.length === 0 || segment === "." || segment === "..") {
|
|
28
|
-
throw errors.new(`store key segment is illegal: ${raw}`)
|
|
29
|
-
}
|
|
30
|
-
if (segment.endsWith(LOCK_SUFFIX)) {
|
|
31
|
-
throw errors.new(`store key collides with the lockfile suffix: ${raw}`)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
94
|
return raw as StoreKey
|
|
35
95
|
}
|
|
36
96
|
|
|
97
|
+
/** Empty, or a StoreKey spelling (the same segment grammar, no leading or trailing slash). */
|
|
98
|
+
function parsePrefix(raw: string): string {
|
|
99
|
+
if (raw.length === 0) {
|
|
100
|
+
return ""
|
|
101
|
+
}
|
|
102
|
+
return storeKey(raw.replace(SLASH_TRIM, ""))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isReservedName(name: string): boolean {
|
|
106
|
+
return tildeFamilyPrefix(stripFormat(name))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reservedTemp(pid: number, seq: number): string {
|
|
110
|
+
return `${TEMP_NAMESPACE}/${String(pid)}.${String(seq)}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reservedLease(key: string, token: bigint): string {
|
|
114
|
+
return `${LEASE_NAMESPACE}/${key}/${String(token)}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A slash path whose first segment is `~tmp` or `~lease`. Not a StoreKey. */
|
|
118
|
+
function reservedName(raw: string): string {
|
|
119
|
+
const segs = raw.split("/")
|
|
120
|
+
if (segs.length === 0 || segs.some((seg) => seg.length === 0 || seg === "." || seg === "..")) {
|
|
121
|
+
throw errors.new(`reserved name is not a slash path: ${raw}`)
|
|
122
|
+
}
|
|
123
|
+
const first = segs[0]
|
|
124
|
+
if (first !== TEMP_NAMESPACE && first !== LEASE_NAMESPACE) {
|
|
125
|
+
throw errors.new(`reserved name is not under ${TEMP_NAMESPACE} or ${LEASE_NAMESPACE}: ${raw}`)
|
|
126
|
+
}
|
|
127
|
+
return raw
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Known scratch-lease document under `~lease`. One path, no LIST. */
|
|
131
|
+
const CKPT_SCRATCH_LEASE = "ckpt-scratch"
|
|
132
|
+
|
|
133
|
+
/** The reserved scratch name: `~lease/ckpt-scratch`. */
|
|
134
|
+
function scratchCkptName(): string {
|
|
135
|
+
return reservedName(`${LEASE_NAMESPACE}/${CKPT_SCRATCH_LEASE}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const SCRATCH_VERSION = 3
|
|
139
|
+
|
|
140
|
+
/** The scratch-lease body: version byte 3, then the 32-byte digest. */
|
|
141
|
+
function encodeCkptScratch(digest: Digest32): Uint8Array {
|
|
142
|
+
const out = new Uint8Array(33)
|
|
143
|
+
out[0] = SCRATCH_VERSION
|
|
144
|
+
out.set(digest, 1)
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The digest a scratch-lease body names, or null. */
|
|
149
|
+
function scratchCkptDigest(bytes: Uint8Array): Digest32 | null {
|
|
150
|
+
if (bytes.length !== 33) {
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
const version = bytes[0]
|
|
154
|
+
if (version !== SCRATCH_VERSION) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
return digest32(bytes.subarray(1))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseCkptScratch(bytes: Uint8Array): Digest32 | null {
|
|
161
|
+
return scratchCkptDigest(bytes)
|
|
162
|
+
}
|
|
163
|
+
|
|
37
164
|
function generation(raw: bigint): Generation {
|
|
38
165
|
if (raw < 0n || raw > U64_MAX) {
|
|
39
166
|
throw errors.new(`generation is a u64: ${raw}`)
|
|
@@ -50,19 +177,19 @@ function assemble(prefix: string, rest: string): StoreKey {
|
|
|
50
177
|
}
|
|
51
178
|
|
|
52
179
|
function manifestKey(prefix: string): StoreKey {
|
|
53
|
-
return assemble(prefix, "manifest
|
|
180
|
+
return assemble(prefix, "manifest")
|
|
54
181
|
}
|
|
55
182
|
|
|
56
183
|
function logKey(prefix: string, braid: Braid, g: Generation): StoreKey {
|
|
57
184
|
return assemble(prefix, `log/${braid}/${hex16(g)}`)
|
|
58
185
|
}
|
|
59
186
|
|
|
60
|
-
function checkpointMdbKey(prefix: string, digest:
|
|
61
|
-
return assemble(prefix, `ckpt/${digest}.mdb`)
|
|
187
|
+
function checkpointMdbKey(prefix: string, digest: Digest32): StoreKey {
|
|
188
|
+
return assemble(prefix, `ckpt/${hex32(digest)}.mdb`)
|
|
62
189
|
}
|
|
63
190
|
|
|
64
|
-
function
|
|
65
|
-
return assemble(prefix, `ckpt/${digest}
|
|
191
|
+
function ckptDocKey(prefix: string, digest: Digest32): StoreKey {
|
|
192
|
+
return assemble(prefix, `ckpt/${hex32(digest)}`)
|
|
66
193
|
}
|
|
67
194
|
|
|
68
195
|
function idsKey(prefix: string, relation: number, field: number): StoreKey {
|
|
@@ -70,19 +197,35 @@ function idsKey(prefix: string, relation: number, field: number): StoreKey {
|
|
|
70
197
|
}
|
|
71
198
|
|
|
72
199
|
function tenantPrefix(root: string, tenant: string): string {
|
|
200
|
+
if (!segmentOk(tenant)) {
|
|
201
|
+
throw errors.new(`tenant id is not a single path segment: ${tenant}`)
|
|
202
|
+
}
|
|
73
203
|
return assemble(root, `t/${tenant}`)
|
|
74
204
|
}
|
|
75
205
|
|
|
76
206
|
export type { Generation, StoreKey }
|
|
77
207
|
export {
|
|
78
|
-
|
|
208
|
+
CKPT_SCRATCH_LEASE,
|
|
79
209
|
checkpointMdbKey,
|
|
210
|
+
ckptDocKey,
|
|
211
|
+
encodeCkptScratch,
|
|
80
212
|
generation,
|
|
81
213
|
hex16,
|
|
82
214
|
idsKey,
|
|
215
|
+
isReservedName,
|
|
216
|
+
LEASE_NAMESPACE,
|
|
83
217
|
LOCK_SUFFIX,
|
|
84
218
|
logKey,
|
|
85
219
|
manifestKey,
|
|
220
|
+
parseCkptScratch,
|
|
221
|
+
parsePrefix,
|
|
222
|
+
reservedLease,
|
|
223
|
+
reservedName,
|
|
224
|
+
reservedTemp,
|
|
225
|
+
scratchCkptDigest,
|
|
226
|
+
scratchCkptName,
|
|
227
|
+
segmentOk,
|
|
86
228
|
storeKey,
|
|
229
|
+
TEMP_NAMESPACE,
|
|
87
230
|
tenantPrefix
|
|
88
231
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,117 +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"
|
|
11
15
|
import type { Braid } from "#descriptor.ts"
|
|
12
|
-
import {
|
|
16
|
+
import { braidHex } from "#descriptor.ts"
|
|
13
17
|
import { refuse } from "#errors.ts"
|
|
14
18
|
import type { Generation } from "#keys.ts"
|
|
15
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
|
|
16
26
|
|
|
17
27
|
interface Manifest {
|
|
18
|
-
readonly fingerprint:
|
|
19
|
-
readonly checkpoint:
|
|
28
|
+
readonly fingerprint: Digest32
|
|
29
|
+
readonly checkpoint: Digest32 | null
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
})
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
33
|
-
if (decoded.error) {
|
|
34
|
-
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}`)
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function refuseUnbacked(count: bigint, remaining: number, minItem: bigint, at: string): void {
|
|
47
|
+
if (count === 0n) {
|
|
48
|
+
return
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
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`)
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeOptionalDigest(out: ByteWriter, digest: Digest32 | null): void {
|
|
56
|
+
if (digest === null) {
|
|
57
|
+
out.u8(0)
|
|
58
|
+
return
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
68
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
69
|
+
if (tag === 1) {
|
|
70
|
+
return reader.array32(at)
|
|
56
71
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
)
|
|
61
82
|
}
|
|
62
|
-
|
|
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 }
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
interface CheckpointHead {
|
|
66
107
|
readonly g: Generation
|
|
67
|
-
readonly hash:
|
|
108
|
+
readonly hash: Digest32
|
|
68
109
|
readonly ts: bigint
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
interface CheckpointFacts {
|
|
72
113
|
readonly braids: ReadonlyMap<Braid, CheckpointHead>
|
|
73
|
-
readonly catalog:
|
|
114
|
+
readonly catalog: Digest32
|
|
74
115
|
readonly writer: bigint
|
|
75
|
-
readonly prev:
|
|
116
|
+
readonly prev: Digest32 | null
|
|
117
|
+
readonly sum: bigint
|
|
76
118
|
}
|
|
77
119
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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`)
|
|
84
137
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|
|
89
150
|
}
|
|
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")
|
|
90
162
|
const braids = new Map<Braid, CheckpointHead>()
|
|
91
|
-
for (
|
|
92
|
-
|
|
93
|
-
|
|
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}`)
|
|
94
168
|
}
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
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
|
+
)
|
|
101
178
|
}
|
|
102
|
-
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")
|
|
103
188
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
109
198
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
199
|
+
if (bytesEqual(facts.catalog, computed)) {
|
|
200
|
+
return
|
|
112
201
|
}
|
|
113
|
-
|
|
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
|
+
)
|
|
114
206
|
}
|
|
115
207
|
|
|
116
208
|
export type { CheckpointFacts, CheckpointHead, Manifest }
|
|
117
|
-
export { parseCheckpoint, parseManifest, renderManifest }
|
|
209
|
+
export { auditCatalog, checkpointVector, DOC_VERSION, parseCheckpoint, parseManifest, renderCheckpoint, renderManifest }
|