UNPKG

alepha

Version:

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

130 lines (109 loc) 4.28 kB
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); }); }); });