@lunora/platform 1.0.0-alpha.2 → 1.0.0-alpha.4
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.
- package/dist/conformance/index.d.mts +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.mjs +1 -1
- package/dist/conformance/suite.d.mts +10 -1
- package/dist/conformance/suite.d.ts +10 -1
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-DMLgo_TI.mjs +1 -0
- package/dist/packem_shared/createReferenceHost-fs0Q8_VA.mjs +1 -0
- package/dist/packem_shared/{socket-host.d-Cq5uVbiH.d.mts → socket-host.d-DCVFvmXn.d.mts} +30 -2
- package/dist/packem_shared/{socket-host.d-Cq5uVbiH.d.ts → socket-host.d-DCVFvmXn.d.ts} +30 -2
- package/package.json +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-CfXSyHOn.mjs +0 -1
- package/dist/packem_shared/createReferenceHost-GO8Hp4ft.mjs +0 -1
|
@@ -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-
|
|
2
|
+
import "../packem_shared/socket-host.d-DCVFvmXn.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-
|
|
2
|
+
import "../packem_shared/socket-host.d-DCVFvmXn.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-
|
|
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-
|
|
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-DCVFvmXn.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-
|
|
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-DCVFvmXn.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,m,k)=>{const{describe:u,expect:t,it:i}=k;u(`host contract: ${n}`,()=>{const w=async()=>m(),l=s=>s.createSocket?.()??{},c=async s=>{const e=await w();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("rejects with the value the closure threw, and stays usable",async()=>{t.assertions(3),await c(async s=>{const e=Object.assign(new Error("closure failed"),{code:"NOT_FOUND",status:404});await t(s.shard.transaction(()=>Promise.reject(e))).rejects.toBe(e),await t(s.shard.runSerialized(()=>Promise.reject(e))).rejects.toBe(e),await t(s.shard.runSerialized(async()=>s.shard.transaction(async()=>"still here"))).resolves.toBe("still here")})}),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 w(),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(y=>y.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-
|
|
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-DCVFvmXn.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.
|
|
559
|
-
*
|
|
560
|
-
*
|
|
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-
|
|
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-DCVFvmXn.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.
|
|
559
|
-
*
|
|
560
|
-
*
|
|
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-
|
|
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};
|
|
@@ -382,6 +382,13 @@ interface ShardHost {
|
|
|
382
382
|
* Run `fn` with exclusive ownership of the shard. Concurrent calls are
|
|
383
383
|
* queued; no two closures run at once for the same shard key. On
|
|
384
384
|
* Cloudflare this maps to `state.blockConcurrencyWhile`.
|
|
385
|
+
*
|
|
386
|
+
* A closure that throws must reject with **the value it threw**, and must
|
|
387
|
+
* leave the host usable for the next call. Engine errors carry a `code` and
|
|
388
|
+
* `status` the RPC edge renders from, so a host that lets its platform
|
|
389
|
+
* substitute a copy silently downgrades every coded error to an internal
|
|
390
|
+
* fault — and a host that tears itself down on a throw makes an ordinary
|
|
391
|
+
* application error cost every other caller on that shard.
|
|
385
392
|
*/
|
|
386
393
|
runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
|
|
387
394
|
/**
|
|
@@ -401,8 +408,10 @@ interface ShardHost {
|
|
|
401
408
|
sql: ShardSqlExec;
|
|
402
409
|
/**
|
|
403
410
|
* Run `fn` inside a durable transaction. If `fn` throws, all writes roll
|
|
404
|
-
* back
|
|
405
|
-
*
|
|
411
|
+
* back and the call rejects with **the value `fn` threw** — see
|
|
412
|
+
* {@link ShardHost.runSerialized} for why the identity matters. Raw
|
|
413
|
+
* `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
|
|
414
|
+
* manages the transaction boundary.
|
|
406
415
|
*/
|
|
407
416
|
transaction: <T>(function_: () => Promise<T>) => Promise<T>;
|
|
408
417
|
/**
|
|
@@ -441,6 +450,17 @@ interface ShardHost {
|
|
|
441
450
|
* — optional. Presence declares that the host can retag a live socket. Hosts
|
|
442
451
|
* that cannot (Cloudflare) omit both methods, and callers that need to
|
|
443
452
|
* retag must instead close and re-accept the socket with new tags.
|
|
453
|
+
*
|
|
454
|
+
* **Reserved-slot budget.** A host may reserve some of its accept-time tag
|
|
455
|
+
* slots for its own bookkeeping — Cloudflare's adapter prepends one identity
|
|
456
|
+
* tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
|
|
457
|
+
* Cloudflare's own cap is 10 tags per socket, 256 characters each
|
|
458
|
+
* (developers.cloudflare.com/durable-objects/api/state/), so with one slot
|
|
459
|
+
* reserved a portable caller should assume **at most 9 usable tags, each
|
|
460
|
+
* bounded to at most 256 characters** — a budget-exceeding `accept` call
|
|
461
|
+
* fails loudly on the host that enforces it (see
|
|
462
|
+
* {@link SocketHost.accept}) rather than passing silently on hosts with no
|
|
463
|
+
* cap and only failing, opaquely, on Cloudflare.
|
|
444
464
|
*/
|
|
445
465
|
/**
|
|
446
466
|
* The socket the engine sends through.
|
|
@@ -499,6 +519,14 @@ interface SocketHost {
|
|
|
499
519
|
* survive recycling too and must be honoured by
|
|
500
520
|
* {@link SocketHost.getSockets}. Returns a handle the engine can
|
|
501
521
|
* send/close through.
|
|
522
|
+
*
|
|
523
|
+
* Portable callers should assume a budget of **at most 9 usable tags, each
|
|
524
|
+
* at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
|
|
525
|
+
* of their own 10-tag cap for bookkeeping; see this file's module-header
|
|
526
|
+
* "Reserved-slot budget" note. A host that enforces a cap rejects an
|
|
527
|
+
* over-budget call rather than accepting it and silently dropping or
|
|
528
|
+
* truncating tags, which would break {@link SocketHost.getSockets}'s
|
|
529
|
+
* exactness requirement.
|
|
502
530
|
*/
|
|
503
531
|
accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
|
|
504
532
|
/**
|
|
@@ -382,6 +382,13 @@ interface ShardHost {
|
|
|
382
382
|
* Run `fn` with exclusive ownership of the shard. Concurrent calls are
|
|
383
383
|
* queued; no two closures run at once for the same shard key. On
|
|
384
384
|
* Cloudflare this maps to `state.blockConcurrencyWhile`.
|
|
385
|
+
*
|
|
386
|
+
* A closure that throws must reject with **the value it threw**, and must
|
|
387
|
+
* leave the host usable for the next call. Engine errors carry a `code` and
|
|
388
|
+
* `status` the RPC edge renders from, so a host that lets its platform
|
|
389
|
+
* substitute a copy silently downgrades every coded error to an internal
|
|
390
|
+
* fault — and a host that tears itself down on a throw makes an ordinary
|
|
391
|
+
* application error cost every other caller on that shard.
|
|
385
392
|
*/
|
|
386
393
|
runSerialized: <T>(function_: () => Promise<T>) => Promise<T>;
|
|
387
394
|
/**
|
|
@@ -401,8 +408,10 @@ interface ShardHost {
|
|
|
401
408
|
sql: ShardSqlExec;
|
|
402
409
|
/**
|
|
403
410
|
* Run `fn` inside a durable transaction. If `fn` throws, all writes roll
|
|
404
|
-
* back
|
|
405
|
-
*
|
|
411
|
+
* back and the call rejects with **the value `fn` threw** — see
|
|
412
|
+
* {@link ShardHost.runSerialized} for why the identity matters. Raw
|
|
413
|
+
* `BEGIN`/`COMMIT`/`ROLLBACK` are forbidden inside the closure; the host
|
|
414
|
+
* manages the transaction boundary.
|
|
406
415
|
*/
|
|
407
416
|
transaction: <T>(function_: () => Promise<T>) => Promise<T>;
|
|
408
417
|
/**
|
|
@@ -441,6 +450,17 @@ interface ShardHost {
|
|
|
441
450
|
* — optional. Presence declares that the host can retag a live socket. Hosts
|
|
442
451
|
* that cannot (Cloudflare) omit both methods, and callers that need to
|
|
443
452
|
* retag must instead close and re-accept the socket with new tags.
|
|
453
|
+
*
|
|
454
|
+
* **Reserved-slot budget.** A host may reserve some of its accept-time tag
|
|
455
|
+
* slots for its own bookkeeping — Cloudflare's adapter prepends one identity
|
|
456
|
+
* tag (so `idFor` survives hibernation) before every `acceptWebSocket` call.
|
|
457
|
+
* Cloudflare's own cap is 10 tags per socket, 256 characters each
|
|
458
|
+
* (developers.cloudflare.com/durable-objects/api/state/), so with one slot
|
|
459
|
+
* reserved a portable caller should assume **at most 9 usable tags, each
|
|
460
|
+
* bounded to at most 256 characters** — a budget-exceeding `accept` call
|
|
461
|
+
* fails loudly on the host that enforces it (see
|
|
462
|
+
* {@link SocketHost.accept}) rather than passing silently on hosts with no
|
|
463
|
+
* cap and only failing, opaquely, on Cloudflare.
|
|
444
464
|
*/
|
|
445
465
|
/**
|
|
446
466
|
* The socket the engine sends through.
|
|
@@ -499,6 +519,14 @@ interface SocketHost {
|
|
|
499
519
|
* survive recycling too and must be honoured by
|
|
500
520
|
* {@link SocketHost.getSockets}. Returns a handle the engine can
|
|
501
521
|
* send/close through.
|
|
522
|
+
*
|
|
523
|
+
* Portable callers should assume a budget of **at most 9 usable tags, each
|
|
524
|
+
* at most 256 characters** — some hosts (Cloudflare) reserve one tag slot
|
|
525
|
+
* of their own 10-tag cap for bookkeeping; see this file's module-header
|
|
526
|
+
* "Reserved-slot budget" note. A host that enforces a cap rejects an
|
|
527
|
+
* over-budget call rather than accepting it and silently dropping or
|
|
528
|
+
* truncating tags, which would break {@link SocketHost.getSockets}'s
|
|
529
|
+
* exactness requirement.
|
|
502
530
|
*/
|
|
503
531
|
accept: (socket: unknown, attachment?: unknown, tags?: ReadonlyArray<string>) => SocketHandle;
|
|
504
532
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/platform",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
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};
|