@fabrika/platform-node 0.2.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/platform-node",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,6 +32,6 @@
32
32
  "test": "bun test"
33
33
  },
34
34
  "dependencies": {
35
- "@fabrika/platform": "^0.2.0"
35
+ "@fabrika/platform": "^0.3.2"
36
36
  }
37
37
  }
package/src/index.ts CHANGED
@@ -20,14 +20,19 @@
20
20
 
21
21
  export { FileSystemAssetServer, type FileSystemAssetServerOptions } from './asset-server-fs'
22
22
  export { S3BlobStore, type S3BlobStoreOptions } from './blob-s3'
23
- export { type Job, PostgresJobConsumer, type PostgresJobConsumerOptions, PostgresJobQueue, type PostgresJobQueueOptions } from './job-queue-postgres'
23
+ export {
24
+ decodeJob,
25
+ type Job,
26
+ PostgresJobConsumer,
27
+ type PostgresJobConsumerOptions,
28
+ PostgresJobQueue,
29
+ type PostgresJobQueueOptions,
30
+ } from './job-queue-postgres'
24
31
  export { rewritePlaceholders, type RewrittenSql } from './placeholders'
25
32
  export {
26
33
  applyPostgresMigrations,
27
34
  definePostgresMigrationPlan,
28
35
  platformNodePostgresMigrationBundle,
29
- type PostgresLegacyMigrationEffect,
30
- type PostgresLegacyMigrationLedger,
31
36
  type PostgresMigration,
32
37
  type PostgresMigrationBundle,
33
38
  postgresMigrationFilenames,
@@ -43,5 +48,4 @@ export {
43
48
  type PostgresServiceMigrationsConfig,
44
49
  } from './service-postgres-migrations'
45
50
  export { PostgresDatabase, PostgresStatement, type SqlBindValue } from './sql-postgres'
46
- export { uuidv7 } from './uuid'
47
51
  export { type BackgroundTasks, type BackgroundTasksOptions, createBackgroundTasks } from './wait-until'
@@ -25,10 +25,9 @@
25
25
  // `FOR UPDATE SKIP LOCKED` is the one Postgres-specific statement in this package; the DDL itself
26
26
  // (migrations/0001_jobs.sql) is in the SQLite ∩ Postgres common subset.
27
27
 
28
- import type { JobQueue, SqlDatabase } from '@fabrika/platform'
29
- import { uuidv7 } from './uuid'
28
+ import { type JobQueue, type SqlDatabase, uuidv7 } from '@fabrika/platform'
30
29
 
31
- /** A claimed job, as handed to a handler. `payload` has been through the caller's `decode`. */
30
+ /** A claimed delivery. Raw claims carry stored JSON text; typed claims carry the caller's decoded payload. */
32
31
  export interface Job<T> {
33
32
  id: string
34
33
  payload: T
@@ -38,6 +37,12 @@ export interface Job<T> {
38
37
  maxAttempts: number
39
38
  }
40
39
 
40
+ /** Parse and narrow one stored delivery without losing its settlement metadata. */
41
+ export function decodeJob<T>(job: Job<string>, decode: (payload: unknown) => T): Job<T> {
42
+ const parsed: unknown = JSON.parse(job.payload)
43
+ return { ...job, payload: decode(parsed) }
44
+ }
45
+
41
46
  export interface PostgresJobQueueOptions {
42
47
  /** Table holding the jobs. Must already exist; see migrations/0001_jobs.sql. */
43
48
  table?: string
@@ -121,6 +126,12 @@ export class PostgresJobQueue<T> implements JobQueue<T> {
121
126
  * than reaching a handler that assumes a shape.
122
127
  */
123
128
  async claim(options: { limit: number; visibilityTimeoutMs: number; decode: (payload: unknown) => T }): Promise<Job<T>[]> {
129
+ const jobs = await this.claimRaw(options)
130
+ return jobs.map((job) => decodeJob(job, options.decode))
131
+ }
132
+
133
+ /** Claim stored JSON text so consumers can decode and settle each delivery independently. */
134
+ async claimRaw(options: { limit: number; visibilityTimeoutMs: number }): Promise<Job<string>[]> {
124
135
  const now = this.now()
125
136
  const { results } = await this.db
126
137
  .prepare(this.claimSql)
@@ -131,10 +142,7 @@ export class PostgresJobQueue<T> implements JobQueue<T> {
131
142
  // restores enqueue order, because ids are monotonic UUIDv7 and therefore strictly time-ordered.
132
143
  return results
133
144
  .sort((left, right) => (left.id < right.id ? -1 : 1))
134
- .map((row) => {
135
- const parsed: unknown = JSON.parse(row.payload)
136
- return { id: row.id, payload: options.decode(parsed), attempts: row.attempts, maxAttempts: row.max_attempts }
137
- })
145
+ .map((row) => ({ id: row.id, payload: row.payload, attempts: row.attempts, maxAttempts: row.max_attempts }))
138
146
  }
139
147
 
140
148
  /** The job is done (or permanently abandoned) — drop the row. Idempotent. */
@@ -149,7 +157,7 @@ export class PostgresJobQueue<T> implements JobQueue<T> {
149
157
  }
150
158
 
151
159
  export interface PostgresJobConsumerOptions<T> {
152
- /** Narrow the stored JSON to the message type. Throwing here abandons the job as undecodable. */
160
+ /** Narrow the stored JSON to the message type. Invalid deliveries retry until their budget is spent. */
153
161
  decode: (payload: unknown) => T
154
162
  /** Handle one job. A throw schedules a retry until `maxAttempts` is spent. */
155
163
  handler: (job: Job<T>) => Promise<void>
@@ -196,23 +204,26 @@ export class PostgresJobConsumer<T> {
196
204
  visibilityTimeoutMs: options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS,
197
205
  retryDelayMs: options.retryDelayMs ?? ((attempts) => Math.min(60_000, 1_000 * 2 ** attempts)),
198
206
  }
199
- this.onError = options.onError ?? ((error) => {
200
- console.error('job handler failed:', error instanceof Error ? error.message : 'unknown error')
207
+ this.onError = options.onError ?? (() => {
208
+ console.error('job handler failed')
201
209
  })
202
210
  this.onAbandoned = options.onAbandoned ?? ((job) => {
203
211
  console.warn(`job ${job.id} abandoned after ${job.attempts} attempt(s)`)
204
212
  })
205
213
  }
206
214
 
207
- /** Run one pass. Returns how many jobs were handled — 0 means the queue was empty. Exposed for tests. */
215
+ /** Run one pass. Returns the raw claimed count, including invalid deliveries. Exposed for tests. */
208
216
  async poll(): Promise<number> {
209
- const jobs = await this.queue.claim({
217
+ const jobs = await this.queue.claimRaw({
210
218
  limit: this.options.batchSize,
211
219
  visibilityTimeoutMs: this.options.visibilityTimeoutMs,
212
- decode: this.options.decode,
213
220
  })
214
221
  for (const job of jobs) {
215
- await this.handle(job)
222
+ try {
223
+ await this.handleRaw(job)
224
+ } catch {
225
+ console.error('job settlement failed')
226
+ }
216
227
  }
217
228
  return jobs.length
218
229
  }
@@ -249,19 +260,34 @@ export class PostgresJobConsumer<T> {
249
260
  let handled = 0
250
261
  try {
251
262
  handled = await this.poll()
252
- } catch (error) {
263
+ } catch {
253
264
  // A claim failure is usually the database being unreachable — log short and back off.
254
- console.error('job poll failed:', error instanceof Error ? error.message : 'unknown error')
265
+ console.error('job poll failed')
255
266
  }
256
267
  if (this.running) {
257
268
  this.schedule(handled > 0 ? 0 : this.options.pollIntervalMs)
258
269
  }
259
270
  }
260
271
 
272
+ private async handleRaw(raw: Job<string>): Promise<void> {
273
+ let job: Job<T>
274
+ try {
275
+ job = decodeJob(raw, this.options.decode)
276
+ } catch {
277
+ if (raw.attempts >= raw.maxAttempts) {
278
+ await this.queue.ack(raw.id)
279
+ console.warn('undecodable job abandoned', { id: raw.id, attempts: raw.attempts, maxAttempts: raw.maxAttempts })
280
+ } else {
281
+ await this.queue.defer(raw.id, this.options.retryDelayMs(raw.attempts))
282
+ }
283
+ return
284
+ }
285
+ await this.handle(job)
286
+ }
287
+
261
288
  private async handle(job: Job<T>): Promise<void> {
262
289
  try {
263
290
  await this.options.handler(job)
264
- await this.queue.ack(job.id)
265
291
  } catch (error) {
266
292
  this.onError(error, job)
267
293
  if (job.attempts >= job.maxAttempts) {
@@ -270,6 +296,8 @@ export class PostgresJobConsumer<T> {
270
296
  return
271
297
  }
272
298
  await this.queue.defer(job.id, this.options.retryDelayMs(job.attempts))
299
+ return
273
300
  }
301
+ await this.queue.ack(job.id)
274
302
  }
275
303
  }
@@ -1,4 +1,4 @@
1
- import type { SqlDatabase, SqlStatement } from '@fabrika/platform'
1
+ import type { SqlDatabase } from '@fabrika/platform'
2
2
  import { readdirSync, readFileSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
 
@@ -15,23 +15,6 @@ export interface PostgresMigration {
15
15
  export interface PostgresMigrationBundle {
16
16
  name: string
17
17
  migrations: readonly PostgresMigration[]
18
- /** Only service-owned bundles may adopt matching entries from the old unqualified ledger. */
19
- adoptLegacy?: boolean
20
- }
21
-
22
- export interface PostgresLegacyMigrationLedger {
23
- /** Old filename-only ledger. */
24
- table: string
25
- /** Tables created atomically by this service's first migration. Every sentinel must exist. */
26
- sentinelTables: readonly string[]
27
- /** Extra effect probes for migrations whose filename could have collided in a shared old ledger. */
28
- effects?: readonly PostgresLegacyMigrationEffect[]
29
- }
30
-
31
- export interface PostgresLegacyMigrationEffect {
32
- migration: string
33
- /** A parameterless SELECT returning one boolean column named `proven`. */
34
- probeSql: string
35
18
  }
36
19
 
37
20
  export interface PostgresMigrationPlan {
@@ -40,14 +23,12 @@ export interface PostgresMigrationPlan {
40
23
  /** Service-specific session advisory lock. Keep this stable across runner upgrades. */
41
24
  lockKey: number
42
25
  bundles: readonly PostgresMigrationBundle[]
43
- legacy?: PostgresLegacyMigrationLedger
44
26
  }
45
27
 
46
28
  export interface ReadPostgresMigrationBundleOptions {
47
29
  name: string
48
30
  directory: string
49
31
  files?: readonly string[]
50
- adoptLegacy?: boolean
51
32
  }
52
33
 
53
34
  interface LedgerRow {
@@ -55,19 +36,6 @@ interface LedgerRow {
55
36
  name: string
56
37
  }
57
38
 
58
- interface LegacyLedgerRow {
59
- name: string
60
- applied_at: number | string
61
- }
62
-
63
- interface PresenceRow {
64
- present: boolean
65
- }
66
-
67
- interface ProvenRow {
68
- proven: boolean
69
- }
70
-
71
39
  /** Read one ordered migration bundle from disk. An explicit file list fails closed when a file is absent. */
72
40
  export function readPostgresMigrationBundle(options: ReadPostgresMigrationBundleOptions): PostgresMigrationBundle {
73
41
  validateBundleName(options.name)
@@ -88,7 +56,6 @@ export function readPostgresMigrationBundle(options: ReadPostgresMigrationBundle
88
56
  name,
89
57
  sql: readFileSync(join(options.directory, name), 'utf8'),
90
58
  })),
91
- ...(options.adoptLegacy === true ? { adoptLegacy: true } : {}),
92
59
  }
93
60
  validateBundle(bundle)
94
61
  return bundle
@@ -122,36 +89,6 @@ export function definePostgresMigrationPlan(plan: PostgresMigrationPlan): Postgr
122
89
  identities.add(identity)
123
90
  }
124
91
  }
125
- if (plan.legacy !== undefined) {
126
- validateIdentifier(plan.legacy.table, 'legacy migration ledger table')
127
- if (plan.legacy.sentinelTables.length === 0) {
128
- throw new Error('Legacy migration adoption requires at least one service sentinel')
129
- }
130
- for (const sentinel of plan.legacy.sentinelTables) {
131
- validateIdentifier(sentinel, 'legacy migration sentinel table')
132
- }
133
- const adoptingBundles = plan.bundles.filter((bundle) => bundle.adoptLegacy === true)
134
- if (adoptingBundles.length !== 1) {
135
- throw new Error('Legacy migration adoption requires exactly one adopting bundle')
136
- }
137
- const adoptable = new Set(
138
- adoptingBundles.flatMap((bundle) => bundle.migrations.map((migration) => migration.name)),
139
- )
140
- const effectNames = new Set<string>()
141
- for (const effect of plan.legacy.effects ?? []) {
142
- validateMigrationName(effect.migration)
143
- if (!adoptable.has(effect.migration)) {
144
- throw new Error(`Legacy migration effect references non-adoptable migration ${effect.migration}`)
145
- }
146
- if (effectNames.has(effect.migration)) {
147
- throw new Error(`Duplicate legacy migration effect ${effect.migration}`)
148
- }
149
- if (effect.probeSql.trim() === '') {
150
- throw new Error(`Legacy migration effect ${effect.migration} is empty`)
151
- }
152
- effectNames.add(effect.migration)
153
- }
154
- }
155
92
  return plan
156
93
  }
157
94
 
@@ -194,8 +131,6 @@ export async function applyPostgresMigrations(
194
131
  const fresh: string[] = []
195
132
  try {
196
133
  await ensureLedger(db, plan.ledgerTable)
197
- const beforeAdoption = await appliedIdentities(db, plan.ledgerTable)
198
- if (beforeAdoption.size === 0) await adoptLegacyMigrations(db, plan)
199
134
  const applied = await appliedIdentities(db, plan.ledgerTable)
200
135
  for (const bundle of plan.bundles) {
201
136
  for (const migration of bundle.migrations) {
@@ -272,84 +207,6 @@ async function appliedIdentities(db: SqlDatabase, ledgerTable: string): Promise<
272
207
  return new Set(results.map((row) => postgresMigrationIdentity(row.bundle, row.name)))
273
208
  }
274
209
 
275
- async function adoptLegacyMigrations(db: SqlDatabase, plan: PostgresMigrationPlan): Promise<void> {
276
- const legacy = plan.legacy
277
- if (legacy === undefined) return
278
- const sentinelStates = await Promise.all(legacy.sentinelTables.map((sentinel) => relationExists(db, sentinel)))
279
- const presentSentinels = sentinelStates.filter(Boolean).length
280
- if (presentSentinels === 0) return
281
- if (presentSentinels !== sentinelStates.length) {
282
- throw new Error('Legacy migration service sentinels are inconsistent')
283
- }
284
- if (!(await relationExists(db, legacy.table))) {
285
- throw new Error('Legacy migration ledger is missing for an existing service schema')
286
- }
287
- for (const sentinel of legacy.sentinelTables) {
288
- if (!(await relationExists(db, sentinel))) {
289
- throw new Error('Legacy migration service sentinels changed during adoption')
290
- }
291
- }
292
- const { results } = await db.prepare(`SELECT name, applied_at FROM ${legacy.table}`).all<LegacyLedgerRow>()
293
- const legacyApplied = new Map(results.map((row) => [row.name, appliedAt(row.applied_at)]))
294
- const adoptingBundle = plan.bundles.find((bundle) => bundle.adoptLegacy === true)
295
- if (adoptingBundle === undefined) throw new Error('Legacy migration adopting bundle is missing')
296
- const firstMigration = adoptingBundle.migrations[0]
297
- if (firstMigration === undefined || !legacyApplied.has(firstMigration.name)) {
298
- throw new Error(`Legacy migration ledger does not prove ${adoptingBundle.name}'s first migration`)
299
- }
300
- for (const effect of legacy.effects ?? []) {
301
- if (!legacyApplied.has(effect.migration)) continue
302
- const row = await db.prepare(effect.probeSql).first<ProvenRow>()
303
- if (row === null || typeof row.proven !== 'boolean') {
304
- throw new Error(`Legacy migration effect ${effect.migration} did not return a boolean proven column`)
305
- }
306
- if (!row.proven) {
307
- throw new Error(`Legacy migration effect ${effect.migration} is missing`)
308
- }
309
- }
310
- const statements: SqlStatement[] = []
311
- for (const bundle of plan.bundles) {
312
- if (bundle.adoptLegacy !== true) continue
313
- for (const migration of bundle.migrations) {
314
- const timestamp = legacyApplied.get(migration.name)
315
- if (timestamp === undefined) continue
316
- statements.push(
317
- db
318
- .prepare(`INSERT INTO ${plan.ledgerTable} (bundle, name, applied_at)
319
- VALUES (?, ?, ?)
320
- ON CONFLICT (bundle, name) DO NOTHING`)
321
- .bind(
322
- bundle.name,
323
- migration.name,
324
- timestamp,
325
- ),
326
- )
327
- }
328
- }
329
- if (statements.length > 0) await db.batch(statements)
330
- }
331
-
332
- async function relationExists(db: SqlDatabase, name: string): Promise<boolean> {
333
- const row = await db
334
- .prepare(`SELECT EXISTS (
335
- SELECT 1
336
- FROM pg_catalog.pg_class relation
337
- JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace
338
- WHERE namespace.nspname = current_schema()
339
- AND relation.relname = ?
340
- AND relation.relkind IN ('r', 'p')
341
- ) AS present`)
342
- .bind(name)
343
- .first<PresenceRow>()
344
- return row?.present === true
345
- }
346
-
347
- function appliedAt(value: number | string): number {
348
- const parsed = typeof value === 'number' ? value : Number(value)
349
- if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error('Legacy migration ledger contains an invalid timestamp')
350
- return parsed
351
- }
352
-
353
210
  function unixNow(): number {
354
211
  return Math.floor(Date.now() / 1_000)
355
212
  }
@@ -2,7 +2,6 @@ import type { SqlDatabase } from '@fabrika/platform'
2
2
  import {
3
3
  applyPostgresMigrations,
4
4
  definePostgresMigrationPlan,
5
- type PostgresLegacyMigrationLedger,
6
5
  type PostgresMigration,
7
6
  type PostgresMigrationBundle,
8
7
  postgresMigrationFilenames,
@@ -18,7 +17,6 @@ export interface PostgresServiceMigrationsConfig {
18
17
  /** Stable service advisory lock. */
19
18
  readonly lockKey: number
20
19
  readonly directory: string
21
- readonly legacy: PostgresLegacyMigrationLedger
22
20
  /** Runtime-owned bundles applied before the service schema. */
23
21
  readonly dependencies?: readonly PostgresMigrationBundle[]
24
22
  }
@@ -32,14 +30,13 @@ export interface PostgresServiceMigrations {
32
30
 
33
31
  /**
34
32
  * Define the repeated service migration wrapper while keeping service identity and lifecycle local.
35
- * Bundle names, ledgers, lock ids, dependency order, and legacy evidence remain explicit inputs.
33
+ * Bundle names, ledgers, lock ids, and dependency order remain explicit inputs.
36
34
  */
37
35
  export function definePostgresServiceMigrations(config: PostgresServiceMigrationsConfig): PostgresServiceMigrations {
38
36
  const read = (directory: string = config.directory): PostgresMigration[] => [
39
37
  ...readPostgresMigrationBundle({
40
38
  name: config.name,
41
39
  directory,
42
- adoptLegacy: true,
43
40
  }).migrations,
44
41
  ]
45
42
  const plan = (migrations: readonly PostgresMigration[] = read()): PostgresMigrationPlan =>
@@ -48,9 +45,8 @@ export function definePostgresServiceMigrations(config: PostgresServiceMigration
48
45
  lockKey: config.lockKey,
49
46
  bundles: [
50
47
  ...(config.dependencies ?? []),
51
- { name: config.name, migrations, adoptLegacy: true },
48
+ { name: config.name, migrations },
52
49
  ],
53
- legacy: config.legacy,
54
50
  })
55
51
  const applyQualified = (
56
52
  db: SqlDatabase,
package/src/uuid.ts DELETED
@@ -1,94 +0,0 @@
1
- /**
2
- * UUIDv7 generator — time-ordered ids, so a jobs table orders by id chronologically and a keyset
3
- * cursor works, monotonic WITHIN a millisecond as well as across one. Ids are minted CALLER-SIDE,
4
- * never by SQL (the repo-wide convention, and the one thing that keeps inserts identical across
5
- * SQLite and Postgres). Self-contained so this package has no dependencies beyond the port it
6
- * implements.
7
- *
8
- * RFC 9562 layout, with §6.2's "fixed bit-length dedicated counter" monotonic variant:
9
- *
10
- * bytes 0..5 48-bit big-endian Unix-millis timestamp
11
- * byte 6 version nibble `7` + the high 4 bits of a 12-bit counter (`rand_a`)
12
- * byte 7 the low 8 bits of that counter
13
- * byte 8 variant bits `10` + 6 random bits
14
- * bytes 9..15 56 random bits
15
- *
16
- * so **62 random bits per id** — down from the 74 of the unguarded Method 0 this used to be — plus 8
17
- * fresh random bits seeding the counter at each new millisecond.
18
- *
19
- * The counter is re-seeded from 8 random bits (the top 4 left zero, the RFC's rollover guard, so at
20
- * least 3840 ids per millisecond are guaranteed) whenever the millisecond advances, and incremented
21
- * otherwise. `rand_a` sits immediately after the timestamp and the version nibble in the canonical
22
- * string, so two ids minted in the same millisecond differ there and a TEXT `ORDER BY` puts them in
23
- * mint order, strictly.
24
- *
25
- * Two clock hazards are handled explicitly rather than by wrapping:
26
- * - counter exhausted inside one millisecond → borrow one millisecond from the future (RFC 9562
27
- * §6.2) and re-seed; the borrowed time is repaid as soon as the clock catches up.
28
- * - the timestamp goes BACKWARDS → keep the last effective millisecond and keep counting, so the
29
- * sequence never repeats and never decreases.
30
- *
31
- * The counter is per GENERATOR — one process. Two ids minted in the same millisecond by two processes
32
- * tie only if their independently seeded counters collide (~1/256), and the 62 random bits break
33
- * that; within one process the order is exact. That is the whole promise: strictly increasing per
34
- * generator, chronological to the millisecond across generators.
35
- *
36
- * `millis` is injectable so a caller that already has a clock (the job queue does) mints ids on THAT
37
- * clock, keeping id order and row order consistent. Because the generator is monotonic, the timestamp
38
- * actually embedded is `max(millis, last embedded)`: a supplied millisecond BEHIND one already used
39
- * is clamped, and the counter carries the ordering instead. Ordering is the promise; an exact
40
- * round-trip of an arbitrary past `millis` is not.
41
- *
42
- * `@fabrika/auth-core` and `@fabrika/control` carry deliberate copies of this same algorithm — see
43
- * the note in each.
44
- */
45
-
46
- /** `rand_a` is 12 bits, all of it counter. */
47
- const COUNTER_MAX = 0xfff
48
- /** Seed from 8 random bits only — the leading 4 stay zero so a millisecond has room to count. */
49
- const COUNTER_SEED_MASK = 0xff
50
-
51
- /** Effective millisecond of the last id. NEVER decreases; a backwards clock is absorbed here. */
52
- let lastMillis = -1
53
- let counter = 0
54
-
55
- export function uuidv7(millis: number = Date.now()): string {
56
- const bytes = new Uint8Array(16)
57
- crypto.getRandomValues(bytes)
58
-
59
- // Byte 7 is read as the counter seed BEFORE the counter overwrites it, so the seed costs no extra
60
- // randomness.
61
- const seed = (bytes[7] ?? 0) & COUNTER_SEED_MASK
62
- if (millis > lastMillis) {
63
- lastMillis = millis
64
- counter = seed
65
- } else if (counter < COUNTER_MAX) {
66
- // Same millisecond, or a clock that went backwards: hold the effective timestamp, count on.
67
- counter += 1
68
- } else {
69
- // Exhausted — borrow a millisecond from the future rather than wrap back under the last id.
70
- lastMillis += 1
71
- counter = seed
72
- }
73
-
74
- // 48-bit big-endian timestamp in bytes 0..5.
75
- bytes[0] = (lastMillis / 2 ** 40) & 0xff
76
- bytes[1] = (lastMillis / 2 ** 32) & 0xff
77
- bytes[2] = (lastMillis / 2 ** 24) & 0xff
78
- bytes[3] = (lastMillis / 2 ** 16) & 0xff
79
- bytes[4] = (lastMillis / 2 ** 8) & 0xff
80
- bytes[5] = lastMillis & 0xff
81
-
82
- // Version 7 in the high nibble of byte 6, the counter in the remaining 12 bits of `rand_a`;
83
- // variant (10xx) in the high bits of byte 8.
84
- bytes[6] = 0x70 | (counter >>> 8)
85
- bytes[7] = counter & 0xff
86
- bytes[8] = (bytes[8] ?? 0) & 0x3f | 0x80
87
-
88
- const hex: string[] = []
89
- for (const b of bytes) {
90
- hex.push(b.toString(16).padStart(2, '0'))
91
- }
92
- const s = hex.join('')
93
- return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`
94
- }