@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/errors.ts
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* causes carried as data properties read back with the `*Of` accessors —
|
|
5
5
|
* never by message-string matching. There is deliberately no
|
|
6
6
|
* `ErrAlreadyApplied`: the state it would name is absorbed by idempotent
|
|
7
|
-
* replay (L10) and never surfaces.
|
|
7
|
+
* replay (L10) and never surfaces. ManifestMissing, Ambiguous, OverWidth,
|
|
8
|
+
* Exhausted, and SlotRetired are named sums of their own: a replica
|
|
9
|
+
* without a manifest, an unproved conditional write, a draw past the
|
|
10
|
+
* lease width or the u64 ceiling, and a below-floor create.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import * as errors from "@superbuilders/errors"
|
|
14
|
+
import type { Braid } from "#descriptor.ts"
|
|
15
|
+
import type { Generation, StoreKey } from "#keys.ts"
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Typed refusal of a protocol object before any apply: batch shape,
|
|
@@ -35,6 +40,25 @@ const ErrChainMismatch = errors.new("bumbledb-log chainMismatch: the batch viola
|
|
|
35
40
|
/** Bounded live-tip losses exhausted — an operational signal, not an outcome arm. */
|
|
36
41
|
const ErrContention = errors.new("bumbledb-log contention: consecutive live-tip losses exhausted the bound")
|
|
37
42
|
|
|
43
|
+
/** A replica (or any read-role handle) found no manifest; only the writer births a store. */
|
|
44
|
+
export const ErrManifestMissing = errors.new("bumbledb-log manifestMissing: the store has no manifest")
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A conditional write the transport cannot prove (S3 409, timeout, a
|
|
48
|
+
* retried PUT). The machine GET-verifies; this identity is the unproved
|
|
49
|
+
* arm, never a proved Exists or Moved.
|
|
50
|
+
*/
|
|
51
|
+
const ErrAmbiguous = errors.new("bumbledb-log ambiguous: the conditional write is unproved")
|
|
52
|
+
|
|
53
|
+
/** A single id draw larger than one lease width; the width is the one block size. */
|
|
54
|
+
const ErrOverWidth = errors.new("bumbledb-log overWidth: the draw exceeds the lease width")
|
|
55
|
+
|
|
56
|
+
/** The next lease would leave u64 — the id space is spent. */
|
|
57
|
+
const ErrExhausted = errors.new("bumbledb-log exhausted: the id space is spent")
|
|
58
|
+
|
|
59
|
+
/** A put_create at a slot below the published checkpoint vector. */
|
|
60
|
+
const ErrSlotRetired = errors.new("bumbledb-log slotRetired: the slot is retired")
|
|
61
|
+
|
|
38
62
|
/** The vendor channel: I/O and store infrastructure failures. */
|
|
39
63
|
const ErrStore = errors.new("bumbledb-log store failure")
|
|
40
64
|
|
|
@@ -54,26 +78,28 @@ type RefusalCause =
|
|
|
54
78
|
| { readonly kind: "UnknownOpKind"; readonly op: number; readonly opKind: number }
|
|
55
79
|
| { readonly kind: "UnknownRelation"; readonly op: number; readonly relation: number }
|
|
56
80
|
| { readonly kind: "ClosedRelation"; readonly op: number; readonly relation: number }
|
|
57
|
-
| { readonly kind: "OpRelationOutsideBraid"; readonly op: number; readonly relation: number; readonly braid:
|
|
81
|
+
| { readonly kind: "OpRelationOutsideBraid"; readonly op: number; readonly relation: number; readonly braid: Braid }
|
|
58
82
|
| { readonly kind: "TagMismatch"; readonly relation: string; readonly row: number; readonly field: string }
|
|
59
83
|
| { readonly kind: "BoolByte"; readonly relation: string; readonly row: number; readonly field: string }
|
|
60
84
|
| { readonly kind: "InvalidUtf8"; readonly relation: string; readonly row: number; readonly field: string }
|
|
61
85
|
| { readonly kind: "EmptyInterval"; readonly relation: string; readonly row: number; readonly field: string }
|
|
62
86
|
| { readonly kind: "IntervalOverflow"; readonly relation: string; readonly row: number; readonly field: string }
|
|
63
87
|
| { readonly kind: "TrailingBytes"; readonly bytes: number }
|
|
64
|
-
| { readonly kind: "
|
|
88
|
+
| { readonly kind: "DigestWidth" }
|
|
89
|
+
| { readonly kind: "Arity"; readonly op: number; readonly relation: string; readonly row: number }
|
|
90
|
+
| { readonly kind: "Malformed"; readonly at: number }
|
|
91
|
+
| { readonly kind: "Overflow" }
|
|
65
92
|
| { readonly kind: "ManifestVersion"; readonly version: number }
|
|
66
|
-
| { readonly kind: "CheckpointShape" }
|
|
67
93
|
| { readonly kind: "CheckpointBraids"; readonly carried: readonly string[]; readonly derived: readonly string[] }
|
|
68
94
|
| { readonly kind: "CheckpointDigest"; readonly expected: string; readonly computed: string }
|
|
69
|
-
| { readonly kind: "NoOpSlot"; readonly braid:
|
|
95
|
+
| { readonly kind: "NoOpSlot"; readonly braid: Braid; readonly slot: Generation; readonly writer: bigint }
|
|
70
96
|
|
|
71
97
|
type ChainCause = "prev" | "slot" | "timestamp"
|
|
72
98
|
|
|
73
99
|
interface ChainMismatchData {
|
|
74
100
|
readonly cause: ChainCause
|
|
75
|
-
readonly braid:
|
|
76
|
-
readonly slot:
|
|
101
|
+
readonly braid: Braid
|
|
102
|
+
readonly slot: Generation
|
|
77
103
|
readonly writer: bigint
|
|
78
104
|
}
|
|
79
105
|
|
|
@@ -83,16 +109,46 @@ type ContentionCause =
|
|
|
83
109
|
readonly statement: string
|
|
84
110
|
readonly determinants: ReadonlyArray<Readonly<Record<string, unknown>>>
|
|
85
111
|
}
|
|
86
|
-
| { readonly kind: "slot-race"; readonly tip:
|
|
112
|
+
| { readonly kind: "slot-race"; readonly tip: Generation }
|
|
87
113
|
|
|
88
114
|
interface ContentionData {
|
|
89
|
-
readonly braid:
|
|
115
|
+
readonly braid: Braid
|
|
90
116
|
readonly cause: ContentionCause
|
|
91
117
|
}
|
|
92
118
|
|
|
119
|
+
type AmbiguousVerb = "create" | "swap"
|
|
120
|
+
|
|
121
|
+
interface AmbiguousData {
|
|
122
|
+
readonly verb: AmbiguousVerb
|
|
123
|
+
readonly key: StoreKey
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface OverWidthData {
|
|
127
|
+
readonly requested: bigint
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ExhaustedData {
|
|
131
|
+
readonly relation: number
|
|
132
|
+
readonly field: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface SlotRetiredData {
|
|
136
|
+
readonly braid: Braid
|
|
137
|
+
readonly slot: Generation
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The id-lease algebra's refusal arms: OverWidth | Exhausted. */
|
|
141
|
+
type LeaseRefusal =
|
|
142
|
+
| { readonly kind: "OverWidth"; readonly requested: bigint }
|
|
143
|
+
| { readonly kind: "Exhausted"; readonly relation: number; readonly field: number }
|
|
144
|
+
|
|
93
145
|
const refusalData = new WeakMap<Error, RefusalCause>()
|
|
94
146
|
const chainData = new WeakMap<Error, ChainMismatchData>()
|
|
95
147
|
const contentionData = new WeakMap<Error, ContentionData>()
|
|
148
|
+
const ambiguousData = new WeakMap<Error, AmbiguousData>()
|
|
149
|
+
const overWidthData = new WeakMap<Error, OverWidthData>()
|
|
150
|
+
const exhaustedData = new WeakMap<Error, ExhaustedData>()
|
|
151
|
+
const slotRetiredData = new WeakMap<Error, SlotRetiredData>()
|
|
96
152
|
|
|
97
153
|
function refuse(cause: RefusalCause, detail: string): never {
|
|
98
154
|
const error = errors.wrap(ErrRefused, detail)
|
|
@@ -112,6 +168,38 @@ function throwContention(data: ContentionData, detail: string): never {
|
|
|
112
168
|
throw error
|
|
113
169
|
}
|
|
114
170
|
|
|
171
|
+
function refuseManifestMissing(detail: string): never {
|
|
172
|
+
throw errors.wrap(ErrManifestMissing, detail)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isManifestMissing(error: unknown): boolean {
|
|
176
|
+
return error instanceof Error && errors.is(error, ErrManifestMissing)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function throwAmbiguous(data: AmbiguousData, detail: string): never {
|
|
180
|
+
const error = errors.wrap(ErrAmbiguous, detail)
|
|
181
|
+
ambiguousData.set(error, data)
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function refuseOverWidth(data: OverWidthData, detail: string): never {
|
|
186
|
+
const error = errors.wrap(ErrOverWidth, detail)
|
|
187
|
+
overWidthData.set(error, data)
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function refuseExhausted(data: ExhaustedData, detail: string): never {
|
|
192
|
+
const error = errors.wrap(ErrExhausted, detail)
|
|
193
|
+
exhaustedData.set(error, data)
|
|
194
|
+
throw error
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function refuseSlotRetired(data: SlotRetiredData, detail: string): never {
|
|
198
|
+
const error = errors.wrap(ErrSlotRetired, detail)
|
|
199
|
+
slotRetiredData.set(error, data)
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
202
|
+
|
|
115
203
|
function refusalOf(error: Error): RefusalCause | undefined {
|
|
116
204
|
return refusalData.get(error)
|
|
117
205
|
}
|
|
@@ -124,6 +212,22 @@ function contentionOf(error: Error): ContentionData | undefined {
|
|
|
124
212
|
return contentionData.get(error)
|
|
125
213
|
}
|
|
126
214
|
|
|
215
|
+
function ambiguousOf(error: Error): AmbiguousData | undefined {
|
|
216
|
+
return ambiguousData.get(error)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function overWidthOf(error: Error): OverWidthData | undefined {
|
|
220
|
+
return overWidthData.get(error)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function exhaustedOf(error: Error): ExhaustedData | undefined {
|
|
224
|
+
return exhaustedData.get(error)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function slotRetiredOf(error: Error): SlotRetiredData | undefined {
|
|
228
|
+
return slotRetiredData.get(error)
|
|
229
|
+
}
|
|
230
|
+
|
|
127
231
|
/** Every store failure wraps the exported sentinel itself, so
|
|
128
232
|
* `errors.is(e, ErrStore)` matches by identity; the vendor error's
|
|
129
233
|
* message rides the detail verbatim. */
|
|
@@ -131,20 +235,46 @@ function wrapStore(inner: Error, detail: string): Error {
|
|
|
131
235
|
return errors.wrap(ErrStore, `${detail}: ${inner.message}`)
|
|
132
236
|
}
|
|
133
237
|
|
|
134
|
-
export type {
|
|
238
|
+
export type {
|
|
239
|
+
AmbiguousData,
|
|
240
|
+
AmbiguousVerb,
|
|
241
|
+
ChainCause,
|
|
242
|
+
ChainMismatchData,
|
|
243
|
+
ContentionCause,
|
|
244
|
+
ContentionData,
|
|
245
|
+
ExhaustedData,
|
|
246
|
+
LeaseRefusal,
|
|
247
|
+
OverWidthData,
|
|
248
|
+
RefusalCause,
|
|
249
|
+
SlotRetiredData
|
|
250
|
+
}
|
|
135
251
|
export {
|
|
252
|
+
ambiguousOf,
|
|
136
253
|
chainMismatchOf,
|
|
137
254
|
contentionOf,
|
|
255
|
+
ErrAmbiguous,
|
|
138
256
|
ErrChainMismatch,
|
|
139
257
|
ErrContention,
|
|
258
|
+
ErrExhausted,
|
|
140
259
|
ErrGapDetected,
|
|
260
|
+
ErrOverWidth,
|
|
141
261
|
ErrRefused,
|
|
142
262
|
ErrReplayDiverged,
|
|
263
|
+
ErrSlotRetired,
|
|
143
264
|
ErrSpanningCommit,
|
|
144
265
|
ErrStore,
|
|
266
|
+
exhaustedOf,
|
|
267
|
+
isManifestMissing,
|
|
268
|
+
overWidthOf,
|
|
145
269
|
refusalOf,
|
|
146
270
|
refuse,
|
|
147
271
|
refuseChain,
|
|
272
|
+
refuseExhausted,
|
|
273
|
+
refuseManifestMissing,
|
|
274
|
+
refuseOverWidth,
|
|
275
|
+
refuseSlotRetired,
|
|
276
|
+
slotRetiredOf,
|
|
277
|
+
throwAmbiguous,
|
|
148
278
|
throwContention,
|
|
149
279
|
wrapStore
|
|
150
280
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* bumbledb, a thin peer of the engine SDK. Public surface: the pure
|
|
4
4
|
* protocol pair (`encodeBatch`/`decodeBatch`, `braidsOf`) mirrored
|
|
5
5
|
* byte-exactly against the Rust driver, the five-verb object store
|
|
6
|
-
* with its tier-1 `fsStore`, and `openReplica`/`openWriter`/
|
|
6
|
+
* with its tier-1 `fsStore`, in-process `memStore`, and the `s3Store` AWS S3 client, and `openReplica`/`openWriter`/
|
|
7
7
|
* `openTenants` composed from the engine SDK's own verbs — the replica
|
|
8
8
|
* hands out the SDK's `Db`, and no engine surface is duplicated.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export type { Braid } from "#braids.ts"
|
|
12
|
-
export { braidsOf, serialAtStatementsOf } from "#braids.ts"
|
|
13
|
-
export type { BatchHeader,
|
|
12
|
+
export { braid, braidsOf, serialAtStatementsOf } from "#braids.ts"
|
|
13
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, EncodeHeader, Op } from "#codec.ts"
|
|
14
14
|
export { decodeBatch, encodeBatch, verifyChain } from "#codec.ts"
|
|
15
|
-
export type {
|
|
15
|
+
export type { Descriptor, SerialStatement, Theory } from "#descriptor.ts"
|
|
16
16
|
export { descriptorOf } from "#descriptor.ts"
|
|
17
17
|
export type { ChainCause, ChainMismatchData, ContentionCause, ContentionData, RefusalCause } from "#errors.ts"
|
|
18
18
|
export {
|
|
@@ -21,18 +21,33 @@ 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"
|
|
32
|
+
export type { Generation, StoreKey } from "#keys.ts"
|
|
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"
|
|
30
43
|
export type { OpenReplicaOptions, Replica } from "#replica.ts"
|
|
31
44
|
export { openReplica } from "#replica.ts"
|
|
32
|
-
export type { Create, Fetched, ObjectStore, Poll, Swap } from "#store.ts"
|
|
33
|
-
export { fsStore } from "#store.ts"
|
|
45
|
+
export type { Create, Etag, Fetched, ObjectStore, Poll, S3Config, S3Credentials, Swap } from "#store.ts"
|
|
46
|
+
export { etag, fsStore, memStore, s3Store } from "#store.ts"
|
|
34
47
|
export type { OpenTenantsOptions, Tenants } from "#tenants.ts"
|
|
35
48
|
export { openTenants } from "#tenants.ts"
|
|
36
|
-
export type {
|
|
37
|
-
export type {
|
|
49
|
+
export type { Interval, Value } from "#value.ts"
|
|
50
|
+
export type { CheckpointOrder, VectorParse } from "#vector.ts"
|
|
51
|
+
export { Overflow, Vector } from "#vector.ts"
|
|
52
|
+
export type { Batch, BraidOutcome, Commit, CommitSplit, Durability, Writer } from "#writer.ts"
|
|
38
53
|
export { openWriter } from "#writer.ts"
|
package/src/keys.ts
CHANGED
|
@@ -1,34 +1,231 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The key layout
|
|
3
|
-
*
|
|
2
|
+
* The key layout: generation numbers zero-padded lowercase hex, 16
|
|
3
|
+
* chars; a prefix is a store; a tenant is a prefix under `t/`. A
|
|
4
|
+
* StoreKey is parsed here, once — the verbs take the proof and never
|
|
5
|
+
* re-check. An empty prefix joins as the rest alone, so a leading
|
|
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.
|
|
4
11
|
*/
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
import * as errors from "@superbuilders/errors"
|
|
14
|
+
import { regex } from "arkregex"
|
|
15
|
+
import type { Digest32 } from "#bytes.ts"
|
|
16
|
+
import { digest32, hex32, U64_MAX } from "#bytes.ts"
|
|
17
|
+
import type { Braid } from "#descriptor.ts"
|
|
18
|
+
|
|
19
|
+
/** A segment wearing this suffix — after format characters are stripped — is not a key. */
|
|
20
|
+
const LOCK_SUFFIX = ".lock"
|
|
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
|
+
|
|
49
|
+
declare const storeKeyBrand: unique symbol
|
|
50
|
+
type StoreKey = string & { readonly [storeKeyBrand]: typeof storeKeyBrand }
|
|
51
|
+
|
|
52
|
+
declare const generationBrand: unique symbol
|
|
53
|
+
type Generation = bigint & { readonly [generationBrand]: typeof generationBrand }
|
|
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
|
+
|
|
89
|
+
function storeKey(raw: string): StoreKey {
|
|
90
|
+
const wellFormed = raw.length > 0 && !raw.startsWith("/") && !raw.endsWith("/") && raw.split("/").every(segmentOk)
|
|
91
|
+
if (!wellFormed) {
|
|
92
|
+
throw errors.new(`store key is not a slash path: ${raw}`)
|
|
93
|
+
}
|
|
94
|
+
return raw as StoreKey
|
|
95
|
+
}
|
|
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
|
+
|
|
164
|
+
function generation(raw: bigint): Generation {
|
|
165
|
+
if (raw < 0n || raw > U64_MAX) {
|
|
166
|
+
throw errors.new(`generation is a u64: ${raw}`)
|
|
167
|
+
}
|
|
168
|
+
return raw as Generation
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hex16(g: Generation): string {
|
|
7
172
|
return g.toString(16).padStart(16, "0")
|
|
8
173
|
}
|
|
9
174
|
|
|
10
|
-
function
|
|
11
|
-
return `${prefix}
|
|
175
|
+
function assemble(prefix: string, rest: string): StoreKey {
|
|
176
|
+
return storeKey(prefix.length === 0 ? rest : `${prefix}/${rest}`)
|
|
12
177
|
}
|
|
13
178
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
179
|
+
function manifestKey(prefix: string): StoreKey {
|
|
180
|
+
return assemble(prefix, "manifest")
|
|
16
181
|
}
|
|
17
182
|
|
|
18
|
-
function
|
|
19
|
-
return
|
|
183
|
+
function logKey(prefix: string, braid: Braid, g: Generation): StoreKey {
|
|
184
|
+
return assemble(prefix, `log/${braid}/${hex16(g)}`)
|
|
20
185
|
}
|
|
21
186
|
|
|
22
|
-
function
|
|
23
|
-
return
|
|
187
|
+
function checkpointMdbKey(prefix: string, digest: Digest32): StoreKey {
|
|
188
|
+
return assemble(prefix, `ckpt/${hex32(digest)}.mdb`)
|
|
24
189
|
}
|
|
25
190
|
|
|
26
|
-
function
|
|
27
|
-
return
|
|
191
|
+
function ckptDocKey(prefix: string, digest: Digest32): StoreKey {
|
|
192
|
+
return assemble(prefix, `ckpt/${hex32(digest)}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function idsKey(prefix: string, relation: number, field: number): StoreKey {
|
|
196
|
+
return assemble(prefix, `ids/${relation.toString(16).padStart(8, "0")}/${field.toString(16).padStart(4, "0")}`)
|
|
28
197
|
}
|
|
29
198
|
|
|
30
199
|
function tenantPrefix(root: string, tenant: string): string {
|
|
31
|
-
|
|
200
|
+
if (!segmentOk(tenant)) {
|
|
201
|
+
throw errors.new(`tenant id is not a single path segment: ${tenant}`)
|
|
202
|
+
}
|
|
203
|
+
return assemble(root, `t/${tenant}`)
|
|
32
204
|
}
|
|
33
205
|
|
|
34
|
-
export {
|
|
206
|
+
export type { Generation, StoreKey }
|
|
207
|
+
export {
|
|
208
|
+
CKPT_SCRATCH_LEASE,
|
|
209
|
+
checkpointMdbKey,
|
|
210
|
+
ckptDocKey,
|
|
211
|
+
encodeCkptScratch,
|
|
212
|
+
generation,
|
|
213
|
+
hex16,
|
|
214
|
+
idsKey,
|
|
215
|
+
isReservedName,
|
|
216
|
+
LEASE_NAMESPACE,
|
|
217
|
+
LOCK_SUFFIX,
|
|
218
|
+
logKey,
|
|
219
|
+
manifestKey,
|
|
220
|
+
parseCkptScratch,
|
|
221
|
+
parsePrefix,
|
|
222
|
+
reservedLease,
|
|
223
|
+
reservedName,
|
|
224
|
+
reservedTemp,
|
|
225
|
+
scratchCkptDigest,
|
|
226
|
+
scratchCkptName,
|
|
227
|
+
segmentOk,
|
|
228
|
+
storeKey,
|
|
229
|
+
TEMP_NAMESPACE,
|
|
230
|
+
tenantPrefix
|
|
231
|
+
}
|