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