alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
130 lines (109 loc) • 4.28 kB
text/typescript
import { AsyncLocalStorage } from "node:async_hooks";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { Alepha } from "../Alepha.ts";
import { AlephaError } from "../errors/AlephaError.ts";
import { $atom } from "../primitives/$atom.ts";
import { $computed } from "../primitives/$computed.ts";
import { AlsProvider } from "../providers/AlsProvider.ts";
import { z } from "../providers/TypeProvider.ts";
describe("$computed", () => {
const cart = $atom({
name: "test.computed.cart",
schema: z.object({
items: z.array(z.object({ price: z.number(), qty: z.number() })),
}),
default: { items: [] },
});
const total = $computed({
name: "test.computed.total",
deps: [cart],
get: (c) => c.items.reduce((sum, item) => sum + item.price * item.qty, 0),
});
const doubled = $computed({
name: "test.computed.doubled",
deps: [total],
get: (t) => t * 2,
});
it("computes from dependency defaults", () => {
const alepha = Alepha.create();
expect(alepha.store.get(total)).toBe(0);
});
it("reflects dependency mutations", () => {
const alepha = Alepha.create();
alepha.store.set(cart, { items: [{ price: 10, qty: 2 }] });
expect(alepha.store.get(total)).toBe(20);
});
it("supports computed-on-computed", () => {
const alepha = Alepha.create();
alepha.store.set(cart, { items: [{ price: 5, qty: 1 }] });
expect(alepha.store.get(doubled)).toBe(10);
});
it("lists transitive dependency keys", () => {
expect(doubled.keys()).toEqual(["test.computed.cart"]);
});
it("cannot be set", () => {
const alepha = Alepha.create();
expect(() => alepha.store.set(total as any, 5)).toThrow(AlephaError);
});
it("is never exported for hydration", () => {
const alepha = Alepha.create();
alepha.store.get(total);
expect(Object.keys(alepha.store.exportAtoms())).not.toContain(
"test.computed.total",
);
});
it("is readable through alepha.get", () => {
const alepha = Alepha.create();
alepha.store.set(cart, { items: [{ price: 3, qty: 3 }] });
expect(alepha.get(total)).toBe(9);
});
// "Keeps ALS fork-scoped deps correct for free" is the stated design
// rationale for $computed's no-cache decision (see this file's brief and
// $computed.ts's JSDoc) — exercise it directly instead of trusting code
// inspection. `AlsProvider.create` defaults to a no-op (`fork()` becomes a
// plain function call); swap in a real `AsyncLocalStorage` for this block
// only, mirroring `fork.spec.ts`.
describe("inside a fork", () => {
const originalAlsCreate = AlsProvider.create;
beforeAll(() => {
AlsProvider.create = () => new AsyncLocalStorage();
});
afterAll(() => {
AlsProvider.create = originalAlsCreate;
});
it("resolves a dependency from the fork's own value, not the app store's", () => {
const alepha = new Alepha();
alepha.store.set(cart, { items: [{ price: 10, qty: 1 }] });
expect(alepha.store.get(total)).toBe(10);
alepha.fork(() => {
alepha.store.set(cart, { items: [{ price: 2, qty: 3 }] });
expect(alepha.store.get(total)).toBe(6);
});
// The fork-scoped mutation must not leak back into the app-level store.
expect(alepha.store.get(total)).toBe(10);
});
});
describe("circular dependencies", () => {
it("throws AlephaError instead of overflowing the stack", () => {
const cycleA = $computed({
name: "test.computed.cycleA",
deps: [],
get: () => 0,
});
const cycleB = $computed({
name: "test.computed.cycleB",
deps: [cycleA],
get: (v: number) => v + 1,
});
// `deps` is `readonly` only at the type level — nothing stops a
// caller from mutating the underlying array at runtime and building a
// genuine cycle. That's exactly what this test does to exercise the
// guard; there is no supported public API to construct a cycle any
// other way.
(cycleA.options.deps as unknown[]).push(cycleB);
expect(() => cycleA.keys()).toThrow(AlephaError);
const alepha = Alepha.create();
expect(() => alepha.store.get(cycleA)).toThrow(AlephaError);
});
});
});