@bjornpagen/bumbledb-log 0.20.0 → 0.20.2

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.
Files changed (76) hide show
  1. package/dist/braids.d.ts +28 -0
  2. package/dist/braids.d.ts.map +1 -0
  3. package/{src/braids.ts → dist/braids.js} +22 -29
  4. package/dist/braids.js.map +1 -0
  5. package/dist/bytes.d.ts +26 -0
  6. package/dist/bytes.d.ts.map +1 -0
  7. package/dist/bytes.js +86 -0
  8. package/dist/bytes.js.map +1 -0
  9. package/dist/chain.d.ts +56 -0
  10. package/dist/chain.d.ts.map +1 -0
  11. package/dist/chain.js +137 -0
  12. package/dist/chain.js.map +1 -0
  13. package/dist/codec.d.ts +71 -0
  14. package/dist/codec.d.ts.map +1 -0
  15. package/dist/codec.js +179 -0
  16. package/dist/codec.js.map +1 -0
  17. package/dist/descriptor.d.ts +61 -0
  18. package/dist/descriptor.d.ts.map +1 -0
  19. package/dist/descriptor.js +160 -0
  20. package/dist/descriptor.js.map +1 -0
  21. package/dist/errors.d.ts +202 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +122 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/index.d.ts +34 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +24 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/keys.d.ts +52 -0
  30. package/dist/keys.d.ts.map +1 -0
  31. package/dist/keys.js +166 -0
  32. package/dist/keys.js.map +1 -0
  33. package/dist/manifest.d.ts +42 -0
  34. package/dist/manifest.d.ts.map +1 -0
  35. package/dist/manifest.js +116 -0
  36. package/dist/manifest.js.map +1 -0
  37. package/dist/replica.d.ts +154 -0
  38. package/dist/replica.d.ts.map +1 -0
  39. package/dist/replica.js +820 -0
  40. package/dist/replica.js.map +1 -0
  41. package/dist/store-s3.d.ts +31 -0
  42. package/dist/store-s3.d.ts.map +1 -0
  43. package/dist/store-s3.js +299 -0
  44. package/dist/store-s3.js.map +1 -0
  45. package/dist/store.d.ts +129 -0
  46. package/dist/store.d.ts.map +1 -0
  47. package/dist/store.js +577 -0
  48. package/dist/store.js.map +1 -0
  49. package/dist/tenants.d.ts +56 -0
  50. package/dist/tenants.d.ts.map +1 -0
  51. package/dist/tenants.js +333 -0
  52. package/dist/tenants.js.map +1 -0
  53. package/dist/tilde-family.json +21 -0
  54. package/dist/vector.d.ts +31 -0
  55. package/dist/vector.d.ts.map +1 -0
  56. package/dist/vector.js +87 -0
  57. package/dist/vector.js.map +1 -0
  58. package/dist/writer.d.ts +106 -0
  59. package/dist/writer.d.ts.map +1 -0
  60. package/dist/writer.js +677 -0
  61. package/dist/writer.js.map +1 -0
  62. package/package.json +12 -7
  63. package/src/bytes.ts +0 -115
  64. package/src/chain.ts +0 -181
  65. package/src/codec.ts +0 -252
  66. package/src/descriptor.ts +0 -240
  67. package/src/errors.ts +0 -309
  68. package/src/index.ts +0 -57
  69. package/src/keys.ts +0 -218
  70. package/src/manifest.ts +0 -161
  71. package/src/replica.ts +0 -1061
  72. package/src/store-s3.ts +0 -377
  73. package/src/store.ts +0 -727
  74. package/src/tenants.ts +0 -414
  75. package/src/vector.ts +0 -103
  76. package/src/writer.ts +0 -977
