@bjornpagen/bumbledb-log 0.19.2 → 0.20.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/src/tenants.ts CHANGED
@@ -17,15 +17,13 @@ import * as path from "node:path"
17
17
  import type { Schema, SchemaRelations } from "@bjornpagen/bumbledb"
18
18
  import * as errors from "@superbuilders/errors"
19
19
  import { regex } from "arkregex"
20
- import { tenantPrefix } from "#keys.ts"
20
+ import { TEMP_NAMESPACE, tenantPrefix } from "#keys.ts"
21
21
  import type { Replica } from "#replica.ts"
22
22
  import { openReplica } from "#replica.ts"
23
23
  import type { FsLease, ObjectStore } from "#store.ts"
24
- import { acquireFsLease, releaseFsLease } from "#store.ts"
24
+ import { acquireFsLease, encodeLease, parseLease, releaseFsLease, sweepStaleTemps, syncedTemp } from "#store.ts"
25
25
 
26
26
  const PINNED_TENANT = "_shared"
27
- const SIGNED_DECIMAL = regex("^-?\\d+$")
28
- const UNSIGNED_DECIMAL = regex("^\\d+$")
29
27
  const TENANT_ID = regex("^[A-Za-z0-9._-]+$")
30
28
 
31
29
  /** Directory-lease lifetime: one owner per replica directory. The pool
@@ -88,25 +86,6 @@ function codeOf(error: Error): string | undefined {
88
86
  return (error as NodeJS.ErrnoException).code
89
87
  }
90
88
 
91
- function encodeLease(holder: bigint, token: bigint, expires: bigint): Uint8Array {
92
- return new TextEncoder().encode(`${holder}\n${token}\n${expires}\n`)
93
- }
94
-
95
- function parseLease(raw: string): { holder: bigint; token: bigint; expires: bigint } | null {
96
- const lines = raw.trim().split("\n")
97
- if (lines.length !== 3) {
98
- return null
99
- }
100
- const [holderLine, tokenLine, expiresLine] = lines
101
- if (holderLine === undefined || tokenLine === undefined || expiresLine === undefined) {
102
- return null
103
- }
104
- if (!SIGNED_DECIMAL.test(holderLine) || !UNSIGNED_DECIMAL.test(tokenLine) || !SIGNED_DECIMAL.test(expiresLine)) {
105
- return null
106
- }
107
- return { holder: BigInt(holderLine), token: BigInt(tokenLine), expires: BigInt(expiresLine) }
108
- }
109
-
110
89
  async function fsyncDir(dir: string): Promise<void> {
111
90
  const handle = await fs.open(dir, "r")
112
91
  const synced = await errors.try(handle.sync())
@@ -131,23 +110,13 @@ async function renewDirLease(held: FsLease, ttlMs: number): Promise<void> {
131
110
  if (lease === null || lease.holder !== held.holder || lease.token !== held.token) {
132
111
  throw errors.new("directory lease is no longer ours")
133
112
  }
134
- const body = encodeLease(held.holder, held.token, BigInt(Date.now() + ttlMs))
135
- const tmp = `${held.path}.renew.${process.pid}`
136
- const file = await fs.open(tmp, "w")
137
- const written = await errors.try(
138
- (async function writeAll() {
139
- await file.writeFile(body)
140
- await file.sync()
141
- })()
142
- )
143
- await file.close()
144
- if (written.error) {
145
- await fs.rm(tmp, { force: true })
146
- throw written.error
147
- }
148
- const replaced = await errors.try(fs.rename(tmp, held.path))
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))
149
118
  if (replaced.error) {
150
- await fs.rm(tmp, { force: true })
119
+ await fs.rm(temp, { force: true })
151
120
  if (codeOf(replaced.error) === "ENOENT") {
152
121
  return
153
122
  }
@@ -199,6 +168,9 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
199
168
  const renewEveryMs = Math.max(1, Math.floor(dirLeaseMs / 3))
200
169
  const open = new Map<string, TenantSlot<Rels>>()
201
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)))
202
174
  let tick = 0
203
175
  let closed = false
204
176
  let renewTimer: ReturnType<typeof setInterval> | undefined
@@ -328,8 +300,14 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
328
300
  }
329
301
 
330
302
  async function openOne(tenant: string): Promise<LiveHandle<Rels>> {
303
+ await swept
331
304
  const dir = path.join(options.dir, tenant)
332
- const lease = await acquireFsLease(dir, "dir", dirLeaseMs, "refuse")
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")
333
311
  let replica: LiveHandle<Rels>
334
312
  try {
335
313
  replica = brandLive(
@@ -432,5 +410,5 @@ function openTenants<Rels extends SchemaRelations>(options: OpenTenantsOptions<R
432
410
  }
433
411
  }
434
412
 
435
- export type { DisposedHandle, LiveHandle, OpenTenantsOptions, Tenants }
413
+ export type { OpenTenantsOptions, Tenants }
436
414
  export { openTenants }
package/src/vector.ts CHANGED
@@ -1,34 +1,19 @@
1
1
  /**
2
- * The per-braid generation map. Sum, domination, checkpoint order,
3
- * apply's increment, and the coordinate's one binary encoding live
4
- * here. Overflow is a refusal of `sum` and of nowhere else.
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
5
  */
