UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

104 lines (97 loc) 3.42 kB
import { $inject } from "alepha"; import { CryptoProvider } from "alepha/crypto"; import { sql } from "drizzle-orm"; import { alephaSequences } from "../entities/alephaSequences.ts"; import { $repository } from "../primitives/$repository.ts"; /** * Portable, scoped numeric sequence provider — works identically on Postgres, * SQLite, and Cloudflare D1. * * Implementation: a single `alepha_sequences` table holds one row per * `(name, scope)` pair. Every call to {@link advance} runs an * `INSERT ... ON CONFLICT (name, scope) DO UPDATE SET value = value + step` * with `RETURNING value`, which is atomic on every supported driver: * * - **Postgres**: row-level lock on `ON CONFLICT DO UPDATE`. * - **SQLite / D1**: writes are serialized, atomic by construction. * * The repository pattern is the same one used by `DatabaseCacheProvider.incr()` * — see that file for the proof-of-design. * * Callers never instantiate this directly. They declare a {@link SequencePrimitive} * via `$sequence()` and call `.next(scope?)` on the primitive — this provider is * the engine behind that call. */ export class SequenceProvider { protected readonly repository = $repository(alephaSequences); protected readonly crypto = $inject(CryptoProvider); /** * Atomically advance the counter for `(name, scope)` and return the new value. * * If the row doesn't exist yet, it's inserted with `value = start` and that * value is returned. Subsequent calls increment by `step`. * * Scope defaults to "default" — pass any string to namespace per * tenant / campaign / parent entity / etc. */ public async advance( name: string, scope: string = "default", opts: { startWith?: number; incrementBy?: number } = {}, ): Promise<number> { const start = opts.startWith ?? 1; const step = opts.incrementBy ?? 1; const table = this.repository.table; const updated = await this.repository.upsert( { id: this.crypto.randomUUID(), name, scope, value: start, }, { target: ["name", "scope"], set: { // Atomic: `value = current_value + step`. Both PG and SQLite (incl. D1) // resolve the right-hand `value` against the existing row's column. value: sql`${table.value} + ${step}`, }, }, ); return Number(updated.value ?? start); } /** * Read the current value without advancing it. Returns `null` if the * `(name, scope)` pair has never been advanced. */ public async peek( name: string, scope: string = "default", ): Promise<number | null> { const row = await this.repository.findOne({ where: { name: { eq: name }, scope: { eq: scope }, } as any, }); return row ? Number(row.value) : null; } /** * Reset the counter for `(name, scope)` to an explicit value. Creates the row * if it doesn't exist. * * Mostly useful in tests and ops scripts — production code should rarely call * this, since it can produce duplicate IDs if anything still holds a previously * advanced value. */ public async reset( name: string, scope: string = "default", value: number, ): Promise<void> { await this.repository.upsert( { id: this.crypto.randomUUID(), name, scope, value }, { target: ["name", "scope"], set: { value } }, ); } }