@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/tenants.ts DELETED
@@ -1,414 +0,0 @@
1
- /**
2
- * Per-tenant replicas (50, case 4): an LRU of replicas keyed by tenant
3
- * id — a tenant is a prefix (`<root>/t/<tenant>`), eviction closes and
4
- * deletes the local dir (the disposable law), and `t/_shared` is
5
- * pinned. Braids shard within a tenant; tenants shard the world. A
6
- * tenant id is one StoreKey segment. `get` returns a live handle whose
7
- * refcount pins it against eviction for the duration of the borrow
8
- * (findings 38, 71) — the pin outlives `get` and drops only on
9
- * `release`; a disposed handle is a distinct type. One fenced lease
10
- * owns each replica directory and is CAS-renewed while the slot is
11
- * open, so a second live replica on that dir is unrepresentable
12
- * (findings 39, 48).
13
- */
14
-
15
- import * as fs from "node:fs/promises"
16
- import * as path from "node:path"
17
- import type { Schema, SchemaRelations } from "@bjornpagen/bumbledb"
18
- import * as errors from "@superbuilders/errors"
19
- import { regex } from "arkregex"
20
- import { TEMP_NAMESPACE, tenantPrefix } from "#keys.ts"
21
- import type { Replica } from "#replica.ts"
22
- import { openReplica } from "#replica.ts"
23
- import type { FsLease, ObjectStore } from "#store.ts"
24
- import { acquireFsLease, encodeLease, parseLease, releaseFsLease, sweepStaleTemps, syncedTemp } from "#store.ts"
25
-
26
- const PINNED_TENANT = "_shared"
27
- const TENANT_ID = regex("^[A-Za-z0-9._-]+$")
28
-
29
- /** Directory-lease lifetime: one owner per replica directory. The pool
30
- * CAS-extends `expires` at one-third this TTL while the slot is open,
31
- * so expiry cannot mint a second owner under a live replica. */
32
- const DIR_LEASE_MS = 300_000
33
-
34
- interface OpenTenantsOptions<Rels extends SchemaRelations> {
35
- readonly store: ObjectStore
36
- readonly root: string
37
- readonly dir: string
38
- readonly theory: Schema<Rels>
39
- /** 50's 400 MB gate: checkpoint + working set per instance. Advisory,
40
- * measured once at each tenant's open — a replica that grows after
41
- * admission is not re-weighed until it is evicted and re-opened. */
42
- readonly budgetBytes?: number
43
- readonly maxOpen?: number
44
- /** Directory-lease TTL. Renewed at one-third this value while the
45
- * replica is open. Default 300_000. */
46
- readonly dirLeaseMs?: number
47
- }
48
-
49
- const liveHandleBrand: unique symbol = Symbol("liveHandle")
50
- const disposedHandleBrand: unique symbol = Symbol("disposedHandle")
51
-
52
- type LiveHandle<Rels extends SchemaRelations> = Replica<Rels> & {
53
- readonly [liveHandleBrand]: typeof liveHandleBrand
54
- /** Drop one borrow. LRU and `evict` become legal only when `refs`
55
- * reaches 0 — not when `get` returns. */
56
- release(): void
57
- }
58
-
59
- /** A handle whose replica is gone. Every verb is a compile-time
60
- * refusal — there is no replica field to call, so `waitFor` into a
61
- * disposed replica is a type error. */
62
- type DisposedHandle = {
63
- readonly [disposedHandleBrand]: typeof disposedHandleBrand
64
- }
65
-
66
- interface Tenants<Rels extends SchemaRelations> extends AsyncDisposable {
67
- get(tenant: string): Promise<LiveHandle<Rels>>
68
- /** Evicts one tenant: closes the replica and deletes its directory.
69
- * `_shared` and a still-pinned borrow refuse by doing nothing. */
70
- evict(tenant: string): Promise<DisposedHandle | null>
71
- /** Drop one `get` borrow for `tenant`. */
72
- release(tenant: string): void
73
- }
74
-
75
- interface TenantSlot<Rels extends SchemaRelations> {
76
- readonly replica: LiveHandle<Rels>
77
- readonly dir: string
78
- readonly lease: FsLease
79
- bytes: number
80
- lastUsed: number
81
- refs: number
82
- leaseLost?: Error
83
- }
84
-
85
- function codeOf(error: Error): string | undefined {
86
- return (error as NodeJS.ErrnoException).code
87
- }
88
-
89
- async function fsyncDir(dir: string): Promise<void> {
90
- const handle = await fs.open(dir, "r")
91
- const synced = await errors.try(handle.sync())
92
- await handle.close()
93
- if (synced.error) {
94
- throw errors.wrap(synced.error, `fsync directory ${dir}`)
95
- }
96
- }
97
-
98
- /** CAS-extend `expires` of a token we already hold. A new token would
99
- * be a second owner; we rewrite the same path so identity does not
100
- * change. */
101
- async function renewDirLease(held: FsLease, ttlMs: number): Promise<void> {
102
- const read = await errors.try(fs.readFile(held.path, "utf8"))
103
- if (read.error) {
104
- if (codeOf(read.error) === "ENOENT") {
105
- return
106
- }
107
- throw read.error
108
- }
109
- const lease = parseLease(read.data)
110
- if (lease === null || lease.holder !== held.holder || lease.token !== held.token) {
111
- throw errors.new("directory lease is no longer ours")
112
- }
113
- const body = encodeLease({ holder: held.holder, token: held.token, expires: BigInt(Date.now() + ttlMs) })
114
- // The temp lives under the reserved `{root}/~tmp` namespace, where a
115
- // crash strands sweepable litter — never beside the lease path.
116
- const temp = await syncedTemp(held.root, body)
117
- const replaced = await errors.try(fs.rename(temp, held.path))
118
- if (replaced.error) {
119
- await fs.rm(temp, { force: true })
120
- if (codeOf(replaced.error) === "ENOENT") {
121
- return
122
- }
123
- throw replaced.error
124
- }
125
- await fsyncDir(held.dir)
126
- }
127
-
128
- async function directoryBytes(dir: string): Promise<number> {
129
- let total = 0
130
- const listed = await errors.try(fs.readdir(dir, { withFileTypes: true, recursive: true }))
131
- if (listed.error) {
132
- return 0
133
- }
134
- for (const entry of listed.data) {
135
- if (entry.isFile()) {
136
- const stat = await errors.try(fs.stat(path.join(entry.parentPath, entry.name)))
137
- if (stat.error === undefined) {
138
- total += stat.data.size
139
- }
140
- }
141
- }
142
- return total
143
- }
144
-
145
- function checkTenantId(tenant: string): void {
146
- if (!TENANT_ID.test(tenant)) {
147
- throw errors.new(`tenant id is not a single path segment: ${tenant}`)
148
- }
149
- }
150
-
151
- function asDisposed(): DisposedHandle {
152
- return { [disposedHandleBrand]: disposedHandleBrand }
153
- }
154
-
155
- async function disposeSlot<Rels extends SchemaRelations>(slot: TenantSlot<Rels>): Promise<void> {
156
- await slot.replica[Symbol.asyncDispose]()
157
- await fs.rm(slot.dir, { recursive: true, force: true })
158
- await releaseFsLease(slot.lease)
159
- }
160
-
161
- function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<Rels>): Tenants<Rels> {
162
- const budgetBytes = options.budgetBytes ?? 400_000_000
163
- const maxOpen = options.maxOpen ?? 32
164
- const dirLeaseMs = options.dirLeaseMs ?? DIR_LEASE_MS
165
- if (dirLeaseMs <= 0) {
166
- throw errors.new("dirLeaseMs must be positive")
167
- }
168
- const renewEveryMs = Math.max(1, Math.floor(dirLeaseMs / 3))
169
- const open = new Map<string, TenantSlot<Rels>>()
170
- const opening = new Map<string, Promise<LiveHandle<Rels>>>()
171
- /** The pool root's `~tmp` holds lease mint/renew/release temps; stale
172
- * ones are crash litter swept once per pool open, best-effort. */
173
- const swept = errors.try(sweepStaleTemps(path.join(options.dir, TEMP_NAMESPACE)))
174
- let tick = 0
175
- let closed = false
176
- let renewTimer: ReturnType<typeof setInterval> | undefined
177
- let leaseGate = Promise.resolve()
178
-
179
- function withLeaseGate<T>(body: () => Promise<T>): Promise<T> {
180
- const run = leaseGate.then(body, body)
181
- leaseGate = run.then(
182
- function absorb() {
183
- return undefined
184
- },
185
- function absorbFailure() {
186
- return undefined
187
- }
188
- )
189
- return run
190
- }
191
-
192
- function dropBorrow(tenant: string): void {
193
- if (closed) {
194
- throw errors.new("tenants pool is disposed")
195
- }
196
- const slot = open.get(tenant)
197
- if (slot === undefined || slot.refs <= 0) {
198
- throw errors.new(`no live borrow for tenant ${tenant}`)
199
- }
200
- slot.refs -= 1
201
- }
202
-
203
- function brandLive(replica: Replica<Rels>, tenant: string): LiveHandle<Rels> {
204
- const handle = replica as LiveHandle<Rels>
205
- Object.defineProperty(handle, liveHandleBrand, { value: liveHandleBrand })
206
- Object.defineProperty(handle, "release", {
207
- value: function release() {
208
- dropBorrow(tenant)
209
- },
210
- enumerable: false
211
- })
212
- return handle
213
- }
214
-
215
- function armRenew(): void {
216
- if (renewTimer !== undefined) {
217
- return
218
- }
219
- renewTimer = setInterval(function tickRenew() {
220
- void errors.try(renewAllOpen())
221
- }, renewEveryMs)
222
- }
223
-
224
- function disarmRenew(): void {
225
- if (open.size > 0 || renewTimer === undefined) {
226
- return
227
- }
228
- clearInterval(renewTimer)
229
- renewTimer = undefined
230
- }
231
-
232
- async function renewAllOpen(): Promise<void> {
233
- await withLeaseGate(async function renewHeld() {
234
- for (const [tenant, slot] of open) {
235
- const result = await errors.try(renewDirLease(slot.lease, dirLeaseMs))
236
- if (result.error !== undefined && open.get(tenant) === slot) {
237
- slot.leaseLost = result.error
238
- }
239
- }
240
- })
241
- }
242
-
243
- async function evictUntilWithin(keep: string): Promise<void> {
244
- for (;;) {
245
- let bytes = 0
246
- for (const slot of open.values()) {
247
- bytes += slot.bytes
248
- }
249
- if (open.size <= maxOpen && bytes <= budgetBytes) {
250
- return
251
- }
252
- let victim: string | null = null
253
- let oldest = Number.POSITIVE_INFINITY
254
- for (const [tenant, slot] of open) {
255
- if (tenant === PINNED_TENANT || tenant === keep || slot.refs > 0) {
256
- continue
257
- }
258
- if (slot.lastUsed < oldest) {
259
- oldest = slot.lastUsed
260
- victim = tenant
261
- }
262
- }
263
- if (victim === null) {
264
- return
265
- }
266
- const slot = open.get(victim)
267
- open.delete(victim)
268
- if (slot !== undefined) {
269
- await withLeaseGate(function disposeVictim() {
270
- return disposeSlot(slot)
271
- })
272
- }
273
- }
274
- }
275
-
276
- /** Increment the borrow. The pin outlives `get` — a `finally`
277
- * decrement here would make LRU/`evict` legal while the caller
278
- * still holds the LiveHandle (finding 38). Decrement only if this
279
- * `get` fails before the handle is in the caller's hands. */
280
- async function pin(slot: TenantSlot<Rels>, tenant: string): Promise<LiveHandle<Rels>> {
281
- if (slot.leaseLost !== undefined) {
282
- throw slot.leaseLost
283
- }
284
- slot.refs += 1
285
- slot.lastUsed = tick
286
- try {
287
- await evictUntilWithin(tenant)
288
- const kept = open.get(tenant)
289
- if (kept === undefined) {
290
- throw errors.new("tenants pool evicted the handle it is returning")
291
- }
292
- await withLeaseGate(function renewOnPin() {
293
- return renewDirLease(kept.lease, dirLeaseMs)
294
- })
295
- return kept.replica
296
- } catch (error) {
297
- slot.refs -= 1
298
- throw error
299
- }
300
- }
301
-
302
- async function openOne(tenant: string): Promise<LiveHandle<Rels>> {
303
- await swept
304
- const dir = path.join(options.dir, tenant)
305
- /** The dir-lease mirrors Rust `acquire_named`: tokens live at
306
- * `{options.dir}/~lease/{tenant}`, outside the disposable replica
307
- * directory `{options.dir}/{tenant}` — the replica-open sweep
308
- * rm-rfs the replica dir's own `~lease` and must not touch the
309
- * held tenant lease. */
310
- const lease = await acquireFsLease(options.dir, tenant, dirLeaseMs, "refuse")
311
- let replica: LiveHandle<Rels>
312
- try {
313
- replica = brandLive(
314
- await openReplica({
315
- store: options.store,
316
- prefix: tenantPrefix(options.root, tenant),
317
- dir,
318
- theory: options.theory
319
- }),
320
- tenant
321
- )
322
- } catch (error) {
323
- await releaseFsLease(lease)
324
- throw error
325
- }
326
- const slot: TenantSlot<Rels> = {
327
- replica,
328
- dir,
329
- lease,
330
- bytes: await directoryBytes(dir),
331
- lastUsed: tick,
332
- refs: 0
333
- }
334
- open.set(tenant, slot)
335
- armRenew()
336
- return await pin(slot, tenant)
337
- }
338
-
339
- return {
340
- async get(tenant) {
341
- if (closed) {
342
- throw errors.new("tenants pool is disposed")
343
- }
344
- checkTenantId(tenant)
345
- tick += 1
346
- const hit = open.get(tenant)
347
- if (hit !== undefined) {
348
- return await pin(hit, tenant)
349
- }
350
- const inflight = opening.get(tenant)
351
- if (inflight !== undefined) {
352
- const ran = await errors.try(inflight)
353
- if (ran.error) {
354
- throw ran.error
355
- }
356
- const slot = open.get(tenant)
357
- if (slot === undefined) {
358
- throw errors.new("tenants pool dropped the tenant during open")
359
- }
360
- return await pin(slot, tenant)
361
- }
362
- const pending = openOne(tenant)
363
- opening.set(tenant, pending)
364
- const ran = await errors.try(pending)
365
- opening.delete(tenant)
366
- if (ran.error) {
367
- throw ran.error
368
- }
369
- return ran.data
370
- },
371
-
372
- async evict(tenant) {
373
- if (closed) {
374
- throw errors.new("tenants pool is disposed")
375
- }
376
- checkTenantId(tenant)
377
- if (tenant === PINNED_TENANT) {
378
- return null
379
- }
380
- const slot = open.get(tenant)
381
- if (slot === undefined || slot.refs > 0) {
382
- return null
383
- }
384
- open.delete(tenant)
385
- await withLeaseGate(function disposeEvicted() {
386
- return disposeSlot(slot)
387
- })
388
- disarmRenew()
389
- return asDisposed()
390
- },
391
-
392
- release(tenant) {
393
- dropBorrow(tenant)
394
- },
395
-
396
- async [Symbol.asyncDispose]() {
397
- closed = true
398
- if (renewTimer !== undefined) {
399
- clearInterval(renewTimer)
400
- renewTimer = undefined
401
- }
402
- await withLeaseGate(async function disposePool() {
403
- for (const slot of open.values()) {
404
- await releaseFsLease(slot.lease)
405
- await slot.replica[Symbol.asyncDispose]()
406
- }
407
- open.clear()
408
- })
409
- }
410
- }
411
- }
412
-
413
- export type { OpenTenantsOptions, Tenants }
414
- export { openTenants }
package/src/vector.ts DELETED
@@ -1,103 +0,0 @@
1
- /**
2
- * The per-braid generation map. Sum, domination, checkpoint order, and
3
- * apply's increment live here. Overflow is a refusal of `sum` and of
4
- * nowhere else.
5
- */
6
-
7
- import { U64_MAX } from "#bytes.ts"
8
- import type { Braid } from "#descriptor.ts"
9
- import type { Generation } from "#keys.ts"
10
- import { generation } from "#keys.ts"
11
-
12
- const Overflow = { tag: "overflow" } as const
13
- type Overflow = typeof Overflow
14
-
15
- type CheckpointOrder = "before" | "equal" | "after"
16
-
17
- /** Applied counts keyed by braid. Any vector is a legal restore point. */
18
- class Vector {
19
- readonly #counts: Map<Braid, bigint>
20
-
21
- constructor(entries: Iterable<readonly [Braid, bigint]> = []) {
22
- const counts = new Map<Braid, bigint>()
23
- for (const [braid, g] of [...entries].sort(function byBraid(left, right) {
24
- if (left[0] < right[0]) {
25
- return -1
26
- }
27
- if (left[0] > right[0]) {
28
- return 1
29
- }
30
- return 0
31
- })) {
32
- counts.set(braid, g)
33
- }
34
- this.#counts = counts
35
- }
36
-
37
- static from(map: ReadonlyMap<Braid, bigint>): Vector {
38
- return new Vector(map)
39
- }
40
-
41
- /** The wholeness arithmetic. The one overflow site. */
42
- sum(): bigint | Overflow {
43
- let acc = 0n
44
- for (const g of this.#counts.values()) {
45
- const next = acc + g
46
- if (next > U64_MAX) {
47
- return Overflow
48
- }
49
- acc = next
50
- }
51
- return acc
52
- }
53
-
54
- /** Pointwise `this[braid] >= other[braid]`; an absent braid is zero. */
55
- dominates(other: Vector): boolean {
56
- for (const [braid, g] of other.#counts) {
57
- if (this.at(braid) < g) {
58
- return false
59
- }
60
- }
61
- return true
62
- }
63
-
64
- /** The total order the manifest CAS installs. */
65
- order(other: Vector): CheckpointOrder {
66
- const left = this.sum()
67
- const right = other.sum()
68
- const leftOverflow = typeof left !== "bigint"
69
- const rightOverflow = typeof right !== "bigint"
70
- if (leftOverflow && rightOverflow) {
71
- return "equal"
72
- }
73
- if (leftOverflow) {
74
- return "after"
75
- }
76
- if (rightOverflow) {
77
- return "before"
78
- }
79
- if (left < right) {
80
- return "before"
81
- }
82
- if (left > right) {
83
- return "after"
84
- }
85
- return "equal"
86
- }
87
-
88
- /** The applied count for `braid`; absent is zero. */
89
- at(braid: Braid): Generation {
90
- return generation(this.#counts.get(braid) ?? 0n)
91
- }
92
-
93
- /** Apply's one mutation: this braid's count advances by one. */
94
- advance(braid: Braid): Vector {
95
- const next = new Map(this.#counts)
96
- const current = next.get(braid) ?? 0n
97
- next.set(braid, current === U64_MAX ? U64_MAX : current + 1n)
98
- return new Vector(next)
99
- }
100
- }
101
-
102
- export type { CheckpointOrder }
103
- export { Overflow, Vector }