@lunora/platform 1.0.0-alpha.2 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
1
  export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.mjs";
2
- import "../packem_shared/socket-host.d-Cq5uVbiH.mjs";
2
+ import "../packem_shared/socket-host.d-dVPE86WP.mjs";
@@ -1,2 +1,2 @@
1
1
  export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.js";
2
- import "../packem_shared/socket-host.d-Cq5uVbiH.js";
2
+ import "../packem_shared/socket-host.d-dVPE86WP.js";
@@ -1 +1 @@
1
- import{createReferenceHost as o}from"../packem_shared/createReferenceHost-GO8Hp4ft.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
1
+ import{createReferenceHost as o}from"../packem_shared/createReferenceHost-Cvx-Socp.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
@@ -1,4 +1,4 @@
1
- import { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-Cq5uVbiH.mjs";
1
+ import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-dVPE86WP.mjs";
2
2
  /**
3
3
  * A conformance host bundles all four platform contracts so a single factory
4
4
  * can stand up a complete, isolated test environment.
@@ -12,6 +12,15 @@ interface ConformanceHost {
12
12
  * contract — platform delivery is the platform's test, not the adapter's.
13
13
  */
14
14
  awaitAlarmFired?: (target: number) => Promise<void>;
15
+ /**
16
+ * Resolve once the host has actually dispatched a scheduled job — invoked
17
+ * its delivery path, not merely expired the timer. Optional, but NOT for a
18
+ * host that declares `scheduler.deadLetter`: that member is the
19
+ * at-least-once claim, and a host that cannot show the TCK a dispatch is
20
+ * claiming what the suite cannot check.
21
+ * @returns `true` when the job was dispatched at least once.
22
+ */
23
+ awaitJobDispatched?: (id: string) => Promise<boolean>;
15
24
  /** Optional cleanup hook (close DBs, release timers). */
16
25
  cleanup?: () => void;
17
26
  /**
@@ -23,6 +32,22 @@ interface ConformanceHost {
23
32
  createSocket?: () => unknown;
24
33
  /** The shard directory under test. */
25
34
  directory: ShardDirectory;
35
+ /**
36
+ * Terminally dispose this host instance, as opposed to {@link
37
+ * ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
38
+ * `cleanup`) use as a per-test reset rather than a true teardown — the DO's
39
+ * storage has no explicit close a test can drive, so `cleanup` there just
40
+ * disarms the pending alarm and drops socket references for the next run.
41
+ *
42
+ * Optional: only a host with a real terminal dispose implements it. Where
43
+ * it exists, the suite calls it once and then asserts every surface that
44
+ * documents a post-close behaviour (`ShardHost.alarms`,
45
+ * `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
46
+ * closed with a `"platform closed: …"` error — the same "report the gap
47
+ * instead of asserting a false close" pattern `scheduler`/`kv` already use
48
+ * for hosts that don't implement a surface at all.
49
+ */
50
+ disposeTerminally?: () => void;
26
51
  /**
27
52
  * The durable key-value store under test. Optional: a host that implements
28
53
  * only the reactive-engine half (`ShardHost`) has no KV surface to offer,
@@ -111,22 +136,5 @@ type VitestApi = {
111
136
  expect: typeof import("vitest").expect;
112
137
  it: typeof import("vitest").it;
113
138
  };
114
- /**
115
- * Define the host-contract conformance suite for the given factory.
116
- *
117
- * The suite asserts the provider-neutral behaviors that every Lunora host must
118
- * provide: single-writer serialization, durable transactions, local SQL,
119
- * durable alarms, socket accept/send/close, attachment round-trip across
120
- * recycle, deterministic shard placement, and durable scheduling.
121
- *
122
- * Usage:
123
- *
124
- * ```ts
125
- * import { describe, expect, it } from "vitest";
126
- * import { createReferenceHost, defineHostContractSuite } from "@lunora/platform/conformance";
127
- *
128
- * defineHostContractSuite("reference", createReferenceHost, { describe, expect, it });
129
- * ```
130
- */
131
139
  declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
132
140
  export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
@@ -1,4 +1,4 @@
1
- import { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-Cq5uVbiH.js";
1
+ import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-dVPE86WP.js";
2
2
  /**
3
3
  * A conformance host bundles all four platform contracts so a single factory
4
4
  * can stand up a complete, isolated test environment.
@@ -12,6 +12,15 @@ interface ConformanceHost {
12
12
  * contract — platform delivery is the platform's test, not the adapter's.
13
13
  */
14
14
  awaitAlarmFired?: (target: number) => Promise<void>;
15
+ /**
16
+ * Resolve once the host has actually dispatched a scheduled job — invoked
17
+ * its delivery path, not merely expired the timer. Optional, but NOT for a
18
+ * host that declares `scheduler.deadLetter`: that member is the
19
+ * at-least-once claim, and a host that cannot show the TCK a dispatch is
20
+ * claiming what the suite cannot check.
21
+ * @returns `true` when the job was dispatched at least once.
22
+ */
23
+ awaitJobDispatched?: (id: string) => Promise<boolean>;
15
24
  /** Optional cleanup hook (close DBs, release timers). */
16
25
  cleanup?: () => void;
17
26
  /**
@@ -23,6 +32,22 @@ interface ConformanceHost {
23
32
  createSocket?: () => unknown;
24
33
  /** The shard directory under test. */
25
34
  directory: ShardDirectory;
35
+ /**
36
+ * Terminally dispose this host instance, as opposed to {@link
37
+ * ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
38
+ * `cleanup`) use as a per-test reset rather than a true teardown — the DO's
39
+ * storage has no explicit close a test can drive, so `cleanup` there just
40
+ * disarms the pending alarm and drops socket references for the next run.
41
+ *
42
+ * Optional: only a host with a real terminal dispose implements it. Where
43
+ * it exists, the suite calls it once and then asserts every surface that
44
+ * documents a post-close behaviour (`ShardHost.alarms`,
45
+ * `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
46
+ * closed with a `"platform closed: …"` error — the same "report the gap
47
+ * instead of asserting a false close" pattern `scheduler`/`kv` already use
48
+ * for hosts that don't implement a surface at all.
49
+ */
50
+ disposeTerminally?: () => void;
26
51
  /**
27
52
  * The durable key-value store under test. Optional: a host that implements
28
53
  * only the reactive-engine half (`ShardHost`) has no KV surface to offer,
@@ -111,22 +136,5 @@ type VitestApi = {
111
136
  expect: typeof import("vitest").expect;
112
137
  it: typeof import("vitest").it;
113
138
  };
114
- /**
115
- * Define the host-contract conformance suite for the given factory.
116
- *
117
- * The suite asserts the provider-neutral behaviors that every Lunora host must
118
- * provide: single-writer serialization, durable transactions, local SQL,
119
- * durable alarms, socket accept/send/close, attachment round-trip across
120
- * recycle, deterministic shard placement, and durable scheduling.
121
- *
122
- * Usage:
123
- *
124
- * ```ts
125
- * import { describe, expect, it } from "vitest";
126
- * import { createReferenceHost, defineHostContractSuite } from "@lunora/platform/conformance";
127
- *
128
- * defineHostContractSuite("reference", createReferenceHost, { describe, expect, it });
129
- * ```
130
- */
131
139
  declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
132
140
  export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
@@ -1 +1 @@
1
- import{resolveShard as l}from"../packem_shared/resolveShard-uGhAKuTB.mjs";const m=(u,h,w)=>{const{describe:d,expect:t,it:o}=w;d(`host contract: ${u}`,()=>{const r=async()=>h(),n=e=>e.createSocket?.()??{};d("ShardHost",()=>{o("serializes mutations so no two closures interleave",async()=>{t.assertions(1);const e=await r(),a=[];await Promise.all([e.shard.runSerialized(async()=>{a.push("a-start"),await new Promise(c=>{setTimeout(c,10)}),a.push("a-end")}),e.shard.runSerialized(async()=>{a.push("b-start"),await new Promise(c=>{setTimeout(c,5)}),a.push("b-end")})]);const s=a.join("").includes("a-starta-end"),i=a.join("").includes("b-startb-end");t(s&&i).toBe(!0),e.cleanup?.()}),o("rolls back a transaction that throws",async()=>{t.assertions(2);const e=await r();await e.shard.transaction(async()=>{e.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),e.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(e.shard.transaction(async()=>{throw e.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const a=e.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(a).toHaveLength(0),e.cleanup?.()}),o("observes its own writes inside a transaction",async()=>{t.assertions(1);const e=await r(),a=await e.shard.transaction(async()=>(e.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),e.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),e.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(a).toBe("hello"),e.cleanup?.()}),o("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3);const e=await r();await e.shard.transaction(async()=>{e.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),e.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),e.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(e.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(e.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...e.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2),e.cleanup?.()}),o("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2);const e=await r(),a=Date.now()+50;if(await e.shard.alarms.set(a),t(await e.shard.alarms.get()).toBe(a),e.awaitAlarmFired===void 0){t(await e.shard.alarms.get()).toBe(a),e.cleanup?.();return}await e.awaitAlarmFired(a),t(await e.shard.alarms.get()).toBeNull(),e.cleanup?.()}),o("deletes a pending alarm",async()=>{t.assertions(1);const e=await r();await e.shard.alarms.set(Date.now()+1e4),await e.shard.alarms.delete(),t(await e.shard.alarms.get()).toBeNull(),e.cleanup?.()})}),d("SocketHost",()=>{o("accepts a socket and can send/close",async()=>{const e=await r(),a=e.socket.accept(n(e),{user:"ada"});t(e.socket.idFor(a)).toBeDefined(),a.send("hello"),t(e.socket.getSockets().map(s=>e.socket.idFor(s))).toContain(e.socket.idFor(a)),e.readFrames!==void 0&&t(e.readFrames(a)).toStrictEqual(["hello"]),t(()=>{a.close(1e3,"done")}).not.toThrow(),e.cleanup?.()}),o("round-trips an attachment on a live socket",async()=>{t.assertions(1);const e=await r(),a={roomId:"room-1",roles:["admin"]},s=e.socket.accept(n(e),a);t(s.deserializeAttachment()).toEqual(a),e.cleanup?.()}),o("round-trips attachments across a recycle",async()=>{t.assertions(1);const e=await r();if(e.simulateRecycle===void 0||e.restoreSocket===void 0){t(!0).toBe(!0),e.cleanup?.();return}const a={roomId:"room-1",roles:["admin"]},s=e.socket.accept(n(e),a);e.simulateRecycle();const i=e.restoreSocket(e.socket.idFor(s),a);t(i.deserializeAttachment()).toEqual(a),e.cleanup?.()}),o("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1);const e=await r(),a=e.socket.accept(n(e),{}),s=e.socket.idFor(a),i=e.socket.idFor(a);t(i).toBe(s),e.cleanup?.()}),o("keeps idFor stable across a recycle",async()=>{t.assertions(1);const e=await r();if(e.simulateRecycle===void 0||e.restoreSocket===void 0){t(!0).toBe(!0),e.cleanup?.();return}const a={roomId:"room-1",roles:["admin"]},s=e.socket.accept(n(e),a),i=e.socket.idFor(s);e.simulateRecycle();const c=e.restoreSocket(i,a);t(e.socket.idFor(c)).toBe(i),e.cleanup?.()}),o("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4);const e=await r(),a=e.socket.accept(n(e),{},["room-a"]),s=e.socket.accept(n(e),{},["room-b"]),i=e.socket.accept(n(e),{}),c=p=>e.socket.idFor(p);t(e.socket.getSockets("room-a").map(c)).toStrictEqual([c(a)]),t(e.socket.getSockets("room-b").map(c)).toStrictEqual([c(s)]),t(e.socket.getSockets("room-c").map(c)).toStrictEqual([]),t(e.socket.getSockets().map(c)).toContain(c(i)),e.cleanup?.()}),o("resolves a raw socket back to its handle",async()=>{t.assertions(2);const e=await r(),a=n(e),s=e.socket.accept(a,{}),i=e.socket.handleFor(a);t(i!==void 0&&e.socket.idFor(i)).toBe(e.socket.idFor(s)),t(e.socket.handleFor(n(e))).toBeUndefined(),e.cleanup?.()}),o("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1);const e=await r(),a=e.socket.accept(n(e),{}),{bufferedAmount:s}=a;t(s===void 0||typeof s=="number"&&s>=0).toBe(!0),e.cleanup?.()}),o("retags a live socket when the host declares mutable tags",async()=>{t.assertions(2);const e=await r();if(e.socket.setTag===void 0){t(e.socket.removeTag).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}const a=e.socket.accept(n(e),{});e.socket.setTag(a,"room-a"),t(e.socket.getSockets("room-a").map(s=>e.socket.idFor(s))).toStrictEqual([e.socket.idFor(a)]),e.socket.removeTag?.(a,"room-a"),t(e.socket.getSockets("room-a").map(s=>e.socket.idFor(s))).toStrictEqual([]),e.cleanup?.()})}),d("ShardDirectory",()=>{o("resolves shard keys deterministically",async()=>{t.assertions(1);const e=await r(),a=await l(e.directory,"tenant-42").fetch(new Request("http://localhost/")),s=await l(e.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(a.text()).resolves.toBe(await s.text()),e.cleanup?.()}),o("dispatches fetch to a resolved stub",async()=>{t.assertions(1);const e=await r(),a=await l(e.directory,"tenant-42").fetch(new Request("http://localhost/"));t(a).toBeInstanceOf(Response),e.cleanup?.()})}),d("SchedulerHost",()=>{o("schedules a job for a future timestamp",async()=>{t.assertions(2);const e=await r();if(e.scheduler===void 0){t(e.scheduler).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}const a=Date.now(),s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(s.scheduledFor).toBeGreaterThanOrEqual(a+50),t(s.id).toBeDefined(),e.cleanup?.()}),o("cancels a scheduled job",async()=>{t.assertions(1);const e=await r();if(e.scheduler===void 0){t(e.scheduler).toBeUndefined(),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),s=await e.scheduler.cancel(a.id);t(s).toBe(!0),e.cleanup?.()}),o("reports a second cancel of the same job as false",async()=>{t.assertions(2);const e=await r();if(e.scheduler===void 0){t(e.scheduler).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});t(await e.scheduler.cancel(a.id)).toBe(!0),t(await e.scheduler.cancel(a.id)).toBe(!1),e.cleanup?.()}),o("gives two identical schedules independently cancellable ids",async()=>{t.assertions(3);const e=await r();if(e.scheduler===void 0){t(e.scheduler).toBeUndefined(),t(!0).toBe(!0),t(!0).toBe(!0),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(s.id).not.toBe(a.id),t(await e.scheduler.cancel(a.id)).toBe(!0),t(await e.scheduler.cancel(s.id)).toBe(!0),e.cleanup?.()}),o("lists a pending job with a zero attempt count",async()=>{t.assertions(2);const e=await r();if(e.scheduler?.list===void 0){t(e.scheduler?.list).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),s=(await e.scheduler.list()).find(i=>i.id===a.id);t(s?.functionPath).toBe("tasks/remind"),t(s?.attempts).toBe(0),e.cleanup?.()}),o("keeps the pending and dead-letter listings disjoint",async()=>{t.assertions(2);const e=await r();if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){t(e.simulateDeadLetter).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(a.id);const s=await e.scheduler.list(),i=await e.scheduler.deadLetter.list();t(s.some(c=>c.id===a.id)).toBe(!1),t(i.some(c=>c.id===a.id)).toBe(!0),e.cleanup?.()}),o("returns a requeued job to the pending set with a fresh budget",async()=>{t.assertions(4);const e=await r();if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){t(e.simulateDeadLetter).toBeUndefined(),t(!0).toBe(!0),t(!0).toBe(!0),t(!0).toBe(!0),e.cleanup?.();return}const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(a.id),t(await e.scheduler.deadLetter.requeue(a.id)).toBe(!0);const s=(await e.scheduler.list()).find(c=>c.id===a.id);t(s).toBeDefined(),t(s?.attempts).toBe(0);const i=await e.scheduler.deadLetter.list();t(i.some(c=>c.id===a.id)).toBe(!1),e.cleanup?.()}),o("reports a requeue of an unparked job as false",async()=>{t.assertions(1);const e=await r();if(e.scheduler?.deadLetter===void 0){t(e.scheduler?.deadLetter).toBeUndefined(),e.cleanup?.();return}t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1),e.cleanup?.()})}),d("ShardKvStore",()=>{o("reads back a written value",async()=>{t.assertions(2);const e=await r();if(e.kv===void 0){t(e.kv).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}await e.kv.put("s:token-1",{userId:"ada"}),t(await e.kv.get("s:token-1")).toEqual({userId:"ada"}),t(await e.kv.get("s:missing")).toBeUndefined(),e.cleanup?.()}),o("deletes a key idempotently",async()=>{t.assertions(3);const e=await r();if(e.kv===void 0){t(e.kv).toBeUndefined(),t(!0).toBe(!0),t(!0).toBe(!0),e.cleanup?.();return}await e.kv.put("k",1),t(await e.kv.delete("k")).toBe(!0),t(await e.kv.delete("k")).toBe(!1),t(await e.kv.get("k")).toBeUndefined(),e.cleanup?.()}),o("enumerates exactly the keys under a prefix",async()=>{t.assertions(2);const e=await r();if(e.kv===void 0){t(e.kv).toBeUndefined(),t(!0).toBe(!0),e.cleanup?.();return}await e.kv.put("s:a",1),await e.kv.put("s:b",2),await e.kv.put("other",3);const a=await e.kv.list({prefix:"s:"}),s=await e.kv.list();t([...a.keys()].toSorted((i,c)=>i.localeCompare(c))).toStrictEqual(["s:a","s:b"]),t(s.size).toBe(3),e.cleanup?.()})})})};export{m as defineHostContractSuite};
1
+ import{resolveShard as k}from"../packem_shared/resolveShard-BzKOUEO4.mjs";const f=/platform closed/u,b=(d,v,S)=>{const{describe:u,expect:t,it:c}=S;u(`host contract: ${d}`,()=>{const p=async()=>v(),l=a=>a.createSocket?.()??{},o=async a=>{const e=await p();try{await a(e)}finally{e.cleanup?.()}};u("ShardHost",()=>{c("serializes mutations so no two closures interleave",async()=>{t.assertions(1),await o(async a=>{const e=[];await Promise.all([a.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(i=>{setTimeout(i,10)}),e.push("a-end")}),a.shard.runSerialized(async()=>{e.push("b-start"),await new Promise(i=>{setTimeout(i,5)}),e.push("b-end")})]);const s=e.join("").includes("a-starta-end"),r=e.join("").includes("b-startb-end");t(s&&r).toBe(!0)})}),c("rolls back a transaction that throws",async()=>{t.assertions(2),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=a.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(e).toHaveLength(0)})}),c("rejects with the value the closure threw, and stays usable",async()=>{t.assertions(3),await o(async a=>{const e=Object.assign(new Error("closure failed"),{code:"NOT_FOUND",status:404});await t(a.shard.transaction(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(async()=>a.shard.transaction(async()=>"still here"))).resolves.toBe("still here")})}),c("observes its own writes inside a transaction",async()=>{t.assertions(1),await o(async a=>{const e=await a.shard.transaction(async()=>(a.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),a.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),a.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(e).toBe("hello")})}),c("keeps overlapping transactions atomic",async()=>{t.assertions(2),await o(async a=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(i=>{setTimeout(i,20)}),a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),s=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,s]);const r=a.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();t(r.map(i=>i.id)).toStrictEqual(["A","B","C"]),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),c("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(a.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),c("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2),await o(async a=>{const e=Date.now()+50;if(await a.shard.alarms.set(e),t(await a.shard.alarms.get()).toBe(e),a.awaitAlarmFired===void 0){t(await a.shard.alarms.get()).toBe(e);return}await a.awaitAlarmFired(e),t(await a.shard.alarms.get()).toBeNull()})}),c("deletes a pending alarm",async()=>{t.assertions(1),await o(async a=>{await a.shard.alarms.set(Date.now()+1e4),await a.shard.alarms.delete(),t(await a.shard.alarms.get()).toBeNull()})})}),u("SocketHost",()=>{c("accepts a socket and can send/close",async()=>{await o(async a=>{const e=a.socket.accept(l(a),{user:"ada"});t(a.socket.idFor(e)).toBeDefined(),e.send("hello"),t(a.socket.getSockets().map(s=>a.socket.idFor(s))).toContain(a.socket.idFor(e)),a.readFrames!==void 0&&t(a.readFrames(e)).toStrictEqual(["hello"]),t(()=>{e.close(1e3,"done")}).not.toThrow()})}),c("round-trips an attachment on a live socket",async()=>{t.assertions(1),await o(async a=>{const e={roomId:"room-1",roles:["admin"]},s=a.socket.accept(l(a),e);t(s.deserializeAttachment()).toEqual(e)})}),c("round-trips attachments across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s);e.simulateRecycle();const i=e.restoreSocket(e.socket.idFor(r),s);t(i.deserializeAttachment()).toEqual(s)})}),c("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),s=a.socket.idFor(e),r=a.socket.idFor(e);t(r).toBe(s)})}),c("keeps idFor stable across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s),i=e.socket.idFor(r);e.simulateRecycle();const n=e.restoreSocket(i,s);t(e.socket.idFor(n)).toBe(i)})}),c("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4),await o(async a=>{const e=a.socket.accept(l(a),{},["room-a"]),s=a.socket.accept(l(a),{},["room-b"]),r=a.socket.accept(l(a),{}),i=n=>a.socket.idFor(n);t(a.socket.getSockets("room-a").map(i)).toStrictEqual([i(e)]),t(a.socket.getSockets("room-b").map(i)).toStrictEqual([i(s)]),t(a.socket.getSockets("room-c").map(i)).toStrictEqual([]),t(a.socket.getSockets().map(i)).toContain(i(r))})}),c("accepts the portable budget of nine caller tags",async()=>{t.assertions(9);const a=await p(),e=Array.from({length:9},(i,n)=>`tag-${String(n)}`),s=a.socket.accept(l(a),{},e),r=i=>a.socket.idFor(i);for(const i of e)t(a.socket.getSockets(i).map(r)).toStrictEqual([r(s)]);a.cleanup?.()}),c("resolves a raw socket back to its handle",async()=>{t.assertions(2),await o(async a=>{const e=l(a),s=a.socket.accept(e,{}),r=a.socket.handleFor(e);t(r!==void 0&&a.socket.idFor(r)).toBe(a.socket.idFor(s)),t(a.socket.handleFor(l(a))).toBeUndefined()})}),c("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),{bufferedAmount:s}=e;t(s===void 0||typeof s=="number"&&s>=0).toBe(!0)})}),c("retags a live socket when the host declares mutable tags",async a=>{await o(async e=>{if(e.socket.setTag===void 0){a.skip(`${d} does not implement mutable socket tags (setTag)`);return}t.assertions(2);const s=e.socket.accept(l(e),{});e.socket.setTag(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([e.socket.idFor(s)]),e.socket.removeTag?.(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([])})})}),u("ShardDirectory",()=>{c("resolves shard keys deterministically",async()=>{t.assertions(1),await o(async a=>{const e=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/")),s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(e.text()).resolves.toBe(await s.text())})}),c("dispatches fetch to a resolved stub",async()=>{t.assertions(1),await o(async a=>{const s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));t(s).toBeInstanceOf(Response)})})}),u("SchedulerHost",()=>{c("schedules a job for a future timestamp",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(2);const s=Date.now(),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(r.scheduledFor).toBeGreaterThanOrEqual(s+50),t(r.id).toBeDefined()})}),c("dispatches a scheduled job at least once",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}const s=e.scheduler.deadLetter!==void 0;if(!s&&e.awaitJobDispatched===void 0){a.skip(`${d} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(s&&t(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const r=e.scheduler.list!==void 0;t.assertions(1+(s?1:0)+(r?1:0));const i=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await t(e.awaitJobDispatched(i.id)).resolves.toBe(!0),r){const n=await e.scheduler.list?.();t(n?.some(w=>w.id===i.id)).toBe(!1)}})}),c("cancels a scheduled job",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(1);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),r=await e.scheduler.cancel(s.id);t(r).toBe(!0)})}),c("reports a second cancel of the same job as false",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(s.id)).toBe(!1)})}),c("gives two identical schedules independently cancellable ids",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(3);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(r.id).not.toBe(s.id),t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(r.id)).toBe(!0)})}),c("lists a pending job with a zero attempt count",async a=>{await o(async e=>{if(e.scheduler?.list===void 0){a.skip(`${d} does not implement SchedulerHost.list`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),i=(await e.scheduler.list()).find(n=>n.id===s.id);t(i?.functionPath).toBe("tasks/remind"),t(i?.attempts).toBe(0)})}),c("keeps the pending and dead-letter listings disjoint",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id);const r=await e.scheduler.list(),i=await e.scheduler.deadLetter.list();t(r.some(n=>n.id===s.id)).toBe(!1),t(i.some(n=>n.id===s.id)).toBe(!0)})}),c("returns a requeued job to the pending set with a fresh budget",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(4);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id),t(await e.scheduler.deadLetter.requeue(s.id)).toBe(!0);const i=(await e.scheduler.list()).find(w=>w.id===s.id);t(i).toBeDefined(),t(i?.attempts).toBe(0);const n=await e.scheduler.deadLetter.list();t(n.some(w=>w.id===s.id)).toBe(!1)})}),c("reports a requeue of an unparked job as false",async a=>{await o(async e=>{if(e.scheduler?.deadLetter===void 0){a.skip(`${d} does not implement scheduler.deadLetter`);return}t.assertions(1),t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})})}),u("ShardKvStore",()=>{c("reads back a written value",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:token-1",{userId:"ada"}),t(await e.kv.get("s:token-1")).toEqual({userId:"ada"}),t(await e.kv.get("s:missing")).toBeUndefined()})}),c("deletes a key idempotently",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(3),await e.kv.put("k",1),t(await e.kv.delete("k")).toBe(!0),t(await e.kv.delete("k")).toBe(!1),t(await e.kv.get("k")).toBeUndefined()})}),c("enumerates exactly the keys under a prefix",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:a",1),await e.kv.put("s:b",2),await e.kv.put("other",3);const s=await e.kv.list({prefix:"s:"}),r=await e.kv.list();t([...s.keys()].toSorted((i,n)=>i.localeCompare(n))).toStrictEqual(["s:a","s:b"]),t(r.size).toBe(3)})})}),u("post-dispose",()=>{c("fails closed on every documented surface once the host is terminally disposed",async a=>{const e=await p();if(e.disposeTerminally===void 0){a.skip(`${d} has no terminal dispose the suite can drive from inside a test`);return}const{removeTag:s,setTag:r}=e.socket,{scheduler:i}=e,n=e.socket.accept(l(e),{}),w=l(e);e.disposeTerminally();const E=async m=>{await t(async()=>{await m()}).rejects.toThrow(f)},y=[()=>e.shard.alarms.set(Date.now()+1e3),()=>e.shard.alarms.delete(),()=>e.socket.accept(w,{}),...r===void 0?[]:[()=>{r(n,"room-a")}],...s===void 0?[]:[()=>{s(n,"room-a")}],...i===void 0?[]:[()=>i.schedule("tasks/remind",{},{delayMs:10})]];t.assertions(y.length);for(const m of y)await E(m)})})})};export{b as defineHostContractSuite};
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardAsyncSqlExec, type f as ShardDirectory, type g as ShardHost, type h as ShardJurisdiction, type i as ShardKvListOptions, type j as ShardKvStore, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-Cq5uVbiH.mjs";
1
+ export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-dVPE86WP.mjs";
2
2
  /**
3
3
  * The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
4
4
  * the framework mount seams rely on — `waitUntil` for fire-and-forget work that
@@ -22,6 +22,13 @@ export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as Sc
22
22
  * fall back to {@link NOOP_EXECUTION_CONTEXT}.
23
23
  */
24
24
  interface ExecutionContextLike {
25
+ /**
26
+ * Present only when Cloudflare Access authenticated the request against a
27
+ * policy attached to the **Worker** (rather than to a hostname). `undefined`
28
+ * on every unauthenticated request, so its presence is itself the "Access
29
+ * authorized this caller" signal — see {@link AccessContextLike}.
30
+ */
31
+ access?: AccessContextLike;
25
32
  cache?: {
26
33
  purge: (options: {
27
34
  purgeEverything?: boolean;
@@ -31,6 +38,48 @@ interface ExecutionContextLike {
31
38
  passThroughOnException?: () => void;
32
39
  waitUntil?: (promise: Promise<unknown>) => void;
33
40
  }
41
+ /**
42
+ * The identity Cloudflare Access attaches to a Worker-protected request.
43
+ *
44
+ * Shape follows the Access application-token payload: `sub` is the stable per-user
45
+ * id, `email` the verified address, `common_name` the service-token name (machine
46
+ * callers, whose `sub` is empty), and `exp` the credential expiry in epoch
47
+ * **seconds**. Group membership is whatever the Access policy emits — a list of
48
+ * names, or of `{ id, name }` objects — hence `unknown`; normalize before use.
49
+ *
50
+ * Cloudflare may add further fields, so the index signature keeps them rather
51
+ * than dropping them: this is a view of a payload we do not own.
52
+ */
53
+ interface AccessIdentityLike {
54
+ [claim: string]: unknown;
55
+ /** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
56
+ common_name?: string;
57
+ /** Verified user email. Present for interactive (SSO) callers. */
58
+ email?: string;
59
+ /** Credential expiry, epoch **seconds**. */
60
+ exp?: number;
61
+ /** IdP group membership — names or `{ id, name }` objects, depending on the policy. */
62
+ groups?: unknown;
63
+ /** Display name from the identity provider, when it emits one. */
64
+ name?: string;
65
+ /** Stable per-user id, and what consumers key a user on. Empty for service tokens. */
66
+ sub?: string;
67
+ /** Cloudflare's per-user UUID. Carried through, but deliberately not used as an id — only this path emits it, so keying on it would not match the JWT path. */
68
+ user_uuid?: string;
69
+ }
70
+ /**
71
+ * The `ctx.access` facade Cloudflare exposes on a Worker protected by Access.
72
+ *
73
+ * Reading the identity from here is preferable to verifying the
74
+ * `Cf-Access-Jwt-Assertion` header: the platform has already authenticated the
75
+ * caller, so there is no JWKS fetch, no audience check to get wrong, and nothing
76
+ * a request can forge — the field simply does not exist unless Access authorized
77
+ * the call. The header path remains the fallback for hostname-scoped Access
78
+ * applications, which do not populate this.
79
+ */
80
+ interface AccessContextLike {
81
+ getIdentity: () => AccessIdentityLike | null | undefined | Promise<AccessIdentityLike | null | undefined>;
82
+ }
34
83
  /**
35
84
  * No-op `ExecutionContext` used when the host runtime didn't supply one (a
36
85
  * non-Cloudflare preview, or a unit test), so the worker's `fetch` always
@@ -65,6 +114,35 @@ interface QueueMessageLike<Body = unknown> {
65
114
  }) => void;
66
115
  readonly timestamp: Date;
67
116
  }
117
+ /** Options accepted by {@link HttpCacheLike.match} and {@link HttpCacheLike.delete}. */
118
+ interface HttpCacheQueryOptions {
119
+ /** Match a non-`GET` request against a stored `GET` entry. */
120
+ ignoreMethod?: boolean;
121
+ }
122
+ /**
123
+ * Minimal projection of one Web Cache API cache — the store a host puts in front
124
+ * of the app, reached on Cloudflare as `caches.default` (the colo cache).
125
+ *
126
+ * Only the three calls Lunora makes are declared, so a host that has a cache but
127
+ * not the whole `Cache` interface still satisfies it, and a unit test can pass a
128
+ * plain object double. This is a **host** primitive, not a binding: it is reached
129
+ * through a runtime global rather than `env`, and a target without one leaves it
130
+ * `undefined` rather than shipping a fake — see `httpCache` in
131
+ * `PlatformCapabilities`.
132
+ *
133
+ * The stored entry is keyed by the request, so a caller that needs `Vary`
134
+ * semantics must fold the varying header values into the key itself: Cloudflare's
135
+ * cache honours `Vary` for `Accept-Encoding` only, and a projection cannot make
136
+ * that portable.
137
+ */
138
+ interface HttpCacheLike {
139
+ /** Evict the entry stored under `request`. Resolves `true` when something was removed. */
140
+ delete: (request: Request | string, options?: HttpCacheQueryOptions) => Promise<boolean>;
141
+ /** The stored response for `request`, or `undefined` on a miss. */
142
+ match: (request: Request | string, options?: HttpCacheQueryOptions) => Promise<Response | undefined>;
143
+ /** Store `response` under `request`. Rejects for a `206`, a `Vary: *`, or a `Set-Cookie`-bearing response. */
144
+ put: (request: Request | string, response: Response) => Promise<void>;
145
+ }
68
146
  /** A single vector match. */
69
147
  interface VectorMatchLike {
70
148
  id: string;
@@ -312,16 +390,18 @@ interface R2BucketLike {
312
390
  delimiter?: string;
313
391
  limit?: number;
314
392
  prefix?: string;
393
+ startAfter?: string;
315
394
  }) => Promise<{
316
395
  cursor?: string;
317
396
  objects: R2ObjectLike[];
318
397
  truncated?: boolean;
319
398
  }>;
320
- put: (key: string, body: ReadableStream | ArrayBuffer | Blob | string | null, options?: {
399
+ put: (key: string, body: ReadableStream | ArrayBuffer | ArrayBufferView | Blob | string | null, options?: {
321
400
  customMetadata?: Record<string, string>;
322
401
  httpMetadata?: {
323
402
  contentType?: string;
324
403
  };
404
+ sha256?: ArrayBuffer | string;
325
405
  }) => Promise<R2ObjectLike>;
326
406
  /** Resume an in-progress multipart upload by id (R2 `resumeMultipartUpload`). Optional; see {@link Storage.resumeMultipartUpload}. */
327
407
  resumeMultipartUpload?: (key: string, uploadId: string) => R2MultipartUploadLike;
@@ -417,15 +497,15 @@ interface MessageSendRequestLike<Body = unknown> {
417
497
  delaySeconds?: number;
418
498
  }
419
499
  /**
420
- * Minimal structural projection of workers-types' `Queue&lt;Body>` (the producer
500
+ * Minimal structural projection of workers-types' `Queue<Body>` (the producer
421
501
  * binding). The real binding's `send`/`sendBatch` resolve to a metadata object;
422
- * we widen the return to `Promise&lt;unknown>` so a plain-object fake satisfies it.
502
+ * we widen the return to `Promise<unknown>` so a plain-object fake satisfies it.
423
503
  */
424
504
  interface QueueBindingLike<Body = unknown> {
425
505
  send: (message: Body, options?: QueueSendOptions) => Promise<unknown>;
426
506
  sendBatch: (messages: Iterable<MessageSendRequestLike<Body>>, options?: QueueSendBatchOptions) => Promise<unknown>;
427
507
  }
428
- /** Structural mirror of workers-types' `Message&lt;Body>` (one delivered message). */
508
+ /** Structural mirror of workers-types' `Message<Body>` (one delivered message). */
429
509
  interface MessageLike<Body = unknown> {
430
510
  /** Acknowledge this message so it is not redelivered. */
431
511
  ack: () => void;
@@ -436,7 +516,7 @@ interface MessageLike<Body = unknown> {
436
516
  retry: (options?: QueueRetryOptions) => void;
437
517
  readonly timestamp: Date;
438
518
  }
439
- /** Structural mirror of workers-types' `MessageBatch&lt;Body>` handed to a consumer. */
519
+ /** Structural mirror of workers-types' `MessageBatch<Body>` handed to a consumer. */
440
520
  interface MessageBatchLike<Body = unknown> {
441
521
  /** Acknowledge every message in the batch. */
442
522
  ackAll: () => void;
@@ -481,22 +561,131 @@ interface PlatformCapabilities {
481
561
  analytics?: Capability;
482
562
  /** Browser rendering / headless browser. */
483
563
  browser?: Capability;
484
- /** Container execution (Cloudflare Containers / Fargate). */
564
+ /**
565
+ * `.commitOrdered()` tables — the `_commitSeq` system field: a per-shard
566
+ * integer allocated once per mutation and strictly increasing in commit
567
+ * order.
568
+ *
569
+ * Listed as a capability rather than assumed, because the ordering
570
+ * guarantee is not the engine's to give. It rests on two things the HOST
571
+ * provides: an atomic write boundary the counter bump shares with the
572
+ * rows it stamps, and serialized execution so two mutations cannot
573
+ * interleave their allocations. A host that offers neither can still
574
+ * create the counter and hand out increasing numbers — they just would
575
+ * not order commits, which is the whole contract.
576
+ */
577
+ commitOrderedTables?: Capability;
578
+ /**
579
+ * Container execution (Cloudflare Containers / Fargate), including
580
+ * `ctx.containers.<name>.exec`. Deliberately one rating rather than two:
581
+ * `exec` is a method on the accessor this key already gates, not a
582
+ * separate app-imported surface, so there is no usage signal codegen
583
+ * could gate it on independently and nothing that could act on a second
584
+ * rating. A host that can reach a container but cannot carry a command
585
+ * result back should say so in this note.
586
+ */
485
587
  containers?: Capability;
486
588
  /** Cross-shard fan-out queries. */
487
589
  crossShardFanout?: Capability;
590
+ /**
591
+ * Durable streams: a `.stream()` run whose chunks are persisted and
592
+ * whose producer outlives the socket that opened it, so a reconnecting
593
+ * or second client resumes the same transcript.
594
+ */
595
+ durableStreams?: Capability;
488
596
  /** Global (replicated) tables backed by a SQL store. */
489
597
  globalTables?: Capability;
598
+ /**
599
+ * A shared HTTP cache in front of the app that the runtime can READ AND
600
+ * WRITE — the Web Cache API (`caches.default` on Cloudflare), projected
601
+ * as `HttpCacheLike`.
602
+ *
603
+ * Rated separately from the app merely emitting `Cache-Control`, because
604
+ * only this half needs a host primitive. Emitting the header is portable
605
+ * by construction: any host that returns an HTTP response can do it, and
606
+ * browsers and downstream CDNs honour it wherever the app runs. What is
607
+ * not portable is a store the Worker itself can `match`/`put` against,
608
+ * which is why `@lunora/runtime`'s REST edge cache degrades to
609
+ * headers-only on a target rated `unsupported` rather than failing.
610
+ */
611
+ httpCache?: Capability;
490
612
  /** BYO database via connection pooling (Hyperdrive / RDS Proxy). */
491
613
  hyperdrive?: Capability;
614
+ /**
615
+ * An identity-aware proxy in front of the app that authenticates the
616
+ * caller before the request reaches it, and hands the runtime a verified
617
+ * identity **out-of-band** — on the execution context rather than on the
618
+ * request (Cloudflare Access attached to a Worker; IAP; an ALB OIDC
619
+ * action).
620
+ *
621
+ * Rated separately from the header-stamping form of the same product
622
+ * because only this one needs a host primitive. An identity-aware proxy
623
+ * that merely adds a signed header is portable by construction: any host
624
+ * that receives an HTTP request can verify it, which is why
625
+ * `@lunora/cloudflare-access` still works on a target rated
626
+ * `unsupported` here (it falls back to the `Cf-Access-Jwt-Assertion`
627
+ * JWT). What is not portable is the identity arriving beside the
628
+ * request, which is why `ExecutionContextLike.access` is a projection a
629
+ * host either populates or does not.
630
+ */
631
+ identityProxy?: Capability;
632
+ /** Image transforms (resize/format/optimize) via an Images binding. */
633
+ images?: Capability;
492
634
  /** Key-value storage (KV / Redis / DynamoDB). */
493
635
  keyValueStore?: Capability;
494
636
  /** Local SQL execution inside a shard. */
495
637
  localSql?: Capability;
496
638
  /** Email sending (Resend / SES / etc). */
497
639
  mail?: Capability;
498
- /** Object storage (R2 / S3 / MinIO). */
640
+ /**
641
+ * `.memory()` tables — the ephemeral tier: rows cleared on every shard
642
+ * cold start, never written to the CDC changelog, refilled by
643
+ * `onShardInit`.
644
+ *
645
+ * The rating answers "does a memory table avoid durable storage on this
646
+ * host", NOT "does it work". The lifetime semantics are the engine's and
647
+ * hold everywhere; whether the rows actually stay out of the durable
648
+ * store depends on the host offering a second, memory-backed SQL handle,
649
+ * which is a per-target fact.
650
+ */
651
+ memoryTables?: Capability;
652
+ /**
653
+ * Object storage (R2 / S3 / MinIO).
654
+ *
655
+ * `ctx.storage.deleteAfterCommit(key)` rides on this rating and gets no
656
+ * key of its own: it needs no host primitive beyond the bucket. The
657
+ * post-commit flush uses `ShardHost.waitUntil` where the host has one and
658
+ * is awaited inline where it does not, so a host that can serve
659
+ * `objectStorage` serves the deferral at the same level.
660
+ */
499
661
  objectStorage?: Capability;
662
+ /**
663
+ * Snapshot backups kept in object storage rather than on the machine
664
+ * that took them — `lunora backup create|list|restore --bucket`, and
665
+ * the platform's own `backupCron`. Distinct from
666
+ * `objectStorage` above because it needs three things a
667
+ * bucket alone does not imply: an admin-gated read of one object
668
+ * (`GET /_lunora/admin/storage/object`), a checksum-verified write, and
669
+ * a scheduler to run the unattended half.
670
+ */
671
+ objectStorageBackups?: Capability;
672
+ /**
673
+ * The CDC changelog's cold tier: rows a retention sweep is about to
674
+ * destroy are written to an object-storage bucket first
675
+ * (`LUNORA_CDC_ARCHIVE`), and a consumer whose cursor has fallen below
676
+ * the retained window is served from there instead of being told to
677
+ * re-seed.
678
+ *
679
+ * Distinct from `objectStorage` because it needs the bucket to do one
680
+ * thing a plain byte store need not: resume a key-ordered listing from a
681
+ * position (`list({ startAfter })`). Without it the read-back re-lists
682
+ * the prefix from the front every time and stops finding the range it
683
+ * needs once enough segments precede the cursor — which fails as a
684
+ * refusal rather than a gap, but fails permanently and silently, so a
685
+ * host that cannot seek should say `unsupported` here rather than
686
+ * inherit `objectStorage`'s rating.
687
+ */
688
+ objectStorageCdcArchive?: Capability;
500
689
  /** Pipelines / streaming data. */
501
690
  pipelines?: Capability;
502
691
  /** Queue-backed workpools. */
@@ -505,10 +694,26 @@ interface PlatformCapabilities {
505
694
  scheduler?: Capability;
506
695
  /** Secrets management. */
507
696
  secrets?: Capability;
697
+ /**
698
+ * `onQueryChange` reactors — server-side reactivity: a subscriber that is
699
+ * not a socket, woken after a write flush when a watched read's result
700
+ * changed.
701
+ *
702
+ * Host-dependent because the whole mechanism rests on the host being able
703
+ * to run work AFTER a write commits, on the same shard, without a client
704
+ * connection to hang it off — and on that work being serialized against
705
+ * further writes so a reactor's own writes cascade deterministically
706
+ * rather than interleaving.
707
+ */
708
+ serverReactors?: Capability;
508
709
  /** Alarms / scheduled wakeup inside a shard. */
509
710
  shardAlarms?: Capability;
510
711
  /** Durable Object-style sharded state. */
511
712
  shardedState?: Capability;
713
+ /** Geographic placement of a shard (`ShardPlacement.locationHint`). */
714
+ shardPlacement?: Capability;
715
+ /** Region-local read replicas of a shard, for one-shot queries. */
716
+ shardReadReplicas?: Capability;
512
717
  /** Vector database (Vectorize / pgvector / Pinecone). */
513
718
  vectorStore?: Capability;
514
719
  /** Hibernated WebSocket subscriptions. */
@@ -535,29 +740,72 @@ declare const CLOUDFLARE_CAPABILITIES: PlatformCapabilities;
535
740
  * The Node capability matrix — `@lunora/platform-node`'s honest self-rating
536
741
  * (plan 234).
537
742
  *
538
- * `@lunora/platform-node` is a spike: a `ShardHost`/`SocketHost`/
539
- * `ShardDirectory`/`ShardKvStore`/`SchedulerHost` implementation over
540
- * `better-sqlite3` and an in-process registry, built to run the conformance
541
- * TCK against a second host and discover what the contracts under-specify.
542
- * It is a single Node process with no distributed placement, no host-level
543
- * scheduler to re-arm timers after a restart, and no bindings at all for the
544
- * Cloudflare-specific products (R2, Vectorize, Workers AI, Queues,
545
- * Workflows, Containers, Browser Rendering, Analytics Engine, Secrets Store,
546
- * Hyperdrive) most `ctx.*` surfaces are built on. Every one of those is
547
- * rated `"unsupported"` here rather than left undeclared — see
548
- * `gateAgainstMatrix` in `@lunora/codegen`, whose fail-closed gate (plan
549
- * 229) treats an undeclared feature as unsupported anyway, but under a
550
- * different diagnostic name than an honest, explicit rating.
743
+ * `@lunora/platform-node` implements every contract in this package
744
+ * (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
745
+ * `SchedulerHost`) over `better-sqlite3` and an in-process registry, plus the
746
+ * `.global()` table backend via `@lunora/sql-store`. It began as a spike to run
747
+ * the conformance TCK against a second host; the durability gaps that spike
748
+ * surfaced alarms and scheduler jobs that were persisted but never re-armed,
749
+ * socket attachments that lived only in memory — are closed, and each is now
750
+ * pinned by a restart test rather than only by a simulated recycle.
551
751
  *
552
- * Two features are rated `"emulated"` rather than `"native"` even though
553
- * this package fully implements their contract, because "native" would
554
- * overstate what a bare Node process provides on its own: `keyValueStore` is
555
- * a SQL table wearing a KV-shaped API, not a dedicated KV product, and
556
- * `websocketHibernation` never actually evicts a socket to save memory it
557
- * only proves the attachment/tag durability half of the contract, not real
558
- * hibernation. Both ratings, and the `"unsupported"` ones for `scheduler`
559
- * durability and `globalTables`, are argued in detail in
560
- * `plans/234-node-host-findings.md`.
752
+ * `scheduler` and `shardAlarms` were rated `"unsupported"` under plan 267, on
753
+ * the grounds that the host stored and timed both while its timer body only
754
+ * cleared bookkeeping nothing dispatched the scheduled function or woke the
755
+ * alarm. That rating was correct for the code it described, and the code is
756
+ * what changed: both now dispatch (through `onDispatch` / `onAlarm`) and both
757
+ * re-arm from their durable rows on construction, so `"emulated"` built on
758
+ * lower-level primitives and *working* is now the honest reading.
759
+ *
760
+ * What remains genuinely absent is everything a single Node process cannot
761
+ * distribute: placement across nodes, failover, and most Cloudflare-specific
762
+ * product bindings (Vectorize, Workers AI, Containers, Browser Rendering,
763
+ * Analytics Engine, Secrets Store, Hyperdrive). Workflows, object storage and
764
+ * queues are the three that CAN be emulated locally — `defineWorkflow` handlers
765
+ * compile onto the `@visulima/workflow` engine, R2 becomes a filesystem bucket,
766
+ * and Queues becomes a durable table with the same batch/ack/retry/dead-letter
767
+ * semantics — so those three are rated `"emulated"`; the rest of the Cloudflare
768
+ * products most `ctx.*` surfaces are built on are rated `"unsupported"` here
769
+ * rather than left undeclared — see `gateAgainstMatrix` in `@lunora/codegen`,
770
+ * whose fail-closed gate (plan 229) treats an undeclared feature as unsupported
771
+ * anyway, but under a different diagnostic name than an honest, explicit rating.
772
+ *
773
+ * Almost nothing here is rated `"native"`, and that is the matrix's own
774
+ * definition doing its job rather than a hedge: `native` means the platform
775
+ * itself provides the feature, and a bare Node process provides essentially
776
+ * none of them — Lunora builds alarms out of `setTimeout` plus a durable row,
777
+ * a KV store out of a SQL table, and `.global()` tables out of a second SQLite
778
+ * file. `localSql` is the exception, because SQLite genuinely is the platform
779
+ * primitive there. The ratings say who does the work; the notes say how well.
780
+ * Both are argued in detail in `plans/234-node-host-findings.md`.
561
781
  */
562
782
  declare const NODE_CAPABILITIES: PlatformCapabilities;
563
- export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike, type ExecutionContextLike, type KVNamespaceLike, type KvGetOptions, type KvListKey, type KvNamespaceListResult, type KvNamespacePutOptions, type KvValue, type KvValueType, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, NODE_CAPABILITIES, NOOP_EXECUTION_CONTEXT, type PlatformCapabilities, type QueueBindingLike, type QueueContentType, type QueueMessageLike, type QueueRetryOptions, type QueueSendBatchOptions, type QueueSendOptions, type QueueSendOptionsLike, type QueueSendRequestLike, type R2BucketLike, type R2MultipartUploadLike, type R2ObjectBodyLike, type R2ObjectLike, type R2RangeLike, type R2UploadedPartLike, type VectorMatchLike, type VectorMetric, type VectorRecordLike, type VectorizeDeleteMutation, type VectorizeIndexDetails, type VectorizeIndexLike, type VectorizeMatch, type VectorizeMatches, type VectorizeQueryOptions, type VectorizeUpsertMutation, type VectorizeVector };
783
+ export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike,
784
+ /**
785
+ * `@lunora/platform` — provider-neutral host contracts for Lunora.
786
+ *
787
+ * This package defines the structural interfaces that separate the Lunora
788
+ * engine from any specific host (Cloudflare Workers, AWS, Rivet, Node, etc.).
789
+ * It contains **types and capability metadata only** — near-zero runtime code.
790
+ *
791
+ * The contracts fall into four groups:
792
+ *
793
+ * 1. **Shard host** (`ShardHost`) — single-writer execution, transactions,
794
+ * local SQL, alarms, and background continuation per shard key.
795
+ * 2. **Socket host** (`SocketHost`) — hibernated WebSocket subscriptions with
796
+ * durable attachments and tagged fan-out.
797
+ * 3. **Shard directory** (`ShardDirectory`) — deterministic placement and RPC
798
+ * dispatch from shard keys to stubs.
799
+ * 4. **Scheduler host** (`SchedulerHost`) — durable delayed jobs, cron, and
800
+ * at-least-once dispatch.
801
+ *
802
+ * Plus canonical binding projections (`KVNamespaceLike`, `R2BucketLike`,
803
+ * `QueueBindingLike`, `D1DatabaseLike`, `VectorizeIndexLike`, …) and the
804
+ * `PlatformCapabilities` matrix that codegen uses to tailor emitted types per
805
+ * target.
806
+ *
807
+ * This package is **zero-dependency** and safe on every runtime (browser,
808
+ * workerd, Node). It is intended to be the leaf dependency every other
809
+ * `@lunora/*` package can import without creating cycles.
810
+ */
811
+ type ExecutionContextLike, type HttpCacheLike, type HttpCacheQueryOptions, type KVNamespaceLike, type KvGetOptions, type KvListKey, type KvNamespaceListResult, type KvNamespacePutOptions, type KvValue, type KvValueType, type MessageBatchLike, type MessageLike, type MessageSendRequestLike, NODE_CAPABILITIES, NOOP_EXECUTION_CONTEXT, type PlatformCapabilities, type QueueBindingLike, type QueueContentType, type QueueMessageLike, type QueueRetryOptions, type QueueSendBatchOptions, type QueueSendOptions, type QueueSendOptionsLike, type QueueSendRequestLike, type R2BucketLike, type R2MultipartUploadLike, type R2ObjectBodyLike, type R2ObjectLike, type R2RangeLike, type R2UploadedPartLike, type VectorMatchLike, type VectorMetric, type VectorRecordLike, type VectorizeDeleteMutation, type VectorizeIndexDetails, type VectorizeIndexLike, type VectorizeMatch, type VectorizeMatches, type VectorizeQueryOptions, type VectorizeUpsertMutation, type VectorizeVector };