alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
800 lines (655 loc) • 26.7 kB
text/typescript
import { AsyncLocalStorage } from "node:async_hooks";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { Alepha } from "../Alepha.ts";
import { AlephaError } from "../errors/AlephaError.ts";
import { TypeBoxError } from "../errors/TypeBoxError.ts";
import { $atom } from "../primitives/$atom.ts";
import { $computed } from "../primitives/$computed.ts";
import { AlsProvider } from "../providers/AlsProvider.ts";
import { StateManager } from "../providers/StateManager.ts";
import { z } from "../providers/TypeProvider.ts";
// Set up AsyncLocalStorage for tests
AlsProvider.create = () => new AsyncLocalStorage();
// Test with custom state interface
interface TestState {
name?: string;
age?: number;
active?: boolean;
config?: {
theme: string;
};
}
describe("StateManager", () => {
let stateManager: StateManager;
beforeEach(() => {
stateManager = Alepha.create().inject(StateManager);
});
describe("Basic Operations", () => {
it("should set and get values with proper typing", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
typedManager.set("name", "John");
typedManager.set("age", 30);
typedManager.set("active", true);
expect(typedManager.get("name")).toBe("John");
expect(typedManager.get("age")).toBe(30);
expect(typedManager.get("active")).toBe(true);
});
it("should return undefined for non-existent keys", () => {
expect(stateManager.get("nonExistent" as any)).toBeUndefined();
});
it("should check if keys exist", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
typedManager.set("name", "John");
expect(typedManager.has("name")).toBe(true);
expect(typedManager.has("age")).toBe(false);
});
it("should handle different data types", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
typedManager.set("name", "hello");
typedManager.set("age", 42);
typedManager.set("active", true);
typedManager.set("config", { theme: "dark" });
expect(typedManager.get("name")).toBe("hello");
expect(typedManager.get("age")).toBe(42);
expect(typedManager.get("active")).toBe(true);
expect(typedManager.get("config")).toEqual({ theme: "dark" });
});
it("should clear all state", () => {
const typedManager = Alepha.create().inject(StateManager<TestState>);
typedManager.set("name", "John");
typedManager.set("age", 30);
typedManager.clear();
expect(typedManager.has("name")).toBe(false);
expect(typedManager.has("age")).toBe(false);
});
it("should return all keys", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
typedManager.set("name", "John");
typedManager.set("age", 30);
const keys = typedManager.keys();
expect(keys).toEqual(expect.arrayContaining(["name", "age"]));
expect(keys.length).toBe(2);
});
});
describe("Mutation Listeners", () => {
it("should call listeners when values change", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
const listener = vi.fn();
alepha.events.on("state:mutate", listener);
typedManager.set("name", "John");
expect(listener).toHaveBeenCalledWith({
key: "name",
value: "John",
prevValue: undefined,
});
});
it("should call listeners with previous values", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
typedManager.set("name", "initial");
const listener = vi.fn();
alepha.events.on("state:mutate", listener);
typedManager.set("name", "updated");
expect(listener).toHaveBeenCalledWith({
key: "name",
value: "updated",
prevValue: "initial",
});
});
it("should return unsubscribe function", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
const listener = vi.fn();
const unsubscribe = alepha.events.on("state:mutate", listener);
typedManager.set("name", "value1");
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
typedManager.set("name", "value2");
expect(listener).toHaveBeenCalledTimes(1);
});
it("should support multiple listeners for same key", async () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
const listener1 = vi.fn();
const listener2 = vi.fn();
alepha.events.on("state:mutate", listener1);
alepha.events.on("state:mutate", listener2);
typedManager.set("name", "value");
await new Promise((r) => setTimeout(r, 10));
expect(listener1).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(1);
});
});
describe("Chaining", () => {
it("should support method chaining", () => {
const alepha = new Alepha();
const typedManager = alepha.inject(StateManager<TestState>);
const result = typedManager.set("name", "John").set("age", 30).clear();
expect(result).toBe(typedManager);
expect(typedManager.has("name")).toBe(false);
});
});
describe("AsyncLocalStorage Integration", () => {
let alepha: Alepha;
let stateManager: StateManager<TestState>;
beforeEach(() => {
alepha = new Alepha();
stateManager = alepha.store as unknown as StateManager<TestState>;
});
it("should use ALS when available and context exists", async () => {
// Set up ALS context with some initial state
const testValue = alepha.context.run(
() => {
return stateManager.get("name");
},
{ name: "ALS Value", context: "test-context" },
);
expect(testValue).toBe("ALS Value");
});
it("should set and get values within ALS context", async () => {
const result = alepha.context.run(
() => {
stateManager.set("name", "ALS Value");
return stateManager.get("name");
},
{ context: "test-context" },
);
expect(result).toBe("ALS Value");
});
it("should use local store when ALS context doesn't exist", () => {
// Outside of ALS context, should use local store
stateManager.set("name", "Local Value");
expect(stateManager.get("name")).toBe("Local Value");
});
it("should set values in ALS when context exists", async () => {
// Note: AlsProvider spreads the store object, so we check values inside the callback
const result = alepha.context.run(
() => {
stateManager.set("name", "ALS Value");
return stateManager.get("name");
},
{ context: "test-context" },
);
expect(result).toBe("ALS Value");
});
it("should set values in local store when no ALS context", () => {
// Outside ALS context, values go to local store
stateManager.set("name", "Local Value");
expect(stateManager.get("name")).toBe("Local Value");
});
it("should emit events when setting values through ALS", async () => {
const listener = vi.fn();
alepha.events.on("state:mutate", listener);
const store = { context: "test-context" };
alepha.context.run(() => {
stateManager.set("name", "ALS Value");
}, store);
expect(listener).toHaveBeenCalledWith({
key: "name",
value: "ALS Value",
prevValue: undefined,
});
});
it("should emit events with previous ALS values", async () => {
const listener = vi.fn();
alepha.events.on("state:mutate", listener);
const store = { name: "Initial ALS", context: "test-context" };
alepha.context.run(() => {
stateManager.set("name", "Updated ALS");
}, store);
expect(listener).toHaveBeenCalledWith({
key: "name",
value: "Updated ALS",
prevValue: "Initial ALS",
});
});
it("should handle state manager without ALS provider", () => {
const typedManager = Alepha.create().inject(StateManager<TestState>);
typedManager.set("name", "Value");
expect(typedManager.get("name")).toBe("Value");
});
it("should prefer ALS values over local store when both exist", async () => {
// Set value in local store first
stateManager.set("name", "Local Value");
// In ALS context, ALS values should take priority
// Note: AlsProvider spreads the store object, so we check values inside the callback
const result = alepha.context.run(
() => {
stateManager.set("name", "ALS Priority");
return stateManager.get("name");
},
{ context: "test-context" },
);
expect(result).toBe("ALS Priority");
});
it("should not skip event emission when values are the same", async () => {
const listener = vi.fn();
alepha.events.on("state:mutate", listener);
const store = { name: "Same Value", context: "test-context" };
alepha.context.run(() => {
// Setting the same value should not emit event
stateManager.set("name", "Same Value");
}, store);
expect(listener).not.toHaveBeenCalled();
});
it("should delete values from ALS when context exists", async () => {
// Note: AlsProvider spreads the store object, so we check values inside the callback
const result = alepha.context.run(
() => {
stateManager.set("name", "To Delete");
stateManager.del("name");
return stateManager.get("name");
},
{ context: "test-context" },
);
expect(result).toBeUndefined();
});
it("should clear only local store, not ALS", async () => {
// Set value in local store
stateManager.set("age", 25);
// Clear local store
stateManager.clear();
// ALS value should still be accessible within ALS context
const store = { name: "ALS Value", context: "test-context" };
const result = alepha.context.run(() => {
return stateManager.get("name");
}, store);
expect(result).toBe("ALS Value");
// Local value should be gone
expect(stateManager.get("age")).toBeUndefined();
});
});
describe("Atom Validation", () => {
const prefs = $atom({
name: "test.validation.prefs",
schema: z.object({ theme: z.string(), count: z.number() }),
default: { theme: "light", count: 0 },
});
it("rejects a write that violates the schema", () => {
const alepha = Alepha.create();
expect(() =>
alepha.store.set(prefs, { theme: "dark", count: "nope" } as any),
).toThrow(TypeBoxError);
});
it("strips unknown keys on write", () => {
const alepha = Alepha.create();
alepha.store.set(prefs, { theme: "dark", count: 1, extra: true } as any);
expect(alepha.store.get(prefs)).toEqual({ theme: "dark", count: 1 });
});
it("validates writes made through the raw string key once registered", () => {
const alepha = Alepha.create();
alepha.store.get(prefs); // registers the atom
expect(() =>
alepha.store.set("test.validation.prefs" as any, { theme: 1 } as any),
).toThrow();
});
it("skips validation when skipValidation is set", () => {
const alepha = Alepha.create();
alepha.store.get(prefs);
alepha.store.set("test.validation.prefs" as any, { theme: 1 } as any, {
skipValidation: true,
});
expect((alepha.store.get(prefs) as any).theme).toBe(1);
});
it("falls back to the default when a pre-seeded value is invalid", () => {
const alepha = Alepha.create({
"test.validation.prefs": { theme: 42 },
} as any);
expect(alepha.store.get(prefs)).toEqual({ theme: "light", count: 0 });
});
it("warns when a pre-seeded value is invalid and falls back to the default", () => {
const warn = vi.fn();
const alepha = Alepha.create({
"test.validation.prefs": { theme: 42 },
"alepha.logger": {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
},
} as any);
expect(alepha.store.get(prefs)).toEqual({ theme: "light", count: 0 });
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toContain("test.validation.prefs");
});
it("decodes a valid pre-seeded value through the schema", () => {
const alepha = Alepha.create({
"test.validation.prefs": { theme: "dark", count: 2, junk: 1 },
} as any);
expect(alepha.store.get(prefs)).toEqual({ theme: "dark", count: 2 });
});
it("exposes registered atoms via getAtom", () => {
const alepha = Alepha.create();
alepha.store.get(prefs);
expect(alepha.store.getAtom("test.validation.prefs")).toBeDefined();
expect(alepha.store.getAtom("nope")).toBeUndefined();
});
});
describe("ALS-aware decode-at-registration", () => {
const settings = $atom({
name: "test.validation.als.settings",
schema: z.object({ theme: z.string(), count: z.number() }),
default: { theme: "light", count: 0 },
});
it("decodes a value written into a request-scoped ALS layer before the atom registers, in place", () => {
const alepha = Alepha.create();
const result = alepha.context.run(
() => {
// Simulate a fork-scoped write landing ahead of the atom's first
// access (e.g. $cookie mirroring a cookie value into the store
// during server:onRequest, before anything reads the atom).
alepha.store.set(
settings.key as any,
{
theme: "dark",
count: 1,
extra: true,
} as any,
);
return alepha.store.get(settings);
},
{ context: "req-1" },
);
expect(result).toEqual({ theme: "dark", count: 1 });
// The fork's value must not have leaked into the app-level store...
expect(alepha.store.get(settings, "app")).not.toEqual({
theme: "dark",
count: 1,
});
// ...but the app-level store MUST still have received the atom's
// declared default. Registration inside a fork is still a
// registration: leaving the app store empty here is what made every
// later, fork-less (or cookie-less) read resolve `undefined` instead
// of the default.
expect(alepha.store.get(settings, "app")).toEqual({
theme: "light",
count: 0,
});
});
it("still resolves the default outside the fork after an atom first registered inside one", () => {
const alepha = Alepha.create();
alepha.context.run(
() => {
alepha.store.set(
settings.key as any,
{
theme: "dark",
count: 1,
} as any,
);
// First registration happens here, inside the fork.
alepha.store.get(settings);
},
{ context: "req-1" },
);
// A later request (a different fork) carrying no seed of its own, and
// the app-level read outside any fork, both see the default.
const next = alepha.context.run(() => alepha.store.get(settings), {
context: "req-2",
});
expect(next).toEqual({ theme: "light", count: 0 });
expect(alepha.store.get(settings)).toEqual({ theme: "light", count: 0 });
});
it("falls back to the default inside the same ALS layer (not the app store) when the seeded value is invalid, and warns", () => {
const warn = vi.fn();
const alepha = Alepha.create({
"alepha.logger": {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn,
error: vi.fn(),
},
} as any);
const result = alepha.context.run(
() => {
alepha.store.set(settings.key as any, { theme: 42 } as any);
return alepha.store.get(settings);
},
{ context: "req-1" },
);
expect(result).toEqual({ theme: "light", count: 0 });
expect(warn).toHaveBeenCalledTimes(1);
// The invalid seed was replaced inside the ALS layer, and the app
// store holds the declared default (never the fork's value).
expect(alepha.store.get(settings, "app")).toEqual({
theme: "light",
count: 0,
});
});
it("hands out a fresh copy of the default when a seeded value is invalid — never the shared declaration", () => {
const alepha = Alepha.create();
const result = alepha.context.run(
() => {
alepha.store.set(settings.key as any, { theme: 42 } as any);
return alepha.store.get(settings);
},
{ context: "req-1" },
);
// A careless caller mutates the value it got back from the
// fallback-to-default path.
(result as { count: number }).count = 424242;
// `atom.options.default` is a module-level object shared by every
// container in the process. If the fallback aliased it instead of
// cloning it through the validator, that mutation would have rewritten
// the declaration itself — permanently, for every request.
expect(settings.options.default).toEqual({ theme: "light", count: 0 });
const fresh = Alepha.create();
expect(fresh.store.get(settings)).toEqual({ theme: "light", count: 0 });
});
it("decodes a value that landed in a parent fork layer before a nested fork registers the atom", () => {
const alepha = Alepha.create();
const result = alepha.context.run(
() => {
alepha.store.set(
settings.key as any,
{
theme: "dark",
count: 5,
} as any,
);
// A nested fork (e.g. a batched sub-request) is the one that
// first accesses the atom — the raw value lives one layer up.
return alepha.context.run(() => alepha.store.get(settings), {
context: "req-1-child",
});
},
{ context: "req-1" },
);
expect(result).toEqual({ theme: "dark", count: 5 });
});
});
describe("Mixed ALS and Local Store Scenarios", () => {
let alepha: Alepha;
let stateManager: StateManager<TestState>;
beforeEach(() => {
alepha = new Alepha();
stateManager = alepha.store as unknown as StateManager<TestState>;
});
it("should handle transitions between ALS and non-ALS contexts", async () => {
// Start with no ALS context
stateManager.set("name", "Local Value");
expect(stateManager.get("name")).toBe("Local Value");
// Switch to ALS context
const store = { context: "test-context" };
const alsResult = alepha.context.run(() => {
stateManager.set("name", "ALS Value");
return stateManager.get("name");
}, store);
expect(alsResult).toBe("ALS Value");
// Outside ALS context, should see local value again
expect(stateManager.get("name")).toBe("Local Value");
});
it("should handle has() method with local store", () => {
// Set in local store
stateManager.set("name", "Local");
// has() checks local store
expect(stateManager.has("name")).toBe(true);
expect(stateManager.has("age")).toBe(false);
});
it("should handle keys() method with local store", () => {
// Set in local store
stateManager.set("active", true);
// keys() returns local store keys
const keys = alepha.store.keys();
expect(keys).toEqual(["active"]);
});
});
describe("Reset, Watch, ServerOnly", () => {
const counter = $atom({
name: "test.extras.counter",
schema: z.object({ value: z.number() }),
default: { value: 0 },
});
const secret = $atom({
name: "test.extras.secret",
schema: z.object({ token: z.string() }),
default: { token: "" },
serverOnly: true,
});
const doubledCounter = $computed({
name: "test.extras.doubled",
deps: [counter],
get: (c) => c.value * 2,
});
const other = $atom({
name: "test.extras.other",
schema: z.object({ value: z.number() }),
default: { value: 0 },
});
const total = $computed({
name: "test.extras.total",
deps: [counter, other],
get: (c, o) => c.value + o.value,
});
it("reset restores the declared default", () => {
const alepha = Alepha.create();
alepha.store.set(counter, { value: 99 });
alepha.store.reset(counter);
expect(alepha.store.get(counter)).toEqual({ value: 0 });
});
it("reset is exposed on the Alepha instance", () => {
const alepha = Alepha.create();
alepha.store.set(counter, { value: 99 });
alepha.reset(counter);
expect(alepha.get(counter)).toEqual({ value: 0 });
});
it("watch fires with value and previous value", async () => {
const alepha = Alepha.create();
const calls: Array<[unknown, unknown]> = [];
alepha.store.watch(counter, (value, prevValue) => {
calls.push([value, prevValue]);
});
alepha.store.set(counter, { value: 1 });
await new Promise((r) => setTimeout(r, 0));
expect(calls).toEqual([[{ value: 1 }, { value: 0 }]]);
});
it("watch returns an unsubscribe function", async () => {
const alepha = Alepha.create();
const calls: unknown[] = [];
const off = alepha.store.watch(counter, (value) => {
calls.push(value);
});
off();
alepha.store.set(counter, { value: 1 });
await new Promise((r) => setTimeout(r, 0));
expect(calls).toEqual([]);
});
it("watch supports computed values", async () => {
const alepha = Alepha.create();
const calls: Array<[unknown, unknown]> = [];
alepha.store.watch(doubledCounter, (value, prevValue) => {
calls.push([value, prevValue]);
});
alepha.store.set(counter, { value: 5 });
await new Promise((r) => setTimeout(r, 0));
expect(calls).toEqual([[10, 0]]);
});
it("register refuses a Computed, so it can never reach exportAtoms", () => {
const alepha = Alepha.create();
// `reset()`/`register()` are the only atom entry points that took a
// Computed without complaint. A registered Computed would land in the
// atom registry and from there be serialized into the SSR hydration
// payload — breaking the "never stored, never serialized" contract.
expect(() => alepha.store.reset(doubledCounter as any)).toThrow(
AlephaError,
);
expect(() => alepha.store.register(doubledCounter as any)).toThrow(
/Cannot register computed value/,
);
expect(alepha.store.getAtom("test.extras.doubled")).toBeUndefined();
expect(alepha.store.exportAtoms()["test.extras.doubled"]).toBeUndefined();
});
it("serverOnly atoms are excluded from exportAtoms", () => {
const alepha = Alepha.create();
alepha.store.set(counter, { value: 1 });
alepha.store.set(secret, { token: "s3cret" });
const exported = alepha.store.exportAtoms();
expect(exported["test.extras.counter"]).toEqual({ value: 1 });
expect(exported["test.extras.secret"]).toBeUndefined();
});
it("serverOnly atoms are excluded from exportAtoms('current') — the ALS/SSR branch", () => {
// This is the branch ReactServerTemplateProvider.buildHydrationData()
// actually calls during SSR (exportAtoms("current")), serializing the
// result straight into the <script id="__ssr"> hydration payload. The
// default-branch test above does not exercise this code path at all.
const alepha = Alepha.create();
const exported = alepha.context.run(
() => {
alepha.store.set(counter, { value: 7 });
alepha.store.set(secret, { token: "s3cret" });
return alepha.store.exportAtoms("current");
},
{ context: "test.extras.ssr-fork" },
);
expect(exported["test.extras.counter"]).toEqual({ value: 7 });
expect(exported["test.extras.secret"]).toBeUndefined();
});
it("watch on a computed tracks prevValue correctly across two different dependency keys", async () => {
const alepha = Alepha.create();
const calls: Array<[unknown, unknown]> = [];
alepha.store.watch(total, (value, prevValue) => {
calls.push([value, prevValue]);
});
// Mutate the first dep key.
alepha.store.set(counter, { value: 3 });
await new Promise((r) => setTimeout(r, 0));
// Then mutate a *different* dep key. The computed's own `prev` must
// have been updated after the first callback, or this second call
// would report a stale prevValue.
alepha.store.set(other, { value: 4 });
await new Promise((r) => setTimeout(r, 0));
expect(calls).toEqual([
[3, 0],
[7, 3],
]);
});
it("reset's clone of the default cannot corrupt future resets (zod parse always returns a fresh object)", () => {
const alepha = Alepha.create();
alepha.store.set(counter, { value: 99 });
alepha.store.reset(counter);
const resetValue = alepha.store.get(counter);
expect(resetValue).toEqual({ value: 0 });
// A careless caller mutates the value handed back by reset/get.
(resetValue as { value: number }).value = 424242;
// The atom's declared default itself must be untouched: `reset()` ->
// `set()` -> `SchemaValidator.validate()` runs `schema.safeParse()`,
// and zod's object parsing always returns a brand-new object, never
// the same reference as `atom.options.default` — even when the input
// is already valid. So the mutation above can never reach here.
expect(counter.options.default).toEqual({ value: 0 });
// Prove it end-to-end too: a fresh container resets to the pristine
// default, unaffected by the mutation performed above.
const freshAlepha = Alepha.create();
freshAlepha.store.set(counter, { value: 1 });
freshAlepha.store.reset(counter);
expect(freshAlepha.store.get(counter)).toEqual({ value: 0 });
});
});
});