UNPKG

alepha

Version:

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

102 lines (89 loc) 3.46 kB
import { $inject, createPrimitive, KIND, Primitive } from "alepha"; import { SequenceProvider } from "../providers/SequenceProvider.ts"; /** * Declare a portable, scoped numeric sequence. * * Works identically on Postgres, SQLite, and Cloudflare D1 — backed by the * shared `alepha_sequences` table managed by {@link SequenceProvider}. * * @example * ```ts * class Quests { * // Sequence name defaults to the property key — here "shortId". * shortId = $sequence(); * * async create(campaignId: number, data: ...) { * // Global counter — same primitive, one shared "default" scope. * const n = await this.shortId.next(); * * // Scoped counter — one independent sequence per campaign. * const perCampaign = await this.shortId.next(String(campaignId)); * } * } * ``` * * @see {@link SequenceProvider} */ export const $sequence = ( options: SequencePrimitiveOptions = {}, ): SequencePrimitive => { return createPrimitive(SequencePrimitive, options); }; // --------------------------------------------------------------------------------------------------------------------- export interface SequencePrimitiveOptions { /** * Sequence name. Defaults to the primitive's property key. Two primitives * sharing the same name share the same counter table rows, which is rarely * what you want — prefer letting the property key drive the name. */ name?: string; /** * Starting value used on the very first `.next()` for a `(name, scope)` pair. * Defaults to `1`. */ startWith?: number; /** * Amount added on every call to `.next()`. Defaults to `1`. */ incrementBy?: number; } // --------------------------------------------------------------------------------------------------------------------- export class SequencePrimitive extends Primitive<SequencePrimitiveOptions> { protected readonly provider = $inject(SequenceProvider); public get name(): string { return this.options.name ?? this.config.propertyKey; } /** * Atomically advance the counter and return the new value. * * Scope defaults to "default". Pass any string to keep an independent counter * per tenant / campaign / parent entity / etc. * * **Transaction semantics:** when called inside a `$transactional` block, the * increment participates in that transaction — commit advances the counter, * rollback unwinds it. This is intentionally different from PG-native * `nextval()` (which leaves gaps on rollback). It means a failed insert that * consumed a `shortId` returns the value to the pool instead of burning it. */ public async next(scope: string = "default"): Promise<number> { return this.provider.advance(this.name, scope, { startWith: this.options.startWith, incrementBy: this.options.incrementBy, }); } /** * Read the current value without advancing it. Returns `null` if the * `(name, scope)` pair has never been advanced. */ public async current(scope: string = "default"): Promise<number | null> { return this.provider.peek(this.name, scope); } /** * Reset the counter for the given scope to an explicit value. Creates the row * if it doesn't exist. Mostly useful in tests and ops scripts. */ public async reset(value: number, scope: string = "default"): Promise<void> { return this.provider.reset(this.name, scope, value); } } $sequence[KIND] = SequencePrimitive;