package/src/store.ts DELETED
@@ -1,727 +0,0 @@
1
- /**
2
- * The object-store capability: exactly five verbs, outcomes as sums,
3
- * infrastructure failures on the ErrStore channel. `fsStore` is tier-1,
4
- * not a dev double — deployment case 5's production backend.
5
- *
6
- * The one on-disk protocol, shared with the Rust driver and pinned by
7
- * the `lease/` corpus goldens: create-only publishes an exclusive
8
- * synced temp (under the reserved `~tmp` namespace) with link(2), where
9
- * EEXIST is the honest exists; the etag is the blake3 of the content,
10
- * lowercase hex, computed on every read and never stored. The mutation
11
- * lock is a fenced CAS lease: a versioned `LEASE/1` body whose identity
12
- * is a monotonic token file `{root}/~lease/{key}/{n}`, with `~head`
13
- * naming the current token. A contender mints the next token iff the
14
- * current lease's own bytes are expired — expiry is the only break.
15
- * Release rewrites the held token with an already-expired body so the
16
- * next acquirer does not wait us out. `created` and `swapped` resolve
17
- * only after fsync of the object file and its parent directory,
18
- * including newly created ancestors. Stale temps are swept at open.
19
- */
20
-
21
- import * as fs from "node:fs/promises"
22
- import * as path from "node:path"
23
- import { internalBlake3 } from "@bjornpagen/bumbledb"
24
- import * as errors from "@superbuilders/errors"
25
- import { regex } from "arkregex"
26
- import { bytesEqual, toHex, U64_MAX } from "#bytes.ts"
27
- import { wrapStore } from "#errors.ts"
28
- import type { StoreKey } from "#keys.ts"
29
- import { LEASE_NAMESPACE, reservedLease, reservedTemp, TEMP_NAMESPACE } from "#keys.ts"
30
-
31
- declare const etagBrand: unique symbol
32
- type Etag = string & { readonly [etagBrand]: typeof etagBrand }
33
-
34
- function etag(raw: string): Etag {
35
- return raw as Etag
36
- }
37
-
38
- interface Fetched {
39
- readonly bytes: Uint8Array
40
- readonly etag: Etag
41
- }
42
-
43
- type Poll = { readonly tag: "unchanged" } | { readonly tag: "changed"; readonly fetched: Fetched }
44
-
45
- type Create =
46
- | { readonly tag: "created"; readonly etag: Etag }
47
- | { readonly tag: "exists" }
48
- | { readonly tag: "ambiguous" }
49
-
50
- type Swap = { readonly tag: "swapped"; readonly etag: Etag } | { readonly tag: "moved" } | { readonly tag: "ambiguous" }
51
-
52
- interface Lease {
53
- readonly holder: bigint
54
- readonly token: bigint
55
- readonly expires: bigint
56
- }
57
-
58
- type CreateProbe =
59
- | { readonly tag: "landed"; readonly etag: Etag }
60
- | { readonly tag: "lost"; readonly fetched: Fetched }
61
- | { readonly tag: "absent" }
62
-
63
- type SwapProbe =
64
- | { readonly tag: "landed"; readonly etag: Etag }
65
- | { readonly tag: "lost"; readonly fetched: Fetched }
66
- | { readonly tag: "absent" }
67
-
68
- interface ObjectStore {
69
- /** GET; null on 404. */
70
- get(key: StoreKey): Promise<Fetched | null>
71
- /** GET with If-None-Match — the cheap manifest poll. */
72
- getIfChanged(key: StoreKey, etag: Etag): Promise<Poll>
73
- /** PUT with If-None-Match: * — the log-slot arbitration primitive. */
74
- putCreate(key: StoreKey, bytes: Uint8Array): Promise<Create>
75
- /** PUT with If-Match — the manifest CAS primitive. */
76
- putSwap(key: StoreKey, bytes: Uint8Array, etag: Etag): Promise<Swap>
77
- /** DELETE, unconditional — the gc verb's tool. */
78
- delete(key: StoreKey): Promise<void>
79
- }
80
-
81
- /** A held fenced lease: identity is the token file `{dir}/{token}`. */
82
- interface FsLease {
83
- readonly root: string
84
- readonly dir: string
85
- readonly holder: bigint
86
- readonly token: bigint
87
- readonly path: string
88
- }
89
-
90
- /** Ceiling of the jittered wait between probes of an unexpired lease. */
91
- const LOCK_RETRY_MS = 10
92
-
93
- /** How long a mutation lease stays current, in milliseconds. */
94
- const MUTATION_TTL_MS = 5_000
95
-
96
- /** A live temp under `~tmp` exists only for write-then-link. Anything
97
- * older than this is crash litter the open sweep deletes. */
98
- const TEMP_STALE_MS = 30_000
99
-
100
- /** `~head` is not a StoreKey. It names the current token so a
101
- * successor reads `{dir}/{n}` without listing. */
102
- const HEAD = "~head"
103
-
104
- function ourHolder(): bigint {
105
- return BigInt(process.pid)
106
- }
107
-
108
- function contentEtag(bytes: Uint8Array): Etag {
109
- return etag(toHex(new Uint8Array(internalBlake3(bytes))))
110
- }
111
-
112
- function codeOf(error: Error): string | undefined {
113
- return (error as NodeJS.ErrnoException).code
114
- }
115
-
116
- /** The lease body's magic first line. Version 1 of the one lock protocol. */
117
- const LEASE_MAGIC = "LEASE/1"
118
-
119
- function encodeLease(lease: Lease): Uint8Array {
120
- return new TextEncoder().encode(`${LEASE_MAGIC}\n${lease.holder}\n${lease.token}\n${lease.expires}\n`)
121
- }
122
-
123
- const U64_DECIMAL = regex("^\\d+$")
124
-
125
- function u64Line(line: string): bigint | null {
126
- if (!U64_DECIMAL.test(line)) {
127
- return null
128
- }
129
- const value = BigInt(line)
130
- if (value > U64_MAX) {
131
- return null
132
- }
133
- return value
134
- }
135
-
136
- /** Lines as Rust `str::lines`: split on `\n`, one trailing terminator
137
- * unyielded, a trailing `\r` stripped per line. */
138
- function leaseLines(raw: string): string[] {
139
- const parts = raw.split("\n")
140
- if (parts.length > 0 && parts[parts.length - 1] === "") {
141
- parts.pop()
142
- }
143
- return parts.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line))
144
- }
145
-
146
- /** The `LEASE/1` body: magic line, then holder, token, expires as
147
- * decimal u64 lines, and nothing after. Anything else is not a lease
148
- * and never breakable. */
149
- function parseLease(raw: string): Lease | null {
150
- const lines = leaseLines(raw)
151
- if (lines.length !== 4) {
152
- return null
153
- }
154
- const [magic, holderLine, tokenLine, expiresLine] = lines
155
- if (magic !== LEASE_MAGIC || holderLine === undefined || tokenLine === undefined || expiresLine === undefined) {
156
- return null
157
- }
158
- const holder = u64Line(holderLine)
159
- const token = u64Line(tokenLine)
160
- const expires = u64Line(expiresLine)
161
- if (holder === null || token === null || expires === null) {
162
- return null
163
- }
164
- return { holder, token, expires }
165
- }
166
-
167
- /** Expiry of the lease's own bytes: the only break. */
168
- function leaseExpired(lease: Lease, nowMs: number): boolean {
169
- return lease.expires <= BigInt(nowMs)
170
- }
171
-
172
- function isUnproved(error: Error): boolean {
173
- const code = codeOf(error)
174
- return code === "EIO" || code === "EINTR" || code === "ETIMEDOUT" || code === "EAGAIN" || code === "EBUSY"
175
- }
176
-
177
- /** A temp or parent that vanished mid-link is not Exists and not Created. */
178
- function isVanished(error: Error): boolean {
179
- return codeOf(error) === "ENOENT"
180
- }
181
-
182
- async function fsyncDir(dir: string): Promise<void> {
183
- const handle = await fs.open(dir, "r")
184
- const synced = await errors.try(handle.sync())
185
- await handle.close()
186
- if (synced.error) {
187
- throw errors.wrap(synced.error, `fsync directory ${dir}`)
188
- }
189
- }
190
-
191
- async function ensureParent(target: string, root: string): Promise<void> {
192
- const dir = path.dirname(target)
193
- const resolvedRoot = path.resolve(root)
194
- const ancestors: string[] = []
195
- let cursor = dir
196
- while (cursor.startsWith(resolvedRoot) && cursor !== resolvedRoot) {
197
- ancestors.push(cursor)
198
- cursor = path.dirname(cursor)
199
- }
200
- const missing: string[] = []
201
- for (const ancestor of ancestors.reverse()) {
202
- const st = await errors.try(fs.stat(ancestor))
203
- if (st.error) {
204
- if (codeOf(st.error) === "ENOENT") {
205
- missing.push(ancestor)
206
- continue
207
- }
208
- throw st.error
209
- }
210
- }
211
- await fs.mkdir(dir, { recursive: true })
212
- for (const created of missing) {
213
- await fsyncDir(path.dirname(created))
214
- }
215
- }
216
-
217
- let tempSeq = 0
218
-
219
- /** Write `bytes` to a fresh `wx` temp under `{root}/~tmp` and fsync it.
220
- * The caller publishes the synced temp atomically. */
221
- async function syncedTemp(root: string, bytes: Uint8Array): Promise<string> {
222
- tempSeq += 1
223
- const temp = path.join(root, ...reservedTemp(process.pid, tempSeq).split("/"))
224
- await fs.mkdir(path.dirname(temp), { recursive: true })
225
- const handle = await fs.open(temp, "wx")
226
- const written = await errors.try(
227
- (async function writeAll() {
228
- await handle.writeFile(bytes)
229
- await handle.sync()
230
- })()
231
- )
232
- await handle.close()
233
- if (written.error) {
234
- await fs.rm(temp, { force: true })
235
- throw errors.wrap(written.error, `write temp for ${root}`)
236
- }
237
- return temp
238
- }
239
-
240
- /** link(2) is the exclusivity primitive: rename replaces an existing
241
- * destination, so it cannot arbitrate create-only, while link fails
242
- * atomically with EEXIST across processes — exactly the
243
- * If-None-Match: * contract. */
244
- async function publishLink(temp: string, dest: string): Promise<"linked" | "occupied"> {
245
- const linked = await errors.try(fs.link(temp, dest))
246
- if (linked.error === undefined) {
247
- return "linked"
248
- }
249
- if (codeOf(linked.error) === "EEXIST") {
250
- return "occupied"
251
- }
252
- throw linked.error
253
- }
254
-
255
- /** The lease directory for `key`: `{root}/~lease/{key}`. */
256
- function leaseDir(root: string, key: string): string {
257
- return path.join(root, LEASE_NAMESPACE, ...key.split("/"))
258
- }
259
-
260
- function tokenPath(dir: string, token: bigint): string {
261
- return path.join(dir, String(token))
262
- }
263
-
264
- function headPath(dir: string): string {
265
- return path.join(dir, HEAD)
266
- }
267
-
268
- async function readHead(dir: string): Promise<bigint | null> {
269
- const read = await errors.try(fs.readFile(headPath(dir), "utf8"))
270
- if (read.error) {
271
- if (codeOf(read.error) === "ENOENT") {
272
- return null
273
- }
274
- throw read.error
275
- }
276
- const token = u64Line(read.data.trim())
277
- if (token === null || token < 1n) {
278
- return null
279
- }
280
- return token
281
- }
282
-
283
- async function writeHead(root: string, dir: string, token: bigint): Promise<void> {
284
- const dest = headPath(dir)
285
- const temp = await syncedTemp(root, new TextEncoder().encode(String(token)))
286
- const replaced = await errors.try(fs.rename(temp, dest))
287
- if (replaced.error) {
288
- await fs.rm(temp, { force: true })
289
- throw replaced.error
290
- }
291
- await fsyncDir(dir)
292
- }
293
-
294
- /** Removes `{dir}/{1..=current-1}` after `~head` names `current`. */
295
- async function forgetPredecessors(dir: string, current: bigint): Promise<void> {
296
- for (let token = current - 1n; token >= 1n; token -= 1n) {
297
- await fs.rm(tokenPath(dir, token), { force: true })
298
- }
299
- }
300
-
301
- interface CurrentLease {
302
- readonly token: bigint
303
- readonly lease: Lease
304
- }
305
-
306
- function probeFrom(dir: string, start: bigint): Promise<CurrentLease | null> {
307
- return (async function probe(): Promise<CurrentLease | null> {
308
- let best: CurrentLease | null = null
309
- let token = start
310
- while (token <= U64_MAX) {
311
- const read = await errors.try(fs.readFile(tokenPath(dir, token), "utf8"))
312
- if (read.error) {
313
- if (codeOf(read.error) === "ENOENT") {
314
- break
315
- }
316
- token += 1n
317
- continue
318
- }
319
- const lease = parseLease(read.data)
320
- if (lease !== null) {
321
- best = { token, lease }
322
- }
323
- token += 1n
324
- }
325
- return best
326
- })()
327
- }
328
-
329
- /** The current lease is `{dir}/{n}` for the token `~head` names, or
330
- * the highest `{dir}/{n}` at or after that hint. A mint past a stale
331
- * head is still visible: the probe opens `n`, `n+1`, … until a gap. */
332
- async function currentLease(dir: string): Promise<CurrentLease | null> {
333
- const start = (await readHead(dir)) ?? 1n
334
- const found = await probeFrom(dir, start)
335
- if (found === null && start > 1n) {
336
- return await probeFrom(dir, 1n)
337
- }
338
- return found
339
- }
340
-
341
- async function tryMint(
342
- root: string,
343
- dir: string,
344
- key: string,
345
- token: bigint,
346
- holder: bigint,
347
- ttlMs: number
348
- ): Promise<boolean> {
349
- await fs.mkdir(dir, { recursive: true })
350
- const body = encodeLease({ holder, token, expires: BigInt(Date.now() + ttlMs) })
351
- const dest = path.join(root, ...reservedLease(key, token).split("/"))
352
- const temp = await syncedTemp(root, body)
353
- const published = await errors.try(publishLink(temp, dest))
354
- await fs.rm(temp, { force: true })
355
- if (published.error) {
356
- throw published.error
357
- }
358
- if (published.data === "occupied") {
359
- return false
360
- }
361
- await fsyncDir(dir)
362
- const headed = await errors.try(writeHead(root, dir, token))
363
- if (headed.error === undefined) {
364
- await forgetPredecessors(dir, token)
365
- }
366
- return true
367
- }
368
-
369
- /** Acquire the fenced lease on `{root}/~lease/{key}`: mint the next
370
- * monotonic token iff the current lease's own bytes are expired.
371
- * `wait` sleeps out a live holder; `refuse` throws. */
372
- async function acquireFsLease(
373
- root: string,
374
- key: string,
375
- ttlMs: number,
376
- contend: "wait" | "refuse" = "wait"
377
- ): Promise<FsLease> {
378
- const dir = leaseDir(root, key)
379
- const holder = ourHolder()
380
- for (;;) {
381
- const current = await currentLease(dir)
382
- if (current !== null && !leaseExpired(current.lease, Date.now())) {
383
- if (contend === "refuse") {
384
- throw errors.new("replica directory has an owner")
385
- }
386
- await new Promise(function later(resolve) {
387
- setTimeout(resolve, Math.random() * LOCK_RETRY_MS)
388
- })
389
- continue
390
- }
391
- const token = current === null ? 1n : current.token + 1n
392
- const minted = await tryMint(root, dir, key, token, holder, ttlMs)
393
- if (!minted) {
394
- continue
395
- }
396
- return { root, dir, holder, token, path: tokenPath(dir, token) }
397
- }
398
- }
399
-
400
- /** True iff this token is still the max — a stale holder lost the CAS
401
- * and must not publish. */
402
- async function stillCurrent(held: FsLease): Promise<boolean> {
403
- const current = await currentLease(held.dir)
404
- return current !== null && current.token === held.token
405
- }
406
-
407
- /** Release by rewriting the held token with an already-expired body so
408
- * the next acquirer does not wait us out. */
409
- async function releaseFsLease(held: FsLease): Promise<void> {
410
- const body = encodeLease({ holder: held.holder, token: held.token, expires: 0n })
411
- const wrote = await errors.try(syncedTemp(held.root, body))
412
- if (wrote.error) {
413
- return
414
- }
415
- const replaced = await errors.try(fs.rename(wrote.data, held.path))
416
- if (replaced.error) {
417
- await fs.rm(wrote.data, { force: true })
418
- return
419
- }
420
- const synced = await errors.try(fsyncDir(held.dir))
421
- if (synced.error) {
422
- return
423
- }
424
- }
425
-
426
- async function sweepStaleTemps(dir: string): Promise<void> {
427
- const listed = await errors.try(fs.readdir(dir, { withFileTypes: true }))
428
- if (listed.error) {
429
- if (codeOf(listed.error) === "ENOENT") {
430
- return
431
- }
432
- throw listed.error
433
- }
434
- const now = Date.now()
435
- for (const entry of listed.data) {
436
- if (!entry.isFile()) {
437
- continue
438
- }
439
- const full = path.join(dir, entry.name)
440
- const st = await errors.try(fs.stat(full))
441
- if (st.error) {
442
- continue
443
- }
444
- if (now - st.data.mtimeMs > TEMP_STALE_MS) {
445
- await fs.rm(full, { force: true })
446
- }
447
- }
448
- }
449
-
450
- /** Sweep crash litter: stale temps under `~tmp`, and superseded tokens
451
- * directly under `~lease` once `~head` names the current one. */
452
- async function sweepReserved(root: string): Promise<void> {
453
- await sweepStaleTemps(path.join(root, TEMP_NAMESPACE))
454
- const dir = path.join(root, LEASE_NAMESPACE)
455
- const current = await currentLease(dir)
456
- if (current !== null) {
457
- await forgetPredecessors(dir, current.token)
458
- }
459
- }
460
-
461
- async function resolveAmbiguousCreate(store: ObjectStore, key: StoreKey, attempted: Uint8Array): Promise<CreateProbe> {
462
- const fetched = await store.get(key)
463
- if (fetched === null) {
464
- return { tag: "absent" }
465
- }
466
- if (bytesEqual(fetched.bytes, attempted)) {
467
- return { tag: "landed", etag: fetched.etag }
468
- }
469
- return { tag: "lost", fetched }
470
- }
471
-
472
- async function resolveAmbiguousSwap(store: ObjectStore, key: StoreKey, attempted: Uint8Array): Promise<SwapProbe> {
473
- const fetched = await store.get(key)
474
- if (fetched === null) {
475
- return { tag: "absent" }
476
- }
477
- if (bytesEqual(fetched.bytes, attempted)) {
478
- return { tag: "landed", etag: fetched.etag }
479
- }
480
- return { tag: "lost", fetched }
481
- }
482
-
483
- function cloneFetched(fetched: Fetched): Fetched {
484
- return { bytes: new Uint8Array(fetched.bytes), etag: fetched.etag }
485
- }
486
-
487
- /** The five verbs over one local directory. One machine is load-bearing. */
488
- function fsStore(root: string): ObjectStore {
489
- const rootPath = path.resolve(root)
490
- const swept = (async function open() {
491
- await sweepReserved(rootPath)
492
- await fs.mkdir(path.join(rootPath, TEMP_NAMESPACE), { recursive: true })
493
- await fs.mkdir(path.join(rootPath, LEASE_NAMESPACE), { recursive: true })
494
- })()
495
-
496
- function objectPath(key: StoreKey): string {
497
- return path.join(rootPath, ...key.split("/"))
498
- }
499
-
500
- async function readFetched(target: string): Promise<Fetched | null> {
501
- const read = await errors.try(fs.readFile(target))
502
- if (read.error) {
503
- if (codeOf(read.error) === "ENOENT") {
504
- return null
505
- }
506
- throw read.error
507
- }
508
- const bytes = new Uint8Array(read.data)
509
- return { bytes, etag: contentEtag(bytes) }
510
- }
511
-
512
- async function withKeyLease<T>(key: StoreKey, body: (held: FsLease) => Promise<T>): Promise<T> {
513
- const held = await acquireFsLease(rootPath, key, MUTATION_TTL_MS)
514
- const ran = await errors.try(body(held))
515
- await releaseFsLease(held)
516
- if (ran.error) {
517
- throw ran.error
518
- }
519
- return ran.data
520
- }
521
-
522
- return {
523
- async get(key) {
524
- await swept
525
- const target = objectPath(key)
526
- const read = await errors.try(readFetched(target))
527
- if (read.error) {
528
- throw wrapStore(read.error, `get ${key}`)
529
- }
530
- return read.data
531
- },
532
-
533
- async getIfChanged(key, etag) {
534
- await swept
535
- const target = objectPath(key)
536
- const read = await errors.try(readFetched(target))
537
- if (read.error) {
538
- throw wrapStore(read.error, `getIfChanged ${key}`)
539
- }
540
- if (read.data === null) {
541
- throw wrapStore(errors.new("poll target absent"), `getIfChanged ${key}`)
542
- }
543
- if (read.data.etag === etag) {
544
- return { tag: "unchanged" }
545
- }
546
- return { tag: "changed", fetched: read.data }
547
- },
548
-
549
- async putCreate(key, bytes) {
550
- await swept
551
- const target = objectPath(key)
552
- const ran = await errors.try(
553
- (async function createBody(): Promise<Create> {
554
- return await withKeyLease(key, async function underLease(held): Promise<Create> {
555
- if (!(await stillCurrent(held))) {
556
- return { tag: "ambiguous" }
557
- }
558
- await ensureParent(target, rootPath)
559
- const temp = await syncedTemp(rootPath, bytes)
560
- const published = await errors.try(publishLink(temp, target))
561
- await fs.rm(temp, { force: true })
562
- if (published.error) {
563
- if (isUnproved(published.error) || isVanished(published.error)) {
564
- return { tag: "ambiguous" }
565
- }
566
- throw published.error
567
- }
568
- if (published.data === "occupied") {
569
- const st = await errors.try(fs.stat(target))
570
- if (st.error) {
571
- if (codeOf(st.error) === "ENOENT" || isUnproved(st.error)) {
572
- return { tag: "ambiguous" }
573
- }
574
- throw st.error
575
- }
576
- if (st.data.isDirectory()) {
577
- throw errors.new("key path is a directory")
578
- }
579
- return { tag: "exists" }
580
- }
581
- const synced = await errors.try(fsyncDir(path.dirname(target)))
582
- if (synced.error) {
583
- return { tag: "ambiguous" }
584
- }
585
- return { tag: "created", etag: contentEtag(bytes) }
586
- })
587
- })()
588
- )
589
- if (ran.error) {
590
- if (isUnproved(ran.error) || isVanished(ran.error)) {
591
- return { tag: "ambiguous" }
592
- }
593
- throw wrapStore(ran.error, `putCreate ${key}`)
594
- }
595
- return ran.data
596
- },
597
-
598
- async putSwap(key, bytes, etag) {
599
- await swept
600
- const target = objectPath(key)
601
- const ran = await errors.try(
602
- (async function swapBody(): Promise<Swap> {
603
- return await withKeyLease(key, async function underLease(held): Promise<Swap> {
604
- if (!(await stillCurrent(held))) {
605
- return { tag: "ambiguous" }
606
- }
607
- const current = await readFetched(target)
608
- if (current === null || current.etag !== etag) {
609
- return { tag: "moved" }
610
- }
611
- const temp = await syncedTemp(rootPath, bytes)
612
- const renamed = await errors.try(fs.rename(temp, target))
613
- if (renamed.error) {
614
- await fs.rm(temp, { force: true })
615
- if (isUnproved(renamed.error)) {
616
- return { tag: "ambiguous" }
617
- }
618
- throw renamed.error
619
- }
620
- const synced = await errors.try(fsyncDir(path.dirname(target)))
621
- if (synced.error) {
622
- return { tag: "ambiguous" }
623
- }
624
- return { tag: "swapped", etag: contentEtag(bytes) }
625
- })
626
- })()
627
- )
628
- if (ran.error) {
629
- if (isUnproved(ran.error)) {
630
- return { tag: "ambiguous" }
631
- }
632
- throw wrapStore(ran.error, `putSwap ${key}`)
633
- }
634
- return ran.data
635
- },
636
-
637
- async delete(key) {
638
- await swept
639
- const target = objectPath(key)
640
- const ran = await errors.try(
641
- (async function deleteBody() {
642
- await withKeyLease(key, async function underLease(held) {
643
- if (!(await stillCurrent(held))) {
644
- return
645
- }
646
- await fs.rm(target, { force: true })
647
- await fsyncDir(path.dirname(target))
648
- })
649
- })()
650
- )
651
- if (ran.error) {
652
- throw wrapStore(ran.error, `delete ${key}`)
653
- }
654
- }
655
- }
656
- }
657
-
658
- /**
659
- * The five verbs over one in-process Map. Single-process only: tests
660
- * and ephemeral dev inside this process, no persistence, no
661
- * cross-process claim, no configuration. Third `Etag` producer beside
662
- * `fsStore` and `s3Store`: blake3 of the content, `fsStore`'s mint,
663
- * carried as the same opaque brand. Every read returns a fresh buffer.
664
- */
665
- function memStore(): ObjectStore {
666
- const objects = new Map<StoreKey, Fetched>()
667
- return {
668
- async get(key) {
669
- const current = objects.get(key)
670
- return current === undefined ? null : cloneFetched(current)
671
- },
672
-
673
- async getIfChanged(key, etag) {
674
- const current = objects.get(key)
675
- if (current === undefined) {
676
- throw wrapStore(errors.new("poll target absent"), `getIfChanged ${key}`)
677
- }
678
- if (current.etag === etag) {
679
- return { tag: "unchanged" }
680
- }
681
- return { tag: "changed", fetched: cloneFetched(current) }
682
- },
683
-
684
- async putCreate(key, bytes) {
685
- if (objects.has(key)) {
686
- return { tag: "exists" }
687
- }
688
- const copy = new Uint8Array(bytes)
689
- const tag = contentEtag(copy)
690
- objects.set(key, { bytes: copy, etag: tag })
691
- return { tag: "created", etag: tag }
692
- },
693
-
694
- async putSwap(key, bytes, etag) {
695
- const current = objects.get(key)
696
- if (current === undefined || current.etag !== etag) {
697
- return { tag: "moved" }
698
- }
699
- const copy = new Uint8Array(bytes)
700
- const tag = contentEtag(copy)
701
- objects.set(key, { bytes: copy, etag: tag })
702
- return { tag: "swapped", etag: tag }
703
- },
704
-
705
- async delete(key) {
706
- objects.delete(key)
707
- }
708
- }
709
- }
710
-
711
- export type { S3Config, S3Credentials } from "#store-s3.ts"
712
- export { s3Store } from "#store-s3.ts"
713
- export type { Create, Etag, Fetched, FsLease, ObjectStore, Poll, Swap }
714
- export {
715
- acquireFsLease,
716
- encodeLease,
717
- etag,
718
- fsStore,
719
- MUTATION_TTL_MS,
720
- memStore,
721
- parseLease,
722
- releaseFsLease,
723
- resolveAmbiguousCreate,
724
- resolveAmbiguousSwap,
725
- sweepStaleTemps,
726
- syncedTemp
727
- }