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

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-GABARI_Q.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-GABARI_Q.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-fs0Q8_VA.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 { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-GABARI_Q.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
  /**
@@ -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 { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-GABARI_Q.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
  /**
@@ -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 h}from"../packem_shared/resolveShard-uGhAKuTB.mjs";const E=(n,w,k)=>{const{describe:u,expect:t,it:i}=k;u(`host contract: ${n}`,()=>{const m=async()=>w(),l=s=>s.createSocket?.()??{},c=async s=>{const e=await m();try{await s(e)}finally{e.cleanup?.()}};u("ShardHost",()=>{i("serializes mutations so no two closures interleave",async()=>{t.assertions(1),await c(async s=>{const e=[];await Promise.all([s.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(r=>{setTimeout(r,10)}),e.push("a-end")}),s.shard.runSerialized(async()=>{e.push("b-start"),await new Promise(r=>{setTimeout(r,5)}),e.push("b-end")})]);const a=e.join("").includes("a-starta-end"),o=e.join("").includes("b-startb-end");t(a&&o).toBe(!0)})}),i("rolls back a transaction that throws",async()=>{t.assertions(2),await c(async s=>{await s.shard.transaction(async()=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),s.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(s.shard.transaction(async()=>{throw s.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=s.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(e).toHaveLength(0)})}),i("observes its own writes inside a transaction",async()=>{t.assertions(1),await c(async s=>{const e=await s.shard.transaction(async()=>(s.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),s.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),s.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(e).toBe("hello")})}),i("keeps overlapping transactions atomic",async()=>{t.assertions(2),await c(async s=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=s.shard.transaction(async()=>{s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(r=>{setTimeout(r,20)}),s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),a=s.shard.transaction(async()=>{s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,a]);const o=s.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();t(o.map(r=>r.id)).toStrictEqual(["A","B","C"]),await t(s.shard.transaction(async()=>{throw s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),i("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3),await c(async s=>{await s.shard.transaction(async()=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),s.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),s.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(s.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(s.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...s.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),i("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2),await c(async s=>{const e=Date.now()+50;if(await s.shard.alarms.set(e),t(await s.shard.alarms.get()).toBe(e),s.awaitAlarmFired===void 0){t(await s.shard.alarms.get()).toBe(e);return}await s.awaitAlarmFired(e),t(await s.shard.alarms.get()).toBeNull()})}),i("deletes a pending alarm",async()=>{t.assertions(1),await c(async s=>{await s.shard.alarms.set(Date.now()+1e4),await s.shard.alarms.delete(),t(await s.shard.alarms.get()).toBeNull()})})}),u("SocketHost",()=>{i("accepts a socket and can send/close",async()=>{await c(async s=>{const e=s.socket.accept(l(s),{user:"ada"});t(s.socket.idFor(e)).toBeDefined(),e.send("hello"),t(s.socket.getSockets().map(a=>s.socket.idFor(a))).toContain(s.socket.idFor(e)),s.readFrames!==void 0&&t(s.readFrames(e)).toStrictEqual(["hello"]),t(()=>{e.close(1e3,"done")}).not.toThrow()})}),i("round-trips an attachment on a live socket",async()=>{t.assertions(1),await c(async s=>{const e={roomId:"room-1",roles:["admin"]},a=s.socket.accept(l(s),e);t(a.deserializeAttachment()).toEqual(e)})}),i("round-trips attachments across a recycle",async s=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){s.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const a={roomId:"room-1",roles:["admin"]},o=e.socket.accept(l(e),a);e.simulateRecycle();const r=e.restoreSocket(e.socket.idFor(o),a);t(r.deserializeAttachment()).toEqual(a)})}),i("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1),await c(async s=>{const e=s.socket.accept(l(s),{}),a=s.socket.idFor(e),o=s.socket.idFor(e);t(o).toBe(a)})}),i("keeps idFor stable across a recycle",async s=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){s.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const a={roomId:"room-1",roles:["admin"]},o=e.socket.accept(l(e),a),r=e.socket.idFor(o);e.simulateRecycle();const d=e.restoreSocket(r,a);t(e.socket.idFor(d)).toBe(r)})}),i("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4),await c(async s=>{const e=s.socket.accept(l(s),{},["room-a"]),a=s.socket.accept(l(s),{},["room-b"]),o=s.socket.accept(l(s),{}),r=d=>s.socket.idFor(d);t(s.socket.getSockets("room-a").map(r)).toStrictEqual([r(e)]),t(s.socket.getSockets("room-b").map(r)).toStrictEqual([r(a)]),t(s.socket.getSockets("room-c").map(r)).toStrictEqual([]),t(s.socket.getSockets().map(r)).toContain(r(o))})}),i("accepts the portable budget of nine caller tags",async()=>{t.assertions(9);const s=await m(),e=Array.from({length:9},(r,d)=>`tag-${String(d)}`),a=s.socket.accept(l(s),{},e),o=r=>s.socket.idFor(r);for(const r of e)t(s.socket.getSockets(r).map(o)).toStrictEqual([o(a)]);s.cleanup?.()}),i("resolves a raw socket back to its handle",async()=>{t.assertions(2),await c(async s=>{const e=l(s),a=s.socket.accept(e,{}),o=s.socket.handleFor(e);t(o!==void 0&&s.socket.idFor(o)).toBe(s.socket.idFor(a)),t(s.socket.handleFor(l(s))).toBeUndefined()})}),i("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1),await c(async s=>{const e=s.socket.accept(l(s),{}),{bufferedAmount:a}=e;t(a===void 0||typeof a=="number"&&a>=0).toBe(!0)})}),i("retags a live socket when the host declares mutable tags",async s=>{await c(async e=>{if(e.socket.setTag===void 0){s.skip(`${n} does not implement mutable socket tags (setTag)`);return}t.assertions(2);const a=e.socket.accept(l(e),{});e.socket.setTag(a,"room-a"),t(e.socket.getSockets("room-a").map(o=>e.socket.idFor(o))).toStrictEqual([e.socket.idFor(a)]),e.socket.removeTag?.(a,"room-a"),t(e.socket.getSockets("room-a").map(o=>e.socket.idFor(o))).toStrictEqual([])})})}),u("ShardDirectory",()=>{i("resolves shard keys deterministically",async()=>{t.assertions(1),await c(async s=>{const e=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/")),a=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(e.text()).resolves.toBe(await a.text())})}),i("dispatches fetch to a resolved stub",async()=>{t.assertions(1),await c(async s=>{const e=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/"));t(e).toBeInstanceOf(Response)})})}),u("SchedulerHost",()=>{i("schedules a job for a future timestamp",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);const a=Date.now(),o=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(o.scheduledFor).toBeGreaterThanOrEqual(a+50),t(o.id).toBeDefined()})}),i("dispatches a scheduled job at least once",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}const a=e.scheduler.deadLetter!==void 0;if(!a&&e.awaitJobDispatched===void 0){s.skip(`${n} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(a&&t(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const o=e.scheduler.list!==void 0;t.assertions(1+(a?1:0)+(o?1:0));const r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await t(e.awaitJobDispatched(r.id)).resolves.toBe(!0),o){const d=await e.scheduler.list?.();t(d?.some(p=>p.id===r.id)).toBe(!1)}})}),i("cancels a scheduled job",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(1);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),o=await e.scheduler.cancel(a.id);t(o).toBe(!0)})}),i("reports a second cancel of the same job as false",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);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)})}),i("gives two identical schedules independently cancellable ids",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(3);const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),o=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(o.id).not.toBe(a.id),t(await e.scheduler.cancel(a.id)).toBe(!0),t(await e.scheduler.cancel(o.id)).toBe(!0)})}),i("lists a pending job with a zero attempt count",async s=>{await c(async e=>{if(e.scheduler?.list===void 0){s.skip(`${n} does not implement SchedulerHost.list`);return}t.assertions(2);const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),o=(await e.scheduler.list()).find(r=>r.id===a.id);t(o?.functionPath).toBe("tasks/remind"),t(o?.attempts).toBe(0)})}),i("keeps the pending and dead-letter listings disjoint",async s=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){s.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(2);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(a.id);const o=await e.scheduler.list(),r=await e.scheduler.deadLetter.list();t(o.some(d=>d.id===a.id)).toBe(!1),t(r.some(d=>d.id===a.id)).toBe(!0)})}),i("returns a requeued job to the pending set with a fresh budget",async s=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){s.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(4);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 o=(await e.scheduler.list()).find(d=>d.id===a.id);t(o).toBeDefined(),t(o?.attempts).toBe(0);const r=await e.scheduler.deadLetter.list();t(r.some(d=>d.id===a.id)).toBe(!1)})}),i("reports a requeue of an unparked job as false",async s=>{await c(async e=>{if(e.scheduler?.deadLetter===void 0){s.skip(`${n} does not implement scheduler.deadLetter`);return}t.assertions(1),t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})})}),u("ShardKvStore",()=>{i("reads back a written value",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} 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()})}),i("deletes a key idempotently",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} 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()})}),i("enumerates exactly the keys under a prefix",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} 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 a=await e.kv.list({prefix:"s:"}),o=await e.kv.list();t([...a.keys()].toSorted((r,d)=>r.localeCompare(d))).toStrictEqual(["s:a","s:b"]),t(o.size).toBe(3)})})})})};export{E 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 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-GABARI_Q.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
@@ -555,9 +555,13 @@ declare const CLOUDFLARE_CAPABILITIES: PlatformCapabilities;
555
555
  * a SQL table wearing a KV-shaped API, not a dedicated KV product, and
556
556
  * `websocketHibernation` never actually evicts a socket to save memory — it
557
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`.
558
+ * hibernation. `scheduler` and `shardAlarms` are rated `"unsupported"`, not
559
+ * `"emulated"`: the Node host stores and times both, but its timer body only
560
+ * clears bookkeeping — nothing dispatches the scheduled function or wakes the
561
+ * alarm callback. `"emulated"` means built on lower-level primitives and
562
+ * working; never-dispatched is not that (plan 267). `globalTables` is also
563
+ * `"unsupported"` — no replicated SQL store is implemented. All ratings are
564
+ * argued in detail in `plans/234-node-host-findings.md`.
561
565
  */
562
566
  declare const NODE_CAPABILITIES: PlatformCapabilities;
563
567
  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 };
