Matchers are now exactly what @endo/patterns makes: tagged records as from pass-style's makeTagged (M.string() is match:string with payload [], M.any() is match:any with payload undefined). Structured clone drops their symbol-keyed and non-enumerable properties, so they cross DO storage and Workers RPC in marshal's smallcaps encoding (the subset patterns need). MapStoreDo is a MapStore as a DO of its own: its creator fixes label and (encoded) shapes once; its id is full authority, and readOnly() hands out the queries alone. Generated-by: Claude Opus 5.5
143 lines
5.8 KiB
TypeScript
143 lines
5.8 KiB
TypeScript
import { env } from "cloudflare:workers";
|
|
import { evictDurableObject } from "cloudflare:test";
|
|
import { describe, expect, it } from "vitest";
|
|
import { M, fromSmallcaps, makeTagged, matches, mustMatch, toSmallcaps } from "../src/patterns.js";
|
|
import { encodeShapes } from "../src/store-do.js";
|
|
|
|
type TestEnv = {
|
|
STORE_DO: DurableObjectNamespace<import("./test-worker.js").StoreHost>;
|
|
MAP_STORE_DO: DurableObjectNamespace<import("../src/store-do.js").MapStoreDo>;
|
|
};
|
|
const testEnv = env as unknown as TestEnv;
|
|
|
|
const SECRET = "session=s3kr1t";
|
|
|
|
/** the message an RPC call fails with, or "" if it succeeds */
|
|
async function errorOf(call: Promise<unknown>): Promise<string> {
|
|
try {
|
|
await call;
|
|
return "";
|
|
} catch (err) {
|
|
return String(err);
|
|
}
|
|
}
|
|
|
|
describe("patterns", () => {
|
|
it("M.string() matches strings only; a literal matches only itself", () => {
|
|
expect(matches("x", M.string())).toBe(true);
|
|
expect(matches(1, M.string())).toBe(false);
|
|
expect(matches(undefined, M.any())).toBe(true);
|
|
expect(matches("cookie", "cookie")).toBe(true);
|
|
expect(matches("authorization", "cookie")).toBe(false);
|
|
});
|
|
|
|
it("matchers are endo's tagged records", () => {
|
|
const s = M.string() as unknown as Record<string | symbol, unknown>;
|
|
expect(s[Symbol.for("passStyle")]).toBe("tagged");
|
|
expect(s[Symbol.toStringTag]).toBe("match:string");
|
|
expect(Object.keys(s)).toEqual(["payload"]);
|
|
expect(s.payload).toEqual([]);
|
|
expect((M.any() as unknown as Record<string, unknown>).payload).toBeUndefined();
|
|
expect(Object.isFrozen(s)).toBe(true);
|
|
});
|
|
|
|
it("smallcaps: endo's encoding, round-tripped", () => {
|
|
expect(toSmallcaps(M.string())).toEqual({ "#tag": "match:string", payload: [] });
|
|
expect(toSmallcaps(M.any())).toEqual({ "#tag": "match:any", payload: "#undefined" });
|
|
expect(toSmallcaps("#cookie")).toBe("!#cookie");
|
|
expect(toSmallcaps([NaN, -0, Infinity])).toEqual(["#NaN", 0, "#Infinity"]);
|
|
const back = fromSmallcaps(toSmallcaps(M.string())) as Parameters<typeof matches>[1];
|
|
expect(matches("x", back)).toBe(true);
|
|
expect(matches(1, back)).toBe(false);
|
|
expect(fromSmallcaps("!#cookie")).toBe("#cookie");
|
|
expect(() => matches("x", makeTagged("match:nope", []))).toThrow("not a pattern");
|
|
});
|
|
|
|
it("mustMatch names the shape but not the specimen", () => {
|
|
expect(() => mustMatch(SECRET, "cookie", "jar: key")).toThrow('jar: key: must match "cookie"');
|
|
expect(() => mustMatch(SECRET, "cookie", "jar: key")).not.toThrow(SECRET);
|
|
});
|
|
});
|
|
|
|
describe("MapStore over RPC", () => {
|
|
const fresh = () => testEnv.STORE_DO.get(testEnv.STORE_DO.newUniqueId());
|
|
|
|
it("rotates: the admin's set is what the read-only view sees next", async () => {
|
|
const store = await fresh().cookies();
|
|
const rd = await store.readOnly();
|
|
await store.init("cookie", "session=old");
|
|
expect(await rd.get("cookie")).toBe("session=old");
|
|
await store.set("cookie", "session=new");
|
|
expect(await rd.entries()).toEqual([["cookie", "session=new"]]);
|
|
});
|
|
|
|
it("the read-only view has no write methods", async () => {
|
|
const store = await fresh().cookies();
|
|
const rd = (await store.readOnly()) as unknown as { set(k: string, v: string): Promise<void> };
|
|
expect(await errorOf(rd.set("cookie", SECRET))).not.toBe("");
|
|
expect(await store.has("cookie")).toBe(false);
|
|
});
|
|
|
|
it("the key shape admits only `cookie`; errors carry no values", async () => {
|
|
const store = await fresh().cookies();
|
|
const err = await errorOf(store.init("authorization", SECRET));
|
|
expect(err).toContain('cookie: key: must match "cookie"');
|
|
expect(err).not.toContain(SECRET);
|
|
expect(await errorOf(store.init("cookie", 42 as unknown as string))).toContain("cookie: value: must match M.string()");
|
|
});
|
|
|
|
it("init refuses a present key; set and get refuse a missing one", async () => {
|
|
const store = await fresh().cookies();
|
|
expect(await errorOf(store.set("cookie", SECRET))).toContain("cookie: key not found");
|
|
expect(await errorOf(store.get("cookie"))).toContain("cookie: key not found");
|
|
await store.init("cookie", SECRET);
|
|
const err = await errorOf(store.init("cookie", SECRET));
|
|
expect(err).toContain("cookie: key already registered");
|
|
expect(err).not.toContain(SECRET);
|
|
});
|
|
|
|
it("survives eviction: entries live in DO storage", async () => {
|
|
const stub = fresh();
|
|
{
|
|
// disposed before eviction: a live store stub holds the DO open
|
|
using store = await stub.cookies();
|
|
await store.init("cookie", "session=durable");
|
|
}
|
|
await evictDurableObject(stub);
|
|
using again = await stub.cookies();
|
|
expect(await again.get("cookie")).toBe("session=durable");
|
|
expect(await again.getSize()).toBe(1);
|
|
await again.delete("cookie");
|
|
expect(await again.keys()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("MapStoreDo", () => {
|
|
const COOKIE_SHAPES = encodeShapes({ keyShape: "cookie", valueShape: M.string() });
|
|
const fresh = () => testEnv.MAP_STORE_DO.get(testEnv.MAP_STORE_DO.newUniqueId());
|
|
|
|
it("keeps the shapes its creator chose, across eviction", async () => {
|
|
const stub = fresh();
|
|
await stub.configure("cookie", COOKIE_SHAPES);
|
|
await evictDurableObject(stub);
|
|
{
|
|
using store = await stub.store();
|
|
expect(await errorOf(store.init("authorization", SECRET))).toContain('cookie: key: must match "cookie"');
|
|
expect(await errorOf(store.init("cookie", 42))).toContain("cookie: value: must match M.string()");
|
|
await store.init("cookie", "session=a");
|
|
}
|
|
using rd = await stub.readOnly();
|
|
expect(await rd.entries()).toEqual([["cookie", "session=a"]]);
|
|
});
|
|
|
|
it("configures once: the same shapes again is a no-op, others are refused", async () => {
|
|
const stub = fresh();
|
|
await stub.configure("cookie", COOKIE_SHAPES);
|
|
await stub.configure("cookie", encodeShapes({ keyShape: "cookie", valueShape: M.string() }));
|
|
expect(await errorOf(stub.configure("cookie", encodeShapes({ keyShape: M.string() })))).toContain(
|
|
"already configured",
|
|
);
|
|
expect(await errorOf(fresh().store())).toContain("not configured");
|
|
});
|
|
});
|