6
6
 
7
- import { ByteReader, ByteWriter, U64_MAX } from "#bytes.ts"
7
+ import { U64_MAX } from "#bytes.ts"
8
8
  import type { Braid } from "#descriptor.ts"
9
- import { braidHex } from "#descriptor.ts"
10
9
  import type { Generation } from "#keys.ts"
11
10
  import { generation } from "#keys.ts"
12
11
 
13
- /** One braid id and its applied count on the wire: u32le + u64le. */
14
- const PAIR_BYTES = 12n
15
-
16
12
  const Overflow = { tag: "overflow" } as const
17
13
  type Overflow = typeof Overflow
18
14
 
19
15
  type CheckpointOrder = "before" | "equal" | "after"
20
16
 
21
- type VectorParse =
22
- | { readonly tag: "ok"; readonly vector: Vector }
23
- | { readonly tag: "truncated" }
24
- | { readonly tag: "trailing"; readonly bytes: number }
25
- | { readonly tag: "malformed" }
26
- | { readonly tag: "overflow" }
27
-
28
- function braidIdOf(id: Braid): number {
29
- return Number.parseInt(id.slice(1), 16)
30
- }
31
-
32
17
  /** Applied counts keyed by braid. Any vector is a legal restore point. */
33
18
  class Vector {
34
19
  readonly #counts: Map<Braid, bigint>
@@ -112,62 +97,7 @@ class Vector {
112
97
  next.set(braid, current === U64_MAX ? U64_MAX : current + 1n)
113
98
  return new Vector(next)
114
99
  }
115
-
116
- /** `u32le` count, then `(u32le braid, u64le g)` pairs in braid order. */
117
- encode(): Uint8Array {
118
- const braids = [...this.#counts.keys()].sort()
119
- const out = new ByteWriter(4 + braids.length * 12)
120
- out.u32le(braids.length)
121
- for (const id of braids) {
122
- out.u32le(braidIdOf(id))
123
- out.u64le(this.#counts.get(id) ?? 0n)
124
- }
125
- return out.finish()
126
- }
127
-
128
- /**
129
- * Inverse of `encode`. A count the remaining bytes cannot open is
130
- * truncated; entries must ascend; overflow is `sum`.
131
- */
132
- static parse(bytes: Uint8Array): VectorParse {
133
- const short = { tag: "short" } as const
134
- const reader = new ByteReader(bytes, {
135
- fail(): never {
136
- throw short
137
- }
138
- })
139
- try {
140
- const count = BigInt(reader.u32le("count"))
141
- if (count > 0n && BigInt(reader.remaining()) / PAIR_BYTES < count) {
142
- return { tag: "truncated" }
143
- }
144
- const entries: Array<readonly [Braid, bigint]> = []
145
- let last: Braid | undefined
146
- for (let i = 0n; i < count; i++) {
147
- const name = braidHex(reader.u32le("braid"))
148
- const g = reader.u64le("g")
149
- if (last !== undefined && last >= name) {
150
- return { tag: "malformed" }
151
- }
152
- entries.push([name, g])
153
- last = name
154
- }
155
- if (reader.remaining() !== 0) {
156
- return { tag: "trailing", bytes: reader.remaining() }
157
- }
158
- const vector = new Vector(entries)
159
- if (typeof vector.sum() !== "bigint") {
160
- return { tag: "overflow" }
161
- }
162
- return { tag: "ok", vector }
163
- } catch (error) {
164
- if (error === short) {
165
- return { tag: "truncated" }
166
- }
167
- throw error
168
- }
169
- }
170
100
  }
171
101
 
172
- export type { CheckpointOrder, VectorParse }
102
+ export type { CheckpointOrder }
173
103
  export { Overflow, Vector }