@lunora/platform 1.0.0-alpha.13 → 1.0.0-alpha.14
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 +17 -18
- package/dist/conformance/suite.d.ts +17 -18
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packem_shared/createReferenceHost-DMBPMGqm.mjs +1 -0
- package/dist/packem_shared/{socket-host.d-Dy2QiVn6.d.mts → socket-host.d-dVPE86WP.d.mts} +1 -18
- package/dist/packem_shared/{socket-host.d-Dy2QiVn6.d.ts → socket-host.d-dVPE86WP.d.ts} +1 -18
- package/package.json +1 -1
- package/dist/packem_shared/createReferenceHost-baEGrNOu.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-dVPE86WP.mjs";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.js";
|
|
2
|
-
import "../packem_shared/socket-host.d-
|
|
2
|
+
import "../packem_shared/socket-host.d-dVPE86WP.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-
|
|
1
|
+
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-DMBPMGqm.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-dVPE86WP.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* A conformance host bundles all four platform contracts so a single factory
|
|
4
4
|
* can stand up a complete, isolated test environment.
|
|
@@ -32,6 +32,22 @@ interface ConformanceHost {
|
|
|
32
32
|
createSocket?: () => unknown;
|
|
33
33
|
/** The shard directory under test. */
|
|
34
34
|
directory: ShardDirectory;
|
|
35
|
+
/**
|
|
36
|
+
* Terminally dispose this host instance, as opposed to {@link
|
|
37
|
+
* ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
|
|
38
|
+
* `cleanup`) use as a per-test reset rather than a true teardown — the DO's
|
|
39
|
+
* storage has no explicit close a test can drive, so `cleanup` there just
|
|
40
|
+
* disarms the pending alarm and drops socket references for the next run.
|
|
41
|
+
*
|
|
42
|
+
* Optional: only a host with a real terminal dispose implements it. Where
|
|
43
|
+
* it exists, the suite calls it once and then asserts every surface that
|
|
44
|
+
* documents a post-close behaviour (`ShardHost.alarms`,
|
|
45
|
+
* `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
|
|
46
|
+
* closed with a `"platform closed: …"` error — the same "report the gap
|
|
47
|
+
* instead of asserting a false close" pattern `scheduler`/`kv` already use
|
|
48
|
+
* for hosts that don't implement a surface at all.
|
|
49
|
+
*/
|
|
50
|
+
disposeTerminally?: () => void;
|
|
35
51
|
/**
|
|
36
52
|
* The durable key-value store under test. Optional: a host that implements
|
|
37
53
|
* only the reactive-engine half (`ShardHost`) has no KV surface to offer,
|
|
@@ -120,22 +136,5 @@ type VitestApi = {
|
|
|
120
136
|
expect: typeof import("vitest").expect;
|
|
121
137
|
it: typeof import("vitest").it;
|
|
122
138
|
};
|
|
123
|
-
/**
|
|
124
|
-
* Define the host-contract conformance suite for the given factory.
|
|
125
|
-
*
|
|
126
|
-
* The suite asserts the provider-neutral behaviors that every Lunora host must
|
|
127
|
-
* provide: single-writer serialization, durable transactions, local SQL,
|
|
128
|
-
* durable alarms, socket accept/send/close, attachment round-trip across
|
|
129
|
-
* recycle, deterministic shard placement, and durable scheduling.
|
|
130
|
-
*
|
|
131
|
-
* Usage:
|
|
132
|
-
*
|
|
133
|
-
* ```ts
|
|
134
|
-
* import { describe, expect, it } from "vitest";
|
|
135
|
-
* import { createReferenceHost, defineHostContractSuite } from "@lunora/platform/conformance";
|
|
136
|
-
*
|
|
137
|
-
* defineHostContractSuite("reference", createReferenceHost, { describe, expect, it });
|
|
138
|
-
* ```
|
|
139
|
-
*/
|
|
140
139
|
declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
|
|
141
140
|
export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-dVPE86WP.js";
|
|
2
2
|
/**
|
|
3
3
|
* A conformance host bundles all four platform contracts so a single factory
|
|
4
4
|
* can stand up a complete, isolated test environment.
|
|
@@ -32,6 +32,22 @@ interface ConformanceHost {
|
|
|
32
32
|
createSocket?: () => unknown;
|
|
33
33
|
/** The shard directory under test. */
|
|
34
34
|
directory: ShardDirectory;
|
|
35
|
+
/**
|
|
36
|
+
* Terminally dispose this host instance, as opposed to {@link
|
|
37
|
+
* ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
|
|
38
|
+
* `cleanup`) use as a per-test reset rather than a true teardown — the DO's
|
|
39
|
+
* storage has no explicit close a test can drive, so `cleanup` there just
|
|
40
|
+
* disarms the pending alarm and drops socket references for the next run.
|
|
41
|
+
*
|
|
42
|
+
* Optional: only a host with a real terminal dispose implements it. Where
|
|
43
|
+
* it exists, the suite calls it once and then asserts every surface that
|
|
44
|
+
* documents a post-close behaviour (`ShardHost.alarms`,
|
|
45
|
+
* `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
|
|
46
|
+
* closed with a `"platform closed: …"` error — the same "report the gap
|
|
47
|
+
* instead of asserting a false close" pattern `scheduler`/`kv` already use
|
|
48
|
+
* for hosts that don't implement a surface at all.
|
|
49
|
+
*/
|
|
50
|
+
disposeTerminally?: () => void;
|
|
35
51
|
/**
|
|
36
52
|
* The durable key-value store under test. Optional: a host that implements
|
|
37
53
|
* only the reactive-engine half (`ShardHost`) has no KV surface to offer,
|
|
@@ -120,22 +136,5 @@ type VitestApi = {
|
|
|
120
136
|
expect: typeof import("vitest").expect;
|
|
121
137
|
it: typeof import("vitest").it;
|
|
122
138
|
};
|
|
123
|
-
/**
|
|
124
|
-
* Define the host-contract conformance suite for the given factory.
|
|
125
|
-
*
|
|
126
|
-
* The suite asserts the provider-neutral behaviors that every Lunora host must
|
|
127
|
-
* provide: single-writer serialization, durable transactions, local SQL,
|
|
128
|
-
* durable alarms, socket accept/send/close, attachment round-trip across
|
|
129
|
-
* recycle, deterministic shard placement, and durable scheduling.
|
|
130
|
-
*
|
|
131
|
-
* Usage:
|
|
132
|
-
*
|
|
133
|
-
* ```ts
|
|
134
|
-
* import { describe, expect, it } from "vitest";
|
|
135
|
-
* import { createReferenceHost, defineHostContractSuite } from "@lunora/platform/conformance";
|
|
136
|
-
*
|
|
137
|
-
* defineHostContractSuite("reference", createReferenceHost, { describe, expect, it });
|
|
138
|
-
* ```
|
|
139
|
-
*/
|
|
140
139
|
declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
|
|
141
140
|
export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
|
|
@@ -1 +1 @@
|
|
|
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};
|
|
1
|
+
import{resolveShard as k}from"../packem_shared/resolveShard-BzKOUEO4.mjs";const f=/platform closed/u,b=(d,v,S)=>{const{describe:u,expect:t,it:c}=S;u(`host contract: ${d}`,()=>{const p=async()=>v(),l=a=>a.createSocket?.()??{},o=async a=>{const e=await p();try{await a(e)}finally{e.cleanup?.()}};u("ShardHost",()=>{c("serializes mutations so no two closures interleave",async()=>{t.assertions(1),await o(async a=>{const e=[];await Promise.all([a.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(i=>{setTimeout(i,10)}),e.push("a-end")}),a.shard.runSerialized(async()=>{e.push("b-start"),await new Promise(i=>{setTimeout(i,5)}),e.push("b-end")})]);const s=e.join("").includes("a-starta-end"),r=e.join("").includes("b-startb-end");t(s&&r).toBe(!0)})}),c("rolls back a transaction that throws",async()=>{t.assertions(2),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=a.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(e).toHaveLength(0)})}),c("rejects with the value the closure threw, and stays usable",async()=>{t.assertions(3),await o(async a=>{const e=Object.assign(new Error("closure failed"),{code:"NOT_FOUND",status:404});await t(a.shard.transaction(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(()=>Promise.reject(e))).rejects.toBe(e),await t(a.shard.runSerialized(async()=>a.shard.transaction(async()=>"still here"))).resolves.toBe("still here")})}),c("observes its own writes inside a transaction",async()=>{t.assertions(1),await o(async a=>{const e=await a.shard.transaction(async()=>(a.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),a.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),a.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(e).toBe("hello")})}),c("keeps overlapping transactions atomic",async()=>{t.assertions(2),await o(async a=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(i=>{setTimeout(i,20)}),a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),s=a.shard.transaction(async()=>{a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,s]);const r=a.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();t(r.map(i=>i.id)).toStrictEqual(["A","B","C"]),await t(a.shard.transaction(async()=>{throw a.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),c("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3),await o(async a=>{await a.shard.transaction(async()=>{a.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),a.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(a.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...a.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),c("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2),await o(async a=>{const e=Date.now()+50;if(await a.shard.alarms.set(e),t(await a.shard.alarms.get()).toBe(e),a.awaitAlarmFired===void 0){t(await a.shard.alarms.get()).toBe(e);return}await a.awaitAlarmFired(e),t(await a.shard.alarms.get()).toBeNull()})}),c("deletes a pending alarm",async()=>{t.assertions(1),await o(async a=>{await a.shard.alarms.set(Date.now()+1e4),await a.shard.alarms.delete(),t(await a.shard.alarms.get()).toBeNull()})})}),u("SocketHost",()=>{c("accepts a socket and can send/close",async()=>{await o(async a=>{const e=a.socket.accept(l(a),{user:"ada"});t(a.socket.idFor(e)).toBeDefined(),e.send("hello"),t(a.socket.getSockets().map(s=>a.socket.idFor(s))).toContain(a.socket.idFor(e)),a.readFrames!==void 0&&t(a.readFrames(e)).toStrictEqual(["hello"]),t(()=>{e.close(1e3,"done")}).not.toThrow()})}),c("round-trips an attachment on a live socket",async()=>{t.assertions(1),await o(async a=>{const e={roomId:"room-1",roles:["admin"]},s=a.socket.accept(l(a),e);t(s.deserializeAttachment()).toEqual(e)})}),c("round-trips attachments across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s);e.simulateRecycle();const i=e.restoreSocket(e.socket.idFor(r),s);t(i.deserializeAttachment()).toEqual(s)})}),c("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),s=a.socket.idFor(e),r=a.socket.idFor(e);t(r).toBe(s)})}),c("keeps idFor stable across a recycle",async a=>{await o(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){a.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const s={roomId:"room-1",roles:["admin"]},r=e.socket.accept(l(e),s),i=e.socket.idFor(r);e.simulateRecycle();const n=e.restoreSocket(i,s);t(e.socket.idFor(n)).toBe(i)})}),c("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4),await o(async a=>{const e=a.socket.accept(l(a),{},["room-a"]),s=a.socket.accept(l(a),{},["room-b"]),r=a.socket.accept(l(a),{}),i=n=>a.socket.idFor(n);t(a.socket.getSockets("room-a").map(i)).toStrictEqual([i(e)]),t(a.socket.getSockets("room-b").map(i)).toStrictEqual([i(s)]),t(a.socket.getSockets("room-c").map(i)).toStrictEqual([]),t(a.socket.getSockets().map(i)).toContain(i(r))})}),c("accepts the portable budget of nine caller tags",async()=>{t.assertions(9);const a=await p(),e=Array.from({length:9},(i,n)=>`tag-${String(n)}`),s=a.socket.accept(l(a),{},e),r=i=>a.socket.idFor(i);for(const i of e)t(a.socket.getSockets(i).map(r)).toStrictEqual([r(s)]);a.cleanup?.()}),c("resolves a raw socket back to its handle",async()=>{t.assertions(2),await o(async a=>{const e=l(a),s=a.socket.accept(e,{}),r=a.socket.handleFor(e);t(r!==void 0&&a.socket.idFor(r)).toBe(a.socket.idFor(s)),t(a.socket.handleFor(l(a))).toBeUndefined()})}),c("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1),await o(async a=>{const e=a.socket.accept(l(a),{}),{bufferedAmount:s}=e;t(s===void 0||typeof s=="number"&&s>=0).toBe(!0)})}),c("retags a live socket when the host declares mutable tags",async a=>{await o(async e=>{if(e.socket.setTag===void 0){a.skip(`${d} does not implement mutable socket tags (setTag)`);return}t.assertions(2);const s=e.socket.accept(l(e),{});e.socket.setTag(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([e.socket.idFor(s)]),e.socket.removeTag?.(s,"room-a"),t(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([])})})}),u("ShardDirectory",()=>{c("resolves shard keys deterministically",async()=>{t.assertions(1),await o(async a=>{const e=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/")),s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(e.text()).resolves.toBe(await s.text())})}),c("dispatches fetch to a resolved stub",async()=>{t.assertions(1),await o(async a=>{const s=await k(a.directory,"tenant-42").fetch(new Request("http://localhost/"));t(s).toBeInstanceOf(Response)})})}),u("SchedulerHost",()=>{c("schedules a job for a future timestamp",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(2);const s=Date.now(),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(r.scheduledFor).toBeGreaterThanOrEqual(s+50),t(r.id).toBeDefined()})}),c("dispatches a scheduled job at least once",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}const s=e.scheduler.deadLetter!==void 0;if(!s&&e.awaitJobDispatched===void 0){a.skip(`${d} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(s&&t(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const r=e.scheduler.list!==void 0;t.assertions(1+(s?1:0)+(r?1:0));const i=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await t(e.awaitJobDispatched(i.id)).resolves.toBe(!0),r){const n=await e.scheduler.list?.();t(n?.some(w=>w.id===i.id)).toBe(!1)}})}),c("cancels a scheduled job",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(1);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),r=await e.scheduler.cancel(s.id);t(r).toBe(!0)})}),c("reports a second cancel of the same job as false",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(s.id)).toBe(!1)})}),c("gives two identical schedules independently cancellable ids",async a=>{await o(async e=>{if(e.scheduler===void 0){a.skip(`${d} does not implement SchedulerHost`);return}t.assertions(3);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(r.id).not.toBe(s.id),t(await e.scheduler.cancel(s.id)).toBe(!0),t(await e.scheduler.cancel(r.id)).toBe(!0)})}),c("lists a pending job with a zero attempt count",async a=>{await o(async e=>{if(e.scheduler?.list===void 0){a.skip(`${d} does not implement SchedulerHost.list`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),i=(await e.scheduler.list()).find(n=>n.id===s.id);t(i?.functionPath).toBe("tasks/remind"),t(i?.attempts).toBe(0)})}),c("keeps the pending and dead-letter listings disjoint",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id);const r=await e.scheduler.list(),i=await e.scheduler.deadLetter.list();t(r.some(n=>n.id===s.id)).toBe(!1),t(i.some(n=>n.id===s.id)).toBe(!0)})}),c("returns a requeued job to the pending set with a fresh budget",async a=>{await o(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){a.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(4);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id),t(await e.scheduler.deadLetter.requeue(s.id)).toBe(!0);const i=(await e.scheduler.list()).find(w=>w.id===s.id);t(i).toBeDefined(),t(i?.attempts).toBe(0);const n=await e.scheduler.deadLetter.list();t(n.some(w=>w.id===s.id)).toBe(!1)})}),c("reports a requeue of an unparked job as false",async a=>{await o(async e=>{if(e.scheduler?.deadLetter===void 0){a.skip(`${d} does not implement scheduler.deadLetter`);return}t.assertions(1),t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})})}),u("ShardKvStore",()=>{c("reads back a written value",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:token-1",{userId:"ada"}),t(await e.kv.get("s:token-1")).toEqual({userId:"ada"}),t(await e.kv.get("s:missing")).toBeUndefined()})}),c("deletes a key idempotently",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(3),await e.kv.put("k",1),t(await e.kv.delete("k")).toBe(!0),t(await e.kv.delete("k")).toBe(!1),t(await e.kv.get("k")).toBeUndefined()})}),c("enumerates exactly the keys under a prefix",async a=>{await o(async e=>{if(e.kv===void 0){a.skip(`${d} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:a",1),await e.kv.put("s:b",2),await e.kv.put("other",3);const s=await e.kv.list({prefix:"s:"}),r=await e.kv.list();t([...s.keys()].toSorted((i,n)=>i.localeCompare(n))).toStrictEqual(["s:a","s:b"]),t(r.size).toBe(3)})})}),u("post-dispose",()=>{c("fails closed on every documented surface once the host is terminally disposed",async a=>{const e=await p();if(e.disposeTerminally===void 0){a.skip(`${d} has no terminal dispose the suite can drive from inside a test`);return}const{removeTag:s,setTag:r}=e.socket,{scheduler:i}=e,n=e.socket.accept(l(e),{}),w=l(e);e.disposeTerminally();const E=async m=>{await t(async()=>{await m()}).rejects.toThrow(f)},y=[()=>e.shard.alarms.set(Date.now()+1e3),()=>e.shard.alarms.delete(),()=>e.socket.accept(w,{}),...r===void 0?[]:[()=>{r(n,"room-a")}],...s===void 0?[]:[()=>{s(n,"room-a")}],...i===void 0?[]:[()=>i.schedule("tasks/remind",{},{delayMs:10})]];t.assertions(y.length);for(const m of y)await E(m)})})})};export{b as defineHostContractSuite};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as
|
|
1
|
+
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-dVPE86WP.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
|
|
4
4
|
* the framework mount seams rely on — `waitUntil` for fire-and-forget work that
|
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
|
|
1
|
+
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-dVPE86WP.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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DatabaseSync as I}from"node:sqlite";let k=0,j=0;const O=()=>(k+=1,`socket-${k}`),R=()=>(j+=1,`job-${j}`),$=a=>a===void 0?null:a,N=a=>typeof a=="string"?new TextEncoder().encode(a).buffer:a instanceof ArrayBuffer?a:ArrayBuffer.isView(a)?a.buffer.slice(a.byteOffset,a.byteOffset+a.byteLength):new ArrayBuffer(0),H=()=>{const a=new I(":memory:");let m=!1;const u=e=>{if(m)throw new Error(`platform closed: cannot ${e}`)},s={alarmAt:null,alarmTimeout:null,pending:[],running:!1},i=new Map,w=new Map,f=new Map,F={exec:(e,...t)=>{const n=a.prepare(e),r=t.map($),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]}}},p=()=>{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,p()}))},D=e=>new Promise((t,n)=>{s.pending.push({function_:e,reject:r=>{n(r)},resolve:r=>{t(r)}}),p()});let y=Promise.resolve();const b=async e=>{a.exec("BEGIN");try{const t=await e();return a.exec("COMMIT"),t}catch(t){throw a.exec("ROLLBACK"),t}},B=e=>{const t=y.then(()=>b(e),()=>b(e));return y=t.then(()=>{},()=>{}),t},C=e=>{const t=typeof e=="number"?e:e.getTime();s.alarmAt=t,s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);const n=Math.max(0,t-Date.now());s.alarmTimeout=setTimeout(()=>{s.alarmAt=null,s.alarmTimeout=null},n)},J={alarms:{delete:()=>{u("delete an alarm"),s.alarmAt=null,s.alarmTimeout!==null&&(clearTimeout(s.alarmTimeout),s.alarmTimeout=null)},get:()=>s.alarmAt,set:e=>{u("set an alarm"),C(e)}},runSerialized:D,sql:F,transaction:B,waitUntil:()=>{}},d=new WeakMap,v=e=>{const t={bufferedAmount:e.bufferedAmount,close:(n,r)=>{e.closed=!0},deserializeAttachment:()=>e.attachment,send:n=>{e.received.push(typeof n=="string"?n:N(n))},serializeAttachment:n=>{e.attachment=n,w.set(e.id,n)}};return e.handle=t,d.set(t,e.id),t},L={accept:(e,t,n)=>{u("accept a socket");const r=O(),c={attachment:t,bufferedAmount:0,closed:!1,raw:e,handle:null,id:r,received:[],tags:new Set(n)};return i.set(r,c),f.set(r,new Set(n)),t!==void 0&&w.set(r,t),v(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=d.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)=>{u("remove a socket tag");const n=d.get(e)??"",r=i.get(n);r!==void 0&&(t===void 0?r.tags.clear():r.tags.delete(t),f.set(n,new Set(r.tags)))},setTag:(e,t)=>{u("set a socket tag");const n=d.get(e)??"",r=i.get(n);r!==void 0&&(r.tags.add(t),f.set(n,new Set(r.tags)))}},T={get:e=>({fetch:async()=>new Response(String(e))}),getByName:e=>({fetch:async()=>new Response(e)}),idForName:e=>`shard:${e}`,jurisdiction:e=>T},h=new Map,P={delete:async e=>h.delete(e),get:async e=>h.get(e),list:async e=>{const t=e?.prefix??"",n=new Map;for(const[r,c]of h)r.startsWith(t)&&n.set(r,c);return n},put:async(e,t)=>{h.set(e,structuredClone(t))}},o=new Map,g=new Map,S=new Set,A=(e,t)=>({attempts:t.attempts,functionPath:t.functionPath,id:e,scheduledFor:t.scheduledFor}),z={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()=>[...g].map(([e,t])=>A(e,t)),requeue:async e=>{const t=g.get(e);return t===void 0?!1:(g.delete(e),o.set(e,{...t,attempts:0,timer:void 0}),!0)}},list:async()=>[...o].map(([e,t])=>A(e,t)),schedule:async(e,t,n)=>{u("schedule a job");const r=R();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()),E=setTimeout(()=>{const M=o.get(r);M!==void 0&&(M.attempts+=1),S.add(r),o.delete(r)},l);return o.set(r,{args:t,attempts:0,functionPath:e,options:n??{},scheduledFor:c,timer:E}),{id:r,scheduledFor:c}}},x=()=>{if(!m){m=!0,a.close(),s.alarmTimeout!==null&&clearTimeout(s.alarmTimeout);for(const e of o.values())clearTimeout(e.timer)}};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)}),S.has(e)},cleanup:x,directory:T,disposeTerminally:x,kv:P,readFrames:e=>(i.get(d.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(f.get(e))};return i.set(e,n),v(n)},scheduler:z,simulateDeadLetter:async e=>{const t=o.get(e);return t===void 0?!1:(clearTimeout(t.timer),o.delete(e),g.set(e,{...t,attempts:(t.options.retry?.maxAttempts??5)+1,timer:void 0}),!0)},shard:J,simulateRecycle:()=>{i.clear()},socket:L}};export{H as createReferenceHost};
|
|
@@ -374,17 +374,6 @@ interface ShardSqlExec {
|
|
|
374
374
|
/** Execute a SQL statement with optional bound parameters. */
|
|
375
375
|
exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
|
|
376
376
|
}
|
|
377
|
-
/**
|
|
378
|
-
* Async SQL executor used by the engine's higher-level paths (global tables,
|
|
379
|
-
* metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
|
|
380
|
-
* alias keeps the platform contract self-contained.
|
|
381
|
-
*/
|
|
382
|
-
interface ShardAsyncSqlExec {
|
|
383
|
-
all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
|
|
384
|
-
run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
|
|
385
|
-
rowsAffected: number;
|
|
386
|
-
}>;
|
|
387
|
-
}
|
|
388
377
|
/**
|
|
389
378
|
* Alarm scheduling for a shard. Alarms are durable: they survive host
|
|
390
379
|
* recycling and fire at the requested timestamp.
|
|
@@ -406,12 +395,6 @@ interface ShardAlarms {
|
|
|
406
395
|
interface ShardHost {
|
|
407
396
|
/** Durable alarm scheduling for the shard. */
|
|
408
397
|
alarms: ShardAlarms;
|
|
409
|
-
/**
|
|
410
|
-
* Async SQL executor for engine paths that need promise-based row access
|
|
411
|
-
* (global tables, metrics, auth). Hosts may implement this over the same
|
|
412
|
-
* underlying storage as `sql`.
|
|
413
|
-
*/
|
|
414
|
-
asyncSql?: ShardAsyncSqlExec;
|
|
415
398
|
/**
|
|
416
399
|
* Run `fn` with exclusive ownership of the shard. Concurrent calls are
|
|
417
400
|
* queued; no two closures run at once for the same shard key. On
|
|
@@ -639,4 +622,4 @@ interface SocketHost {
|
|
|
639
622
|
*/
|
|
640
623
|
setTag?: (socket: SocketHandle, tag: string) => void;
|
|
641
624
|
}
|
|
642
|
-
export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d,
|
|
625
|
+
export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardDirectory as e, ShardHost as f, ShardJurisdiction as g, ShardKvListOptions as h, ShardKvStore as i, ShardRegionHint as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
|
|
@@ -374,17 +374,6 @@ interface ShardSqlExec {
|
|
|
374
374
|
/** Execute a SQL statement with optional bound parameters. */
|
|
375
375
|
exec: <Row = SqlRow>(query: string, ...bindings: ReadonlyArray<unknown>) => ShardSqlCursor<Row>;
|
|
376
376
|
}
|
|
377
|
-
/**
|
|
378
|
-
* Async SQL executor used by the engine's higher-level paths (global tables,
|
|
379
|
-
* metrics, auth). Already defined in `@lunora/sql-store` as `SqlExec`; this
|
|
380
|
-
* alias keeps the platform contract self-contained.
|
|
381
|
-
*/
|
|
382
|
-
interface ShardAsyncSqlExec {
|
|
383
|
-
all: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRow[]>;
|
|
384
|
-
run: (sql: string, params: ReadonlyArray<unknown>) => Promise<{
|
|
385
|
-
rowsAffected: number;
|
|
386
|
-
}>;
|
|
387
|
-
}
|
|
388
377
|
/**
|
|
389
378
|
* Alarm scheduling for a shard. Alarms are durable: they survive host
|
|
390
379
|
* recycling and fire at the requested timestamp.
|
|
@@ -406,12 +395,6 @@ interface ShardAlarms {
|
|
|
406
395
|
interface ShardHost {
|
|
407
396
|
/** Durable alarm scheduling for the shard. */
|
|
408
397
|
alarms: ShardAlarms;
|
|
409
|
-
/**
|
|
410
|
-
* Async SQL executor for engine paths that need promise-based row access
|
|
411
|
-
* (global tables, metrics, auth). Hosts may implement this over the same
|
|
412
|
-
* underlying storage as `sql`.
|
|
413
|
-
*/
|
|
414
|
-
asyncSql?: ShardAsyncSqlExec;
|
|
415
398
|
/**
|
|
416
399
|
* Run `fn` with exclusive ownership of the shard. Concurrent calls are
|
|
417
400
|
* queued; no two closures run at once for the same shard key. On
|
|
@@ -639,4 +622,4 @@ interface SocketHost {
|
|
|
639
622
|
*/
|
|
640
623
|
setTag?: (socket: SocketHandle, tag: string) => void;
|
|
641
624
|
}
|
|
642
|
-
export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d,
|
|
625
|
+
export { DirectShardDirectory as D, ScheduleOptions as S, TwoStepShardDirectory as T, ScheduledJob as a, ScheduledJobStatus as b, SchedulerHost as c, ShardAlarms as d, ShardDirectory as e, ShardHost as f, ShardJurisdiction as g, ShardKvListOptions as h, ShardKvStore as i, ShardRegionHint as j, ShardSqlCursor as k, ShardSqlExec as l, ShardStub as m, SocketHandle as n, SocketHost as o, SqlRow as p, resolveShard as r };
|
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.14",
|
|
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
|
-
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};
|