alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
104 lines (97 loc) • 3.42 kB
text/typescript
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 } },
);
}
}