@lunora/platform 1.0.0-alpha.10 → 1.0.0-alpha.12
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.mjs +1 -1
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +96 -1
- package/dist/index.d.ts +96 -1
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-C-0rt9Lx.mjs +1 -0
- package/dist/packem_shared/createReferenceHost-baEGrNOu.mjs +1 -0
- package/dist/packem_shared/resolveShard-BzKOUEO4.mjs +1 -0
- package/package.json +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-DXNmuRst.mjs +0 -1
- package/dist/packem_shared/createReferenceHost-NGFPX8d2.mjs +0 -1
- package/dist/packem_shared/resolveShard-BPCXp8pD.mjs +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-
|
|
1
|
+
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-baEGrNOu.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveShard as h}from"../packem_shared/resolveShard-BPCXp8pD.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};
|
|
1
|
+
import{resolveShard as k}from"../packem_shared/resolveShard-BzKOUEO4.mjs";const E=(n,m,y)=>{const{describe:u,expect:t,it:c}=y;u(`host contract: ${n}`,()=>{const p=async()=>m(),l=a=>a.createSocket?.()??{},o=async a=>{const e=await p();try{await a(e)}finally{e.cleanup?.()}};u("ShardHost",()=>{c("serializes mutations so no two closures interleave",async()=>{t.assertions(1),await o(async a=>{const e=[];await Promise.all([a.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(i=>{setTimeout(i,10)}),e.push("a-end")}),a.shard.runSerialized(async()=>{e.push("b-start"),await new Promise(i=>{setTimeout(i,5)}),e.push("b-end")})]);const s=e.join("").includes("a-starta-end"),r=e.join("").includes("b-startb-end");t(s&&r).toBe(!0)})}),c("rolls back a transaction that throws",async()=>{t.assertions(2),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=a.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(e).toHaveLength(0)})}),c("rejects with the value the closure threw, and stays usable",async()=>{t.assertions(3),await o(async a=>{const e=Object.assign(new Error("closure failed"),{code:"NOT_FOUND",status:404});await t(a.shard.transaction(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(async()=>a.shard.transaction(async()=>"still here"))).resolves.toBe("still here")})}),c("observes its own writes inside a transaction",async()=>{t.assertions(1),await o(async a=>{const e=await a.shard.transaction(async()=>(a.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),a.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),a.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(e).toBe("hello")})}),c("keeps overlapping transactions atomic",async()=>{t.assertions(2),await o(async a=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(i=>{setTimeout(i,20)}),a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),s=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,s]);const r=a.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();t(r.map(i=>i.id)).toStrictEqual(["A","B","C"]),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),c("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(a.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),c("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2),await o(async a=>{const e=Date.now()+50;if(await a.shard.alarms.set(e),t(await a.shard.alarms.get()).toBe(e),a.awaitAlarmFired===void 0){t(await a.shard.alarms.get()).toBe(e);return}await a.awaitAlarmFired(e),t(await a.shard.alarms.get()).toBeNull()})}),c("deletes a pending alarm",async()=>{t.assertions(1),await o(async a=>{await a.shard.alarms.set(Date.now()+1e4),await a.shard.alarms.delete(),t(await a.shard.alarms.get()).toBeNull()})})}),u("SocketHost",()=>{c("accepts a socket and can send/close",async()=>{await o(async a=>{const e=a.socket.accept(l(a),{user:"ada"});t(a.socket.idFor(e)).toBeDefined(),e.send("hello"),t(a.socket.getSockets().map(s=>a.socket.idFor(s))).toContain(a.socket.idFor(e)),a.readFrames!==void 0&&t(a.readFrames(e)).toStrictEqual(["hello"]),t(()=>{e.close(1e3,"done")}).not.toThrow()})}),c("round-trips an attachment on a live socket",async()=>{t.assertions(1),await o(async a=>{const e={roomId:"room-1",roles:["admin"]},s=a.socket.accept(l(a),e);t(s.deserializeAttachment()).toEqual(e)})}),c("round-trips attachments across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s);e.simulateRecycle();const i=e.restoreSocket(e.socket.idFor(r),s);t(i.deserializeAttachment()).toEqual(s)})}),c("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),s=a.socket.idFor(e),r=a.socket.idFor(e);t(r).toBe(s)})}),c("keeps idFor stable across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s),i=e.socket.idFor(r);e.simulateRecycle();const d=e.restoreSocket(i,s);t(e.socket.idFor(d)).toBe(i)})}),c("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4),await o(async a=>{const e=a.socket.accept(l(a),{},["room-a"]),s=a.socket.accept(l(a),{},["room-b"]),r=a.socket.accept(l(a),{}),i=d=>a.socket.idFor(d);t(a.socket.getSockets("room-a").map(i)).toStrictEqual([i(e)]),t(a.socket.getSockets("room-b").map(i)).toStrictEqual([i(s)]),t(a.socket.getSockets("room-c").map(i)).toStrictEqual([]),t(a.socket.getSockets().map(i)).toContain(i(r))})}),c("accepts the portable budget of nine caller tags",async()=>{t.assertions(9);const a=await p(),e=Array.from({length:9},(i,d)=>`tag-${String(d)}`),s=a.socket.accept(l(a),{},e),r=i=>a.socket.idFor(i);for(const i of e)t(a.socket.getSockets(i).map(r)).toStrictEqual([r(s)]);a.cleanup?.()}),c("resolves a raw socket back to its handle",async()=>{t.assertions(2),await o(async a=>{const e=l(a),s=a.socket.accept(e,{}),r=a.socket.handleFor(e);t(r!==void 0&&a.socket.idFor(r)).toBe(a.socket.idFor(s)),t(a.socket.handleFor(l(a))).toBeUndefined()})}),c("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),{bufferedAmount:s}=e;t(s===void 0||typeof s=="number"&&s>=0).toBe(!0)})}),c("retags a live socket when the host declares mutable tags",async a=>{await o(async e=>{if(e.socket.setTag===void 0){a.skip(`${n} does not implement mutable socket tags (setTag)`);return}t.assertions(2);const s=e.socket.accept(l(e),{});e.socket.setTag(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([e.socket.idFor(s)]),e.socket.removeTag?.(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([])})})}),u("ShardDirectory",()=>{c("resolves shard keys deterministically",async()=>{t.assertions(1),await o(async a=>{const e=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/")),s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(e.text()).resolves.toBe(await s.text())})}),c("dispatches fetch to a resolved stub",async()=>{t.assertions(1),await o(async a=>{const s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));t(s).toBeInstanceOf(Response)})})}),u("SchedulerHost",()=>{c("schedules a job for a future timestamp",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);const s=Date.now(),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(r.scheduledFor).toBeGreaterThanOrEqual(s+50),t(r.id).toBeDefined()})}),c("dispatches a scheduled job at least once",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${n} does not implement SchedulerHost`);return}const s=e.scheduler.deadLetter!==void 0;if(!s&&e.awaitJobDispatched===void 0){a.skip(`${n} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(s&&t(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const r=e.scheduler.list!==void 0;t.assertions(1+(s?1:0)+(r?1:0));const i=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await t(e.awaitJobDispatched(i.id)).resolves.toBe(!0),r){const d=await e.scheduler.list?.();t(d?.some(w=>w.id===i.id)).toBe(!1)}})}),c("cancels a scheduled job",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${n} does not implement SchedulerHost`);return}t.assertions(1);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),r=await e.scheduler.cancel(s.id);t(r).toBe(!0)})}),c("reports a second cancel of the same job as false",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(s.id)).toBe(!1)})}),c("gives two identical schedules independently cancellable ids",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${n} does not implement SchedulerHost`);return}t.assertions(3);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(r.id).not.toBe(s.id),t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(r.id)).toBe(!0)})}),c("lists a pending job with a zero attempt count",async a=>{await o(async e=>{if(e.scheduler?.list===void 0){a.skip(`${n} does not implement SchedulerHost.list`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),i=(await e.scheduler.list()).find(d=>d.id===s.id);t(i?.functionPath).toBe("tasks/remind"),t(i?.attempts).toBe(0)})}),c("keeps the pending and dead-letter listings disjoint",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id);const r=await e.scheduler.list(),i=await e.scheduler.deadLetter.list();t(r.some(d=>d.id===s.id)).toBe(!1),t(i.some(d=>d.id===s.id)).toBe(!0)})}),c("returns a requeued job to the pending set with a fresh budget",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(4);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id),t(await e.scheduler.deadLetter.requeue(s.id)).toBe(!0);const i=(await e.scheduler.list()).find(w=>w.id===s.id);t(i).toBeDefined(),t(i?.attempts).toBe(0);const d=await e.scheduler.deadLetter.list();t(d.some(w=>w.id===s.id)).toBe(!1)})}),c("reports a requeue of an unparked job as false",async a=>{await o(async e=>{if(e.scheduler?.deadLetter===void 0){a.skip(`${n} does not implement scheduler.deadLetter`);return}t.assertions(1),t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})})}),u("ShardKvStore",()=>{c("reads back a written value",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${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()})}),c("deletes a key idempotently",async a=>{await o(async e=>{if(e.kv===void 0){a.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()})}),c("enumerates exactly the keys under a prefix",async a=>{await o(async e=>{if(e.kv===void 0){a.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 s=await e.kv.list({prefix:"s:"}),r=await e.kv.list();t([...s.keys()].toSorted((i,d)=>i.localeCompare(d))).toStrictEqual(["s:a","s:b"]),t(r.size).toBe(3)})})})})};export{E as defineHostContractSuite};
|
package/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,13 @@ export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as Sc
|
|
|
22
22
|
* fall back to {@link NOOP_EXECUTION_CONTEXT}.
|
|
23
23
|
*/
|
|
24
24
|
interface ExecutionContextLike {
|
|
25
|
+
/**
|
|
26
|
+
* Present only when Cloudflare Access authenticated the request against a
|
|
27
|
+
* policy attached to the **Worker** (rather than to a hostname). `undefined`
|
|
28
|
+
* on every unauthenticated request, so its presence is itself the "Access
|
|
29
|
+
* authorized this caller" signal — see {@link AccessContextLike}.
|
|
30
|
+
*/
|
|
31
|
+
access?: AccessContextLike;
|
|
25
32
|
cache?: {
|
|
26
33
|
purge: (options: {
|
|
27
34
|
purgeEverything?: boolean;
|
|
@@ -31,6 +38,48 @@ interface ExecutionContextLike {
|
|
|
31
38
|
passThroughOnException?: () => void;
|
|
32
39
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The identity Cloudflare Access attaches to a Worker-protected request.
|
|
43
|
+
*
|
|
44
|
+
* Shape follows the Access application-token payload: `sub` is the stable per-user
|
|
45
|
+
* id, `email` the verified address, `common_name` the service-token name (machine
|
|
46
|
+
* callers, whose `sub` is empty), and `exp` the credential expiry in epoch
|
|
47
|
+
* **seconds**. Group membership is whatever the Access policy emits — a list of
|
|
48
|
+
* names, or of `{ id, name }` objects — hence `unknown`; normalize before use.
|
|
49
|
+
*
|
|
50
|
+
* Cloudflare may add further fields, so the index signature keeps them rather
|
|
51
|
+
* than dropping them: this is a view of a payload we do not own.
|
|
52
|
+
*/
|
|
53
|
+
interface AccessIdentityLike {
|
|
54
|
+
[claim: string]: unknown;
|
|
55
|
+
/** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
|
|
56
|
+
common_name?: string;
|
|
57
|
+
/** Verified user email. Present for interactive (SSO) callers. */
|
|
58
|
+
email?: string;
|
|
59
|
+
/** Credential expiry, epoch **seconds**. */
|
|
60
|
+
exp?: number;
|
|
61
|
+
/** IdP group membership — names or `{ id, name }` objects, depending on the policy. */
|
|
62
|
+
groups?: unknown;
|
|
63
|
+
/** Display name from the identity provider, when it emits one. */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** Stable per-user id, and what consumers key a user on. Empty for service tokens. */
|
|
66
|
+
sub?: string;
|
|
67
|
+
/** Cloudflare's per-user UUID. Carried through, but deliberately not used as an id — only this path emits it, so keying on it would not match the JWT path. */
|
|
68
|
+
user_uuid?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The `ctx.access` facade Cloudflare exposes on a Worker protected by Access.
|
|
72
|
+
*
|
|
73
|
+
* Reading the identity from here is preferable to verifying the
|
|
74
|
+
* `Cf-Access-Jwt-Assertion` header: the platform has already authenticated the
|
|
75
|
+
* caller, so there is no JWKS fetch, no audience check to get wrong, and nothing
|
|
76
|
+
* a request can forge — the field simply does not exist unless Access authorized
|
|
77
|
+
* the call. The header path remains the fallback for hostname-scoped Access
|
|
78
|
+
* applications, which do not populate this.
|
|
79
|
+
*/
|
|
80
|
+
interface AccessContextLike {
|
|
81
|
+
getIdentity: () => AccessIdentityLike | null | undefined | Promise<AccessIdentityLike | null | undefined>;
|
|
82
|
+
}
|
|
34
83
|
/**
|
|
35
84
|
* No-op `ExecutionContext` used when the host runtime didn't supply one (a
|
|
36
85
|
* non-Cloudflare preview, or a unit test), so the worker's `fetch` always
|
|
@@ -496,6 +545,24 @@ interface PlatformCapabilities {
|
|
|
496
545
|
globalTables?: Capability;
|
|
497
546
|
/** BYO database via connection pooling (Hyperdrive / RDS Proxy). */
|
|
498
547
|
hyperdrive?: Capability;
|
|
548
|
+
/**
|
|
549
|
+
* An identity-aware proxy in front of the app that authenticates the
|
|
550
|
+
* caller before the request reaches it, and hands the runtime a verified
|
|
551
|
+
* identity **out-of-band** — on the execution context rather than on the
|
|
552
|
+
* request (Cloudflare Access attached to a Worker; IAP; an ALB OIDC
|
|
553
|
+
* action).
|
|
554
|
+
*
|
|
555
|
+
* Rated separately from the header-stamping form of the same product
|
|
556
|
+
* because only this one needs a host primitive. An identity-aware proxy
|
|
557
|
+
* that merely adds a signed header is portable by construction: any host
|
|
558
|
+
* that receives an HTTP request can verify it, which is why
|
|
559
|
+
* `@lunora/cloudflare-access` still works on a target rated
|
|
560
|
+
* `unsupported` here (it falls back to the `Cf-Access-Jwt-Assertion`
|
|
561
|
+
* JWT). What is not portable is the identity arriving beside the
|
|
562
|
+
* request, which is why `ExecutionContextLike.access` is a projection a
|
|
563
|
+
* host either populates or does not.
|
|
564
|
+
*/
|
|
565
|
+
identityProxy?: Capability;
|
|
499
566
|
/** Key-value storage (KV / Redis / DynamoDB). */
|
|
500
567
|
keyValueStore?: Capability;
|
|
501
568
|
/** Local SQL execution inside a shard. */
|
|
@@ -596,4 +663,32 @@ declare const CLOUDFLARE_CAPABILITIES: PlatformCapabilities;
|
|
|
596
663
|
* Both are argued in detail in `plans/234-node-host-findings.md`.
|
|
597
664
|
*/
|
|
598
665
|
declare const NODE_CAPABILITIES: PlatformCapabilities;
|
|
599
|
-
export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike,
|
|
666
|
+
export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike,
|
|
667
|
+
/**
|
|
668
|
+
* `@lunora/platform` — provider-neutral host contracts for Lunora.
|
|
669
|
+
*
|
|
670
|
+
* This package defines the structural interfaces that separate the Lunora
|
|
671
|
+
* engine from any specific host (Cloudflare Workers, AWS, Rivet, Node, etc.).
|
|
672
|
+
* It contains **types and capability metadata only** — near-zero runtime code.
|
|
673
|
+
*
|
|
674
|
+
* The contracts fall into four groups:
|
|
675
|
+
*
|
|
676
|
+
* 1. **Shard host** (`ShardHost`) — single-writer execution, transactions,
|
|
677
|
+
* local SQL, alarms, and background continuation per shard key.
|
|
678
|
+
* 2. **Socket host** (`SocketHost`) — hibernated WebSocket subscriptions with
|
|
679
|
+
* durable attachments and tagged fan-out.
|
|
680
|
+
* 3. **Shard directory** (`ShardDirectory`) — deterministic placement and RPC
|
|
681
|
+
* dispatch from shard keys to stubs.
|
|
682
|
+
* 4. **Scheduler host** (`SchedulerHost`) — durable delayed jobs, cron, and
|
|
683
|
+
* at-least-once dispatch.
|
|
684
|
+
*
|
|
685
|
+
* Plus canonical binding projections (`KVNamespaceLike`, `R2BucketLike`,
|
|
686
|
+
* `QueueBindingLike`, `D1DatabaseLike`, `VectorizeIndexLike`, …) and the
|
|
687
|
+
* `PlatformCapabilities` matrix that codegen uses to tailor emitted types per
|
|
688
|
+
* target.
|
|
689
|
+
*
|
|
690
|
+
* This package is **zero-dependency** and safe on every runtime (browser,
|
|
691
|
+
* workerd, Node). It is intended to be the leaf dependency every other
|
|
692
|
+
* `@lunora/*` package can import without creating cycles.
|
|
693
|
+
*/
|
|
694
|
+
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
|
@@ -22,6 +22,13 @@ export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as Sc
|
|
|
22
22
|
* fall back to {@link NOOP_EXECUTION_CONTEXT}.
|
|
23
23
|
*/
|
|
24
24
|
interface ExecutionContextLike {
|
|
25
|
+
/**
|
|
26
|
+
* Present only when Cloudflare Access authenticated the request against a
|
|
27
|
+
* policy attached to the **Worker** (rather than to a hostname). `undefined`
|
|
28
|
+
* on every unauthenticated request, so its presence is itself the "Access
|
|
29
|
+
* authorized this caller" signal — see {@link AccessContextLike}.
|
|
30
|
+
*/
|
|
31
|
+
access?: AccessContextLike;
|
|
25
32
|
cache?: {
|
|
26
33
|
purge: (options: {
|
|
27
34
|
purgeEverything?: boolean;
|
|
@@ -31,6 +38,48 @@ interface ExecutionContextLike {
|
|
|
31
38
|
passThroughOnException?: () => void;
|
|
32
39
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The identity Cloudflare Access attaches to a Worker-protected request.
|
|
43
|
+
*
|
|
44
|
+
* Shape follows the Access application-token payload: `sub` is the stable per-user
|
|
45
|
+
* id, `email` the verified address, `common_name` the service-token name (machine
|
|
46
|
+
* callers, whose `sub` is empty), and `exp` the credential expiry in epoch
|
|
47
|
+
* **seconds**. Group membership is whatever the Access policy emits — a list of
|
|
48
|
+
* names, or of `{ id, name }` objects — hence `unknown`; normalize before use.
|
|
49
|
+
*
|
|
50
|
+
* Cloudflare may add further fields, so the index signature keeps them rather
|
|
51
|
+
* than dropping them: this is a view of a payload we do not own.
|
|
52
|
+
*/
|
|
53
|
+
interface AccessIdentityLike {
|
|
54
|
+
[claim: string]: unknown;
|
|
55
|
+
/** Service-token name. Present for non-interactive (machine) callers instead of `email`. */
|
|
56
|
+
common_name?: string;
|
|
57
|
+
/** Verified user email. Present for interactive (SSO) callers. */
|
|
58
|
+
email?: string;
|
|
59
|
+
/** Credential expiry, epoch **seconds**. */
|
|
60
|
+
exp?: number;
|
|
61
|
+
/** IdP group membership — names or `{ id, name }` objects, depending on the policy. */
|
|
62
|
+
groups?: unknown;
|
|
63
|
+
/** Display name from the identity provider, when it emits one. */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** Stable per-user id, and what consumers key a user on. Empty for service tokens. */
|
|
66
|
+
sub?: string;
|
|
67
|
+
/** Cloudflare's per-user UUID. Carried through, but deliberately not used as an id — only this path emits it, so keying on it would not match the JWT path. */
|
|
68
|
+
user_uuid?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The `ctx.access` facade Cloudflare exposes on a Worker protected by Access.
|
|
72
|
+
*
|
|
73
|
+
* Reading the identity from here is preferable to verifying the
|
|
74
|
+
* `Cf-Access-Jwt-Assertion` header: the platform has already authenticated the
|
|
75
|
+
* caller, so there is no JWKS fetch, no audience check to get wrong, and nothing
|
|
76
|
+
* a request can forge — the field simply does not exist unless Access authorized
|
|
77
|
+
* the call. The header path remains the fallback for hostname-scoped Access
|
|
78
|
+
* applications, which do not populate this.
|
|
79
|
+
*/
|
|
80
|
+
interface AccessContextLike {
|
|
81
|
+
getIdentity: () => AccessIdentityLike | null | undefined | Promise<AccessIdentityLike | null | undefined>;
|
|
82
|
+
}
|
|
34
83
|
/**
|
|
35
84
|
* No-op `ExecutionContext` used when the host runtime didn't supply one (a
|
|
36
85
|
* non-Cloudflare preview, or a unit test), so the worker's `fetch` always
|
|
@@ -496,6 +545,24 @@ interface PlatformCapabilities {
|
|
|
496
545
|
globalTables?: Capability;
|
|
497
546
|
/** BYO database via connection pooling (Hyperdrive / RDS Proxy). */
|
|
498
547
|
hyperdrive?: Capability;
|
|
548
|
+
/**
|
|
549
|
+
* An identity-aware proxy in front of the app that authenticates the
|
|
550
|
+
* caller before the request reaches it, and hands the runtime a verified
|
|
551
|
+
* identity **out-of-band** — on the execution context rather than on the
|
|
552
|
+
* request (Cloudflare Access attached to a Worker; IAP; an ALB OIDC
|
|
553
|
+
* action).
|
|
554
|
+
*
|
|
555
|
+
* Rated separately from the header-stamping form of the same product
|
|
556
|
+
* because only this one needs a host primitive. An identity-aware proxy
|
|
557
|
+
* that merely adds a signed header is portable by construction: any host
|
|
558
|
+
* that receives an HTTP request can verify it, which is why
|
|
559
|
+
* `@lunora/cloudflare-access` still works on a target rated
|
|
560
|
+
* `unsupported` here (it falls back to the `Cf-Access-Jwt-Assertion`
|
|
561
|
+
* JWT). What is not portable is the identity arriving beside the
|
|
562
|
+
* request, which is why `ExecutionContextLike.access` is a projection a
|
|
563
|
+
* host either populates or does not.
|
|
564
|
+
*/
|
|
565
|
+
identityProxy?: Capability;
|
|
499
566
|
/** Key-value storage (KV / Redis / DynamoDB). */
|
|
500
567
|
keyValueStore?: Capability;
|
|
501
568
|
/** Local SQL execution inside a shard. */
|
|
@@ -596,4 +663,32 @@ declare const CLOUDFLARE_CAPABILITIES: PlatformCapabilities;
|
|
|
596
663
|
* Both are argued in detail in `plans/234-node-host-findings.md`.
|
|
597
664
|
*/
|
|
598
665
|
declare const NODE_CAPABILITIES: PlatformCapabilities;
|
|
599
|
-
export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike,
|
|
666
|
+
export { type AnalyticsEngineDataPoint, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, CLOUDFLARE_CAPABILITIES, type Capability, type CapabilityLevel, type D1DatabaseLike, type D1PreparedStatementLike, type D1SessionLike,
|
|
667
|
+
/**
|
|
668
|
+
* `@lunora/platform` — provider-neutral host contracts for Lunora.
|
|
669
|
+
*
|
|
670
|
+
* This package defines the structural interfaces that separate the Lunora
|
|
671
|
+
* engine from any specific host (Cloudflare Workers, AWS, Rivet, Node, etc.).
|
|
672
|
+
* It contains **types and capability metadata only** — near-zero runtime code.
|
|
673
|
+
*
|
|
674
|
+
* The contracts fall into four groups:
|
|
675
|
+
*
|
|
676
|
+
* 1. **Shard host** (`ShardHost`) — single-writer execution, transactions,
|
|
677
|
+
* local SQL, alarms, and background continuation per shard key.
|
|
678
|
+
* 2. **Socket host** (`SocketHost`) — hibernated WebSocket subscriptions with
|
|
679
|
+
* durable attachments and tagged fan-out.
|
|
680
|
+
* 3. **Shard directory** (`ShardDirectory`) — deterministic placement and RPC
|
|
681
|
+
* dispatch from shard keys to stubs.
|
|
682
|
+
* 4. **Scheduler host** (`SchedulerHost`) — durable delayed jobs, cron, and
|
|
683
|
+
* at-least-once dispatch.
|
|
684
|
+
*
|
|
685
|
+
* Plus canonical binding projections (`KVNamespaceLike`, `R2BucketLike`,
|
|
686
|
+
* `QueueBindingLike`, `D1DatabaseLike`, `VectorizeIndexLike`, …) and the
|
|
687
|
+
* `PlatformCapabilities` matrix that codegen uses to tailor emitted types per
|
|
688
|
+
* target.
|
|
689
|
+
*
|
|
690
|
+
* This package is **zero-dependency** and safe on every runtime (browser,
|
|
691
|
+
* workerd, Node). It is intended to be the leaf dependency every other
|
|
692
|
+
* `@lunora/*` package can import without creating cycles.
|
|
693
|
+
*/
|
|
694
|
+
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-C-0rt9Lx.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-BzKOUEO4.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"},durableStreams:{level:"emulated",note:"Lunora persists each chunk to the shard's SQLite under a monotonic seq and keeps the producer alive past the socket via waitUntil; the platform has no streaming primitive of its own, and a run whose DO is evicted mid-flight ends as STREAM_INTERRUPTED rather than resuming"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},shardPlacement:{level:"native",note:"DurableObjectNamespace.get/getByName locationHint — best-effort, and honoured only by the resolution that creates the object"},shardReadReplicas:{level:"emulated",note:"Lunora follows the shard's CDC changelog into a replica DO placed in the reader's region; the platform replicates for durability, not for reads, so the follow loop is ours"},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"},objectStorageBackups:{level:"native",note:"`lunora backup create|list|restore --bucket` writes NDJSON snapshots + a manifest sidecar per snapshot through the admin storage routes (checksum-verified upload, admin-gated object read), and `backupCron`/`backupStore` runs the same layout unattended on a Cron Trigger. Both are bounded by what a single request body / a Worker isolate can hold, not by 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"},identityProxy:{level:"native",note:"Cloudflare Access. A policy attached to the Worker covers its custom domains, routes, workers.dev and preview URLs at once, and the authenticated identity arrives on the execution context as ctx.access — no header to verify, and nothing a request can forge to manufacture one. A hostname-scoped Access application instead stamps the Cf-Access-Jwt-Assertion header, which needs no host support at all"}}},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:"emulated",note:"The @lunora/sql-store core on its own SQLite file via the reference sqliteDialect — full store semantics, but one node with no replication"},websocketHibernation:{level:"emulated",note:"Socket registry with attachments/tags persisted to SQLite, so subscription state survives a process restart; nothing is ever actually evicted from memory, so this is durability without hibernation's memory saving"},durableStreams:{level:"unsupported",note:"The transcript store is host-neutral (@lunora/shard-engine), but the attach/produce state machine lives in @lunora/do and nothing in this host mounts it — a durable stream declared here would silently behave as an ephemeral one"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},shardAlarms:{level:"emulated",note:"setTimeout over a durable row, dispatched to onAlarm and re-armed on construction, so an alarm survives a restart and one whose time elapsed while the process was down fires late rather than never"},shardPlacement:{level:"unsupported",note:"One process — every shard lives where the process does, so a location hint has nowhere to place it"},shardReadReplicas:{level:"unsupported",note:"One process and one region: a replica here would be a second copy of a database already on the same disk"},crossShardFanout:{level:"emulated",note:"@lunora/runtime's query coordinator over the in-process shard registry; listShardKeys is seeded from the shard files on disk, and answers every shard rather than only those holding the table (a correct superset, at the cost of visiting shards with nothing to say)"},queues:{level:"emulated",note:`createNodeQueueHost (@lunora/platform-node) — a QueueBindingLike producer per declared queue over a durable _lunora_queue_messages table, and a batched consumer feeding the same dispatchQueueBatch the Cloudflare host uses. delaySeconds (capped at 12h), all four content types, maxBatchSize/maxBatchTimeout assembly, per-message ack/retry with workerd's implicit-ack-on-return and retry-on-throw, maxRetries into a declared deadLetterQueue (or parked in place, never dropped), and a visibility window so a crash mid-handler redelivers. Delivery is driven by poll(); there is no timer, because this host has no dev server to own one. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint`},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) compiles defineWorkflow handlers onto the @visulima/workflow engine (createRuntime): step/sleep/waitForEvent are durable + replay-safe, status maps to complete/errored/waiting/terminated, create({ id }) is honoured through a durable alias row (so ctx.spawn resolves and a retried create is one run), and runs survive a restart when backed by createNodeWorkflowStore (a SQLite WorkflowStore; the store is required, so no caller silently gets in-process-only state). Gaps: no pause/restart; terminate is not a barrier, so an activation already in flight overwrites the tombstone; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},scheduler:{level:"emulated",note:"SQLite job table dispatched to onDispatch and re-armed on construction, with retry backoff and a dead-letter queue; the only host implementing runtime cron registration (SchedulerHost.cron), which Cloudflare cannot offer"},objectStorageBackups:{level:"emulated",note:"The commands work unchanged, but the bucket underneath is createNodeR2Bucket — a directory on the same machine the CLI runs on, so a bucket-backed backup here is not the separate failure domain it is on Cloudflare. The scheduled half additionally needs this host's scheduler, which exists but is not a shipping target"},objectStorage:{level:"emulated",note:"createNodeR2Bucket (@lunora/platform-node) — an R2BucketLike over the local filesystem (fs/promises, head/list/range). One file per object with the metadata in a trailer, so the single rename that publishes the bytes publishes their checksum and content-type with them, and a get reads body and metadata through one handle rather than reopening the path. put streams into the staged file and .body streams the requested range; .arrayBuffer()/.text() still allocate the range they return. The body is single-use, as R2's is. Keys fold the way the host filesystem folds them, so `A` and `a` are one object on a case-insensitive volume where real R2 keeps two. No multipart uploads, no presigned URLs"},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"},identityProxy:{level:"unsupported",note:"Nothing sits in front of this host to authenticate callers, so it never populates the execution context's access identity. @lunora/cloudflare-access still works here through its Cf-Access-Jwt-Assertion fallback, which is a plain header check and needs no host support"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DatabaseSync as J}from"node:sqlite";let S=0,x=0;const L=()=>(S+=1,`socket-${S}`),P=()=>(x+=1,`job-${x}`),z=s=>s===void 0?null:s,I=s=>typeof s=="string"?new TextEncoder().encode(s).buffer:s instanceof ArrayBuffer?s:ArrayBuffer.isView(s)?s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength):new ArrayBuffer(0),W=()=>{const s=new J(":memory:"),a={alarmAt:null,alarmTimeout:null,pending:[],running:!1},i=new Map,h=new Map,d=new Map,M={exec:(e,...t)=>{const n=s.prepare(e),r=t.map(z),l=e.trim().toLowerCase().startsWith("select")?n.all(...r):(n.run(...r),[]);return{[Symbol.iterator]:()=>l[Symbol.iterator](),one:()=>{if(l.length!==1)throw new Error(`expected exactly one row, got ${String(l.length)}`);return l[0]},toArray:()=>[...l]}}},F={all:async(e,t)=>s.prepare(e).all(...t),run:async(e,t)=>{const r=s.prepare(e).run(...t);return{rowsAffected:Number(r.changes)}}},g=()=>{if(a.running||a.pending.length===0)return;const e=a.pending.shift();e!==void 0&&(a.running=!0,e.function_().then(e.resolve,e.reject).finally(()=>{a.running=!1,g()}))},j=e=>new Promise((t,n)=>{a.pending.push({function_:e,reject:r=>{n(r)},resolve:r=>{t(r)}}),g()});let w=Promise.resolve();const y=async e=>{s.exec("BEGIN");try{const t=await e();return s.exec("COMMIT"),t}catch(t){throw s.exec("ROLLBACK"),t}},k={alarms:{delete:()=>{a.alarmAt=null,a.alarmTimeout!==null&&(clearTimeout(a.alarmTimeout),a.alarmTimeout=null)},get:()=>a.alarmAt,set:e=>{const t=typeof e=="number"?e:e.getTime();a.alarmAt=t,a.alarmTimeout!==null&&clearTimeout(a.alarmTimeout);const n=Math.max(0,t-Date.now());a.alarmTimeout=setTimeout(()=>{a.alarmAt=null,a.alarmTimeout=null},n)}},asyncSql:F,runSerialized:j,sql:M,transaction:e=>{const t=w.then(()=>y(e),()=>y(e));return w=t.then(()=>{},()=>{}),t},waitUntil:()=>{}},u=new WeakMap,p=e=>{const t={bufferedAmount:e.bufferedAmount,close:(n,r)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:n=>{e.received.push(typeof n=="string"?n:I(n))},serializeAttachment:n=>{e.attachment=n,h.set(e.id,n)}};return e.handle=t,u.set(t,e.id),t},D={accept:(e,t,n)=>{const r=L(),c={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:r,received:[],tags:new Set(n)};return i.set(r,c),d.set(r,new Set(n)),t!==void 0&&h.set(r,t),p(c)},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=u.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 n=u.get(e)??"",r=i.get(n);r!==void 0&&(t===void 0?r.tags.clear():r.tags.delete(t),d.set(n,new Set(r.tags)))},setTag:(e,t)=>{const n=u.get(e)??"",r=i.get(n);r!==void 0&&(r.tags.add(t),d.set(n,new Set(r.tags)))}},b={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>b},m=new Map,B={delete:async e=>m.delete(e),get:async e=>m.get(e),list:async e=>{const t=e?.prefix??"",n=new Map;for(const[r,c]of m)r.startsWith(t)&&n.set(r,c);return n},put:async(e,t)=>{m.set(e,structuredClone(t))}},o=new Map,f=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=o.get(e);return t!==void 0&&await new Promise(n=>{setTimeout(n,Math.max(0,t.scheduledFor-Date.now())+30)}),v.has(e)},cleanup:()=>{s.close(),a.alarmTimeout!==null&&clearTimeout(a.alarmTimeout);for(const e of o.values())clearTimeout(e.timer)},directory:b,kv:B,readFrames:e=>(i.get(u.get(e)??"")?.received??[]).filter(t=>typeof t=="string"),restoreSocket:(e,t)=>{const n={attachment:t,bufferedAmount:0,closed:!1,handle:null,id:e,raw:void 0,received:[],tags:new Set(d.get(e))};return i.set(e,n),p(n)},scheduler:{cancel:async e=>{const t=o.get(e);return t===void 0?!1:(clearTimeout(t.timer),o.delete(e),!0)},cron:async()=>{},deadLetter:{list:async()=>[...f].map(([e,t])=>T(e,t)),requeue:async e=>{const t=f.get(e);return t===void 0?!1:(f.delete(e),o.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...o].map(([e,t])=>T(e,t)),schedule:async(e,t,n)=>{const r=P();let c;n?.at===void 0?c=Date.now()+(n?.delayMs??0):c=typeof n.at=="number"?n.at:n.at.getTime();const l=Math.max(0,c-Date.now()),C=setTimeout(()=>{const A=o.get(r);A!==void 0&&(A.attempts+=1),v.add(r),o.delete(r)},l);return o.set(r,{args:t,attempts:0,functionPath:e,options:n??{},scheduledFor:c,timer:C}),{id:r,scheduledFor:c}}},simulateDeadLetter:async e=>{const t=o.get(e);return t===void 0?!1:(clearTimeout(t.timer),o.delete(e),f.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:k,simulateRecycle:()=>{i.clear()},socket:D}};export{W as createReferenceHost};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a=(e,g,N)=>e.getByName!==void 0?e.getByName(g,N):e.get(e.idForName(g),N);export{a as resolveShard};
|
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.12",
|
|
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"},durableStreams:{level:"emulated",note:"Lunora persists each chunk to the shard's SQLite under a monotonic seq and keeps the producer alive past the socket via waitUntil; the platform has no streaming primitive of its own, and a run whose DO is evicted mid-flight ends as STREAM_INTERRUPTED rather than resuming"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},shardPlacement:{level:"native",note:"DurableObjectNamespace.get/getByName locationHint — best-effort, and honoured only by the resolution that creates the object"},shardReadReplicas:{level:"emulated",note:"Lunora follows the shard's CDC changelog into a replica DO placed in the reader's region; the platform replicates for durability, not for reads, so the follow loop is ours"},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"},objectStorageBackups:{level:"native",note:"`lunora backup create|list|restore --bucket` writes NDJSON snapshots + a manifest sidecar per snapshot through the admin storage routes (checksum-verified upload, admin-gated object read), and `backupCron`/`backupStore` runs the same layout unattended on a Cron Trigger. Both are bounded by what a single request body / a Worker isolate can hold, not by 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:"emulated",note:"The @lunora/sql-store core on its own SQLite file via the reference sqliteDialect — full store semantics, but one node with no replication"},websocketHibernation:{level:"emulated",note:"Socket registry with attachments/tags persisted to SQLite, so subscription state survives a process restart; nothing is ever actually evicted from memory, so this is durability without hibernation's memory saving"},durableStreams:{level:"unsupported",note:"The transcript store is host-neutral (@lunora/shard-engine), but the attach/produce state machine lives in @lunora/do and nothing in this host mounts it — a durable stream declared here would silently behave as an ephemeral one"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},shardAlarms:{level:"emulated",note:"setTimeout over a durable row, dispatched to onAlarm and re-armed on construction, so an alarm survives a restart and one whose time elapsed while the process was down fires late rather than never"},shardPlacement:{level:"unsupported",note:"One process — every shard lives where the process does, so a location hint has nowhere to place it"},shardReadReplicas:{level:"unsupported",note:"One process and one region: a replica here would be a second copy of a database already on the same disk"},crossShardFanout:{level:"emulated",note:"@lunora/runtime's query coordinator over the in-process shard registry; listShardKeys is seeded from the shard files on disk, and answers every shard rather than only those holding the table (a correct superset, at the cost of visiting shards with nothing to say)"},queues:{level:"emulated",note:`createNodeQueueHost (@lunora/platform-node) — a QueueBindingLike producer per declared queue over a durable _lunora_queue_messages table, and a batched consumer feeding the same dispatchQueueBatch the Cloudflare host uses. delaySeconds (capped at 12h), all four content types, maxBatchSize/maxBatchTimeout assembly, per-message ack/retry with workerd's implicit-ack-on-return and retry-on-throw, maxRetries into a declared deadLetterQueue (or parked in place, never dropped), and a visibility window so a crash mid-handler redelivers. Delivery is driven by poll(); there is no timer, because this host has no dev server to own one. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint`},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) compiles defineWorkflow handlers onto the @visulima/workflow engine (createRuntime): step/sleep/waitForEvent are durable + replay-safe, status maps to complete/errored/waiting/terminated, create({ id }) is honoured through a durable alias row (so ctx.spawn resolves and a retried create is one run), and runs survive a restart when backed by createNodeWorkflowStore (a SQLite WorkflowStore; the store is required, so no caller silently gets in-process-only state). Gaps: no pause/restart; terminate is not a barrier, so an activation already in flight overwrites the tombstone; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},scheduler:{level:"emulated",note:"SQLite job table dispatched to onDispatch and re-armed on construction, with retry backoff and a dead-letter queue; the only host implementing runtime cron registration (SchedulerHost.cron), which Cloudflare cannot offer"},objectStorageBackups:{level:"emulated",note:"The commands work unchanged, but the bucket underneath is createNodeR2Bucket — a directory on the same machine the CLI runs on, so a bucket-backed backup here is not the separate failure domain it is on Cloudflare. The scheduled half additionally needs this host's scheduler, which exists but is not a shipping target"},objectStorage:{level:"emulated",note:"createNodeR2Bucket (@lunora/platform-node) — an R2BucketLike over the local filesystem (fs/promises, head/list/range). One file per object with the metadata in a trailer, so the single rename that publishes the bytes publishes their checksum and content-type with them, and a get reads body and metadata through one handle rather than reopening the path. put streams into the staged file and .body streams the requested range; .arrayBuffer()/.text() still allocate the range they return. The body is single-use, as R2's is. Keys fold the way the host filesystem folds them, so `A` and `a` are one object on a case-insensitive volume where real R2 keeps two. No multipart uploads, no presigned URLs"},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 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=c.get(e)??"",r=l.get(a);r!==void 0&&(t===void 0?r.tags.clear():r.tags.delete(t),u.set(a,new Set(r.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};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const a=(e,o,t)=>e.getByName!==void 0?e.getByName(o,t):e.get(e.idForName(o),t);export{a as resolveShard};
|