alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
116 lines (99 loc) • 4.28 kB
text/typescript
import { $inject, Alepha } from "alepha";
import { describe, expect, it } from "vitest";
import { $sequence } from "../core/primitives/$sequence.ts";
import { DatabaseProvider } from "../core/providers/drivers/DatabaseProvider.ts";
import { AlephaOrmPostgres } from "../postgres/index.ts";
describe("$sequence", () => {
class App {
seq = $sequence();
seq2 = $sequence({ startWith: 100, incrementBy: 2 });
perCampaign = $sequence();
}
const alepha = Alepha.create().with(AlephaOrmPostgres);
const app = alepha.inject(App);
it("should generate sequential numbers", async () => {
expect(await app.seq.next()).toBe(1);
expect(await app.seq.next()).toBe(2);
expect(await app.seq.next()).toBe(3);
expect(await app.seq.current()).toBe(3);
expect(await app.seq.next()).toBe(4);
});
it("should support custom start and increment options", async () => {
expect(await app.seq2.next()).toBe(100);
expect(await app.seq2.next()).toBe(102);
expect(await app.seq2.next()).toBe(104);
expect(await app.seq2.current()).toBe(104);
expect(await app.seq2.next()).toBe(106);
});
it("should keep independent counters per scope", async () => {
// Each scope starts from the same start value, independent of every other.
expect(await app.perCampaign.next("campaign-1")).toBe(1);
expect(await app.perCampaign.next("campaign-1")).toBe(2);
expect(await app.perCampaign.next("campaign-2")).toBe(1);
expect(await app.perCampaign.next("campaign-2")).toBe(2);
expect(await app.perCampaign.next("campaign-1")).toBe(3);
expect(await app.perCampaign.current("campaign-1")).toBe(3);
expect(await app.perCampaign.current("campaign-2")).toBe(2);
expect(await app.perCampaign.current("never-touched")).toBeNull();
});
it("should reset to an explicit value", async () => {
await app.perCampaign.reset(42, "campaign-1");
expect(await app.perCampaign.current("campaign-1")).toBe(42);
expect(await app.perCampaign.next("campaign-1")).toBe(43);
});
});
describe("$sequence inside $transactional", () => {
/**
* Sequence calls participate in the surrounding transaction (because they
* route through Repository, which auto-joins `alepha.orm.tx`). This locks
* the contract:
* - commit → counter advanced
* - rollback → counter unchanged (same value handed out on the next call)
*
* This is DIFFERENT from PG-native `nextval()` (which is tx-independent and
* leaves gaps on rollback). The chosen semantics are intentional: in app
* code, a failed insert that consumed a shortId should give the id back.
*/
class App {
seq = $sequence();
db = $inject(DatabaseProvider);
}
const alepha = Alepha.create().with(AlephaOrmPostgres);
const app = alepha.inject(App);
it("commits the increment together with the surrounding tx", async () => {
await app.db.transactional(async () => {
const n = await app.seq.next("tx-commit");
expect(n).toBe(1);
});
expect(await app.seq.current("tx-commit")).toBe(1);
expect(await app.seq.next("tx-commit")).toBe(2);
});
it("rolls the increment back together with the surrounding tx", async () => {
await expect(
app.db.transactional(async () => {
const n = await app.seq.next("tx-rollback");
expect(n).toBe(1);
throw new Error("boom");
}),
).rejects.toThrow("boom");
// Counter unchanged — rolled back with the outer tx.
expect(await app.seq.current("tx-rollback")).toBeNull();
// Next call hands out the same value the rolled-back call saw.
expect(await app.seq.next("tx-rollback")).toBe(1);
});
it("isolates rollbacks per scope", async () => {
// Bump scope A first, outside any tx.
expect(await app.seq.next("scope-a")).toBe(1);
await expect(
app.db.transactional(async () => {
await app.seq.next("scope-a"); // → 2 in this tx
await app.seq.next("scope-b"); // → 1 in this tx
throw new Error("rollback");
}),
).rejects.toThrow("rollback");
// scope-a returns to its pre-tx value of 1.
expect(await app.seq.current("scope-a")).toBe(1);
// scope-b never persisted at all.
expect(await app.seq.current("scope-b")).toBeNull();
});
});