package/dist/index.d.ts 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.js";
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-GABARI_Q.js";
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
@@ -555,9 +555,13 @@ declare const CLOUDFLARE_CAPABILITIES: PlatformCapabilities;
555
555
  * a SQL table wearing a KV-shaped API, not a dedicated KV product, and
556
556
  * `websocketHibernation` never actually evicts a socket to save memory — it
557
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`.
558
+ * hibernation. `scheduler` and `shardAlarms` are rated `"unsupported"`, not
559
+ * `"emulated"`: the Node host stores and times both, but its timer body only
560
+ * clears bookkeeping — nothing dispatches the scheduled function or wakes the
561
+ * alarm callback. `"emulated"` means built on lower-level primitives and
562
+ * working; never-dispatched is not that (plan 267). `globalTables` is also
563
+ * `"unsupported"` — no replicated SQL store is implemented. All ratings are
564
+ * argued in detail in `plans/234-node-host-findings.md`.
561
565
  */
562
566
  declare const NODE_CAPABILITIES: PlatformCapabilities;
563
567
  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 };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-CfXSyHOn.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-uGhAKuTB.mjs";export{O as CLOUDFLARE_CAPABILITIES,e as NODE_CAPABILITIES,E as NOOP_EXECUTION_CONTEXT,C as resolveShard};
1
+ import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-DMLgo_TI.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-uGhAKuTB.mjs";export{O as CLOUDFLARE_CAPABILITIES,e as NODE_CAPABILITIES,E as NOOP_EXECUTION_CONTEXT,C as resolveShard};
@@ -0,0 +1 @@
1
+ const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},objectStorage:{level:"native",note:"R2"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize; query/upsert namespace scoping is native (remote filter), but getByIds/deleteByIds id-path tenant isolation is facade-enforced (client-side verification) since Vectorize's id operations take no namespace option"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},containers:{level:"native",note:"Cloudflare Containers"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover"},globalTables:{level:"unsupported",note:"No replicated SQL store (D1-equivalent) implemented"},websocketHibernation:{level:"emulated",note:"In-process socket registry; attachments/tags survive a simulated recycle, not a process restart, and nothing is ever actually evicted from memory"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},shardAlarms:{level:"unsupported",note:"In-process bookkeeping only — the armed timer clears state and never wakes anything; no dispatch, and nothing re-arms across a restart"},crossShardFanout:{level:"unsupported",note:"No query coordinator / relay tier implemented"},queues:{level:"unsupported",note:"No Cloudflare Queues equivalent implemented"},workflows:{level:"unsupported",note:"No Cloudflare Workflows equivalent implemented"},scheduler:{level:"unsupported",note:"Jobs are stored and timed but never dispatched — no delivery, no retries; also not durable across a process restart"},objectStorage:{level:"unsupported",note:"No R2/S3-equivalent binding implemented"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"@lunora/mail's queue-backed sends need a queues binding, which this target does not provide"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars)"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
@@ -0,0 +1 @@
1
+ import{DatabaseSync as R}from"node:sqlite";let b=0,S=0;const j=()=>(b+=1,`socket-${b}`),C=()=>(S+=1,`job-${S}`),q=n=>n===void 0?null:n,E=n=>typeof n=="string"?new TextEncoder().encode(n).buffer:n instanceof ArrayBuffer?n:ArrayBuffer.isView(n)?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):new ArrayBuffer(0),O=()=>{const n=new R(":memory:"),s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},l=new Map,f=new Map,u=new Map,x={exec:(e,...t)=>{const a=n.prepare(e),r=t.map(q),o=e.trim().toLowerCase().startsWith("select")?a.all(...r):(a.run(...r),[]);return{[Symbol.iterator]:()=>o[Symbol.iterator](),one:()=>{if(o.length!==1)throw new Error(`expected exactly one row, got ${String(o.length)}`);return o[0]},toArray:()=>[...o]}}},M={all:async(e,t)=>n.prepare(e).all(...t),run:async(e,t)=>{const a=n.prepare(e).run(...t);return{rowsAffected:Number(a.changes)}}},h=()=>{if(s.running||s.pending.length===0)return;const e=s.pending.shift();e!==void 0&&(s.running=!0,e.function_().then(e.resolve,e.reject).finally(()=>{s.running=!1,h()}))},F=e=>new Promise((t,a)=>{s.pending.push({function_:e,reject:r=>{a(r)},resolve:r=>{t(r)}}),h()});let g=Promise.resolve();const p=async e=>{n.exec("BEGIN");try{const t=await e();return n.exec("COMMIT"),t}catch(t){throw n.exec("ROLLBACK"),t}},D={alarms:{delete:()=>{s.alarmAt=null,s.alarmTimeout!==null&&(clearTimeout(s.alarmTimeout),s.alarmTimeout=null)},get:()=>s.alarmAt,set:e=>{const t=typeof e=="number"?e:e.getTime();s.alarmAt=t,s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);const a=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},a)}},asyncSql:M,runSerialized:F,sql:x,transaction:e=>{const t=g.then(()=>p(e),()=>p(e));return g=t.then(()=>{},()=>{}),t},waitUntil:()=>{}},c=new WeakMap,w=e=>{const t={bufferedAmount:e.bufferedAmount,close:(a,r)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:a=>{e.received.push(typeof a=="string"?a:E(a))},serializeAttachment:a=>{e.attachment=a,f.set(e.id,a)}};return e.handle=t,c.set(t,e.id),t},P={accept:(e,t,a)=>{const r=j(),o={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:r,received:[],tags:new Set(a)};return l.set(r,o),u.set(r,new Set(a)),t!==void 0&&f.set(r,t),w(o)},getSockets:e=>{const t=[...l.values()];return(e===void 0?t:t.filter(a=>a.tags.has(e))).map(a=>a.handle)},handleFor:e=>[...l.values()].find(t=>t.raw===e)?.handle,idFor:e=>{const t=c.get(e);if(t===void 0)throw new Error("reference host: idFor called with a handle this host never issued");return t},removeTag:(e,t)=>{const a=l.get(c.get(e)??"");a!==void 0&&(t===void 0?a.tags.clear():a.tags.delete(t),u.set(c.get(e)??"",new Set(a.tags)))},setTag:(e,t)=>{const a=c.get(e)??"",r=l.get(a);r!==void 0&&(r.tags.add(t),u.set(a,new Set(r.tags)))}},y={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>y},d=new Map,k={delete:async e=>d.delete(e),get:async e=>d.get(e),list:async e=>{const t=e?.prefix??"",a=new Map;for(const[r,o]of d)r.startsWith(t)&&a.set(r,o);return a},put:async(e,t)=>{d.set(e,structuredClone(t))}},i=new Map,m=new Map,v=new Set,T=(e,t)=>({attempts:t.attempts,functionPath:t.functionPath,id:e,scheduledFor:t.scheduledFor});return{awaitAlarmFired:async e=>{await new Promise(t=>{setTimeout(t,Math.max(0,e-Date.now())+30)})},awaitJobDispatched:async e=>{const t=i.get(e);return t!==void 0&&await new Promise(a=>{setTimeout(a,Math.max(0,t.scheduledFor-Date.now())+30)}),v.has(e)},cleanup:()=>{n.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of i.values())clearTimeout(e.timer)},directory:y,kv:k,readFrames:e=>(l.get(c.get(e)??"")?.received??[]).filter(t=>typeof t=="string"),restoreSocket:(e,t)=>{const a={attachment:t,bufferedAmount:0,closed:!1,handle:null,id:e,raw:void 0,received:[],tags:new Set(u.get(e))};return l.set(e,a),w(a)},scheduler:{cancel:async e=>{const t=i.get(e);return t===void 0?!1:(clearTimeout(t.timer),i.delete(e),!0)},cron:async()=>{},deadLetter:{list:async()=>[...m].map(([e,t])=>T(e,t)),requeue:async e=>{const t=m.get(e);return t===void 0?!1:(m.delete(e),i.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...i].map(([e,t])=>T(e,t)),schedule:async(e,t,a)=>{const r=C();let o;a?.at===void 0?o=Date.now()+(a?.delayMs??0):o=typeof a.at=="number"?a.at:a.at.getTime();const B=Math.max(0,o-Date.now()),L=setTimeout(()=>{const A=i.get(r);A!==void 0&&(A.attempts+=1),v.add(r),i.delete(r)},B);return i.set(r,{args:t,attempts:0,functionPath:e,options:a??{},scheduledFor:o,timer:L}),{id:r,scheduledFor:o}}},simulateDeadLetter:async e=>{const t=i.get(e);return t===void 0?!1:(clearTimeout(t.timer),i.delete(e),m.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:D,simulateRecycle:()=>{l.clear()},socket:P}};export{O as createReferenceHost};
@@ -441,6 +441,17 @@ interface ShardHost {
441
441
  * — optional. Presence declares that the host can retag a live socket. Hosts
442
442
  * that cannot (Cloudflare) omit both methods, and callers that need to
443
443
  * retag must instead close and re-accept the socket with new tags.
444
+ *
445
+ * **Reserved-slot budget.** A host may reserve some of its accept-time tag
446
+ * slots for its own bookkeeping — Cloudflare's adapter prepends one identity
447
+ * tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
448
+ * Cloudflare's own cap is 10 tags per socket, 256 characters each
449
+ * (developers.cloudflare.com/durable-objects/api/state/), so with one slot
450
+ * reserved a portable caller should assume **at most 9 usable tags, each
451
+ * bounded to at most 256 characters** — a budget-exceeding `accept` call
452
+ * fails loudly on the host that enforces it (see
453
+ * {@link SocketHost.accept}) rather than passing silently on hosts with no
454
+ * cap and only failing, opaquely, on Cloudflare.
444
455
  */
445
456
  /**
446
457
  * The socket the engine sends through.
@@ -499,6 +510,14 @@ interface SocketHost {
499
510
  * survive recycling too and must be honoured by
500
511
  * {@link SocketHost.getSockets}. Returns a handle the engine can
501
512
  * send/close through.
513
+ *
514
+ * Portable callers should assume a budget of **at most 9 usable tags, each
515
+ * at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
516
+ * of their own 10-tag cap for bookkeeping; see this file's module-header
517
+ * "Reserved-slot budget" note. A host that enforces a cap rejects an
518
+ * over-budget call rather than accepting it and silently dropping or
519
+ * truncating tags, which would break {@link SocketHost.getSockets}'s
520
+ * exactness requirement.
502
521
  */
503
522
  accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
504
523
  /**
@@ -441,6 +441,17 @@ interface ShardHost {
441
441
  * — optional. Presence declares that the host can retag a live socket. Hosts
442
442
  * that cannot (Cloudflare) omit both methods, and callers that need to
443
443
  * retag must instead close and re-accept the socket with new tags.
444
+ *
445
+ * **Reserved-slot budget.** A host may reserve some of its accept-time tag
446
+ * slots for its own bookkeeping — Cloudflare's adapter prepends one identity
447
+ * tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
448
+ * Cloudflare's own cap is 10 tags per socket, 256 characters each
449
+ * (developers.cloudflare.com/durable-objects/api/state/), so with one slot
450
+ * reserved a portable caller should assume **at most 9 usable tags, each
451
+ * bounded to at most 256 characters** — a budget-exceeding `accept` call
452
+ * fails loudly on the host that enforces it (see
453
+ * {@link SocketHost.accept}) rather than passing silently on hosts with no
454
+ * cap and only failing, opaquely, on Cloudflare.
444
455
  */
445
456
  /**
446
457
  * The socket the engine sends through.
@@ -499,6 +510,14 @@ interface SocketHost {
499
510
  * survive recycling too and must be honoured by
500
511
  * {@link SocketHost.getSockets}. Returns a handle the engine can
501
512
  * send/close through.
513
+ *
514
+ * Portable callers should assume a budget of **at most 9 usable tags, each
515
+ * at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
516
+ * of their own 10-tag cap for bookkeeping; see this file's module-header
517
+ * "Reserved-slot budget" note. A host that enforces a cap rejects an
518
+ * over-budget call rather than accepting it and silently dropping or
519
+ * truncating tags, which would break {@link SocketHost.getSockets}'s
520
+ * exactness requirement.
502
521
  */
503
522
  accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
504
523
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/platform",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Provider-neutral host contracts for Lunora: shard/socket/directory/scheduler interfaces, binding projections, and the platform capability matrix",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},objectStorage:{level:"native",note:"R2"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},containers:{level:"native",note:"Cloudflare Containers"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover"},globalTables:{level:"unsupported",note:"No replicated SQL store (D1-equivalent) implemented"},websocketHibernation:{level:"emulated",note:"In-process socket registry; attachments/tags survive a simulated recycle, not a process restart, and nothing is ever actually evicted from memory"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},shardAlarms:{level:"emulated",note:"In-process setTimeout; the timestamp can be persisted to SQLite but nothing re-arms it on process restart"},crossShardFanout:{level:"unsupported",note:"No query coordinator / relay tier implemented"},queues:{level:"unsupported",note:"No Cloudflare Queues equivalent implemented"},workflows:{level:"unsupported",note:"No Cloudflare Workflows equivalent implemented"},scheduler:{level:"emulated",note:"In-process setTimeout only; not durable across a process restart, and no dynamic cron registration is implemented"},objectStorage:{level:"unsupported",note:"No R2/S3-equivalent binding implemented"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"@lunora/mail's queue-backed sends need a queues binding, which this target does not provide"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars)"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
@@ -1 +0,0 @@
1
- import{DatabaseSync as k}from"node:sqlite";let w=0,v=0;const B=()=>(w+=1,`socket-${w}`),D=()=>(v+=1,`job-${v}`),L=n=>n===void 0?null:n,C=n=>typeof n=="string"?new TextEncoder().encode(n).buffer:n instanceof ArrayBuffer?n:ArrayBuffer.isView(n)?n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength):new ArrayBuffer(0),R=()=>{const n=new k(":memory:"),s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},i=new Map,f=new Map,u=new Map,T={exec:(e,...t)=>{const r=n.prepare(e),a=t.map(L),o=e.trim().toLowerCase().startsWith("select")?r.all(...a):(r.run(...a),[]);return{[Symbol.iterator]:()=>o[Symbol.iterator](),one:()=>{if(o.length!==1)throw new Error(`expected exactly one row, got ${String(o.length)}`);return o[0]},toArray:()=>[...o]}}},A={all:async(e,t)=>n.prepare(e).all(...t),run:async(e,t)=>{const r=n.prepare(e).run(...t);return{rowsAffected:Number(r.changes)}}},g=()=>{if(s.running||s.pending.length===0)return;const e=s.pending.shift();e!==void 0&&(s.running=!0,e.function_().then(e.resolve,e.reject).finally(()=>{s.running=!1,g()}))},b={alarms:{delete:()=>{s.alarmAt=null,s.alarmTimeout!==null&&(clearTimeout(s.alarmTimeout),s.alarmTimeout=null)},get:()=>s.alarmAt,set:e=>{const t=typeof e=="number"?e:e.getTime();s.alarmAt=t,s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);const r=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},r)}},asyncSql:A,runSerialized:e=>new Promise((t,r)=>{s.pending.push({function_:e,reject:a=>{r(a)},resolve:a=>{t(a)}}),g()}),sql:T,transaction:async e=>{n.exec("BEGIN");try{const t=await e();return n.exec("COMMIT"),t}catch(t){throw n.exec("ROLLBACK"),t}},waitUntil:()=>{}},c=new WeakMap,h=e=>{const t={bufferedAmount:e.bufferedAmount,close:(r,a)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:r=>{e.received.push(typeof r=="string"?r:C(r))},serializeAttachment:r=>{e.attachment=r,f.set(e.id,r)}};return e.handle=t,c.set(t,e.id),t},M={accept:(e,t,r)=>{const a=B(),o={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:a,received:[],tags:new Set(r)};return i.set(a,o),u.set(a,new Set(r)),t!==void 0&&f.set(a,t),h(o)},getSockets:e=>{const t=[...i.values()];return(e===void 0?t:t.filter(r=>r.tags.has(e))).map(r=>r.handle)},handleFor:e=>[...i.values()].find(t=>t.raw===e)?.handle,idFor:e=>{const t=c.get(e);if(t===void 0)throw new Error("reference host: idFor called with a handle this host never issued");return t},removeTag:(e,t)=>{const r=i.get(c.get(e)??"");r!==void 0&&(t===void 0?r.tags.clear():r.tags.delete(t),u.set(c.get(e)??"",new Set(r.tags)))},setTag:(e,t)=>{const r=c.get(e)??"",a=i.get(r);a!==void 0&&(a.tags.add(t),u.set(r,new Set(a.tags)))}},p={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>p},d=new Map,S={delete:async e=>d.delete(e),get:async e=>d.get(e),list:async e=>{const t=e?.prefix??"",r=new Map;for(const[a,o]of d)a.startsWith(t)&&r.set(a,o);return r},put:async(e,t)=>{d.set(e,structuredClone(t))}},l=new Map,m=new Map,y=(e,t)=>({attempts:t.attempts,functionPath:t.functionPath,id:e,scheduledFor:t.scheduledFor});return{awaitAlarmFired:async e=>{await new Promise(t=>{setTimeout(t,Math.max(0,e-Date.now())+30)})},cleanup:()=>{n.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of l.values())clearTimeout(e.timer)},directory:p,kv:S,readFrames:e=>(i.get(c.get(e)??"")?.received??[]).filter(t=>typeof t=="string"),restoreSocket:(e,t)=>{const r={attachment:t,bufferedAmount:0,closed:!1,handle:null,id:e,raw:void 0,received:[],tags:new Set(u.get(e))};return i.set(e,r),h(r)},scheduler:{cancel:async e=>{const t=l.get(e);return t===void 0?!1:(clearTimeout(t.timer),l.delete(e),!0)},cron:async()=>{},deadLetter:{list:async()=>[...m].map(([e,t])=>y(e,t)),requeue:async e=>{const t=m.get(e);return t===void 0?!1:(m.delete(e),l.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...l].map(([e,t])=>y(e,t)),schedule:async(e,t,r)=>{const a=D();let o;r?.at===void 0?o=Date.now()+(r?.delayMs??0):o=typeof r.at=="number"?r.at:r.at.getTime();const x=Math.max(0,o-Date.now()),F=setTimeout(()=>{l.delete(a)},x);return l.set(a,{args:t,attempts:0,functionPath:e,options:r??{},scheduledFor:o,timer:F}),{id:a,scheduledFor:o}}},simulateDeadLetter:async e=>{const t=l.get(e);return t===void 0?!1:(clearTimeout(t.timer),l.delete(e),m.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:b,simulateRecycle:()=>{i.clear()},socket:M}};export{R as createReferenceHost};