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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
1
  export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.mjs";
2
- import "../packem_shared/socket-host.d-GABARI_Q.mjs";
2
+ import "../packem_shared/socket-host.d-Dn14vebI.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-GABARI_Q.js";
2
+ import "../packem_shared/socket-host.d-Dn14vebI.js";
@@ -1 +1 @@
1
- import{createReferenceHost as o}from"../packem_shared/createReferenceHost-fs0Q8_VA.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
1
+ import{createReferenceHost as o}from"../packem_shared/createReferenceHost-B9XE4Gpl.mjs";import{defineHostContractSuite as f}from"./suite.mjs";export{o as createReferenceHost,f as defineHostContractSuite};
@@ -1,4 +1,4 @@
1
- import { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-GABARI_Q.mjs";
1
+ import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-Dn14vebI.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.
@@ -30,8 +30,64 @@ interface ConformanceHost {
30
30
  * hosts that don't care.
31
31
  */
32
32
  createSocket?: () => unknown;
33
+ /**
34
+ * How many times this host has dispatched `functionPath` — the cron legs'
35
+ * only window into a schedule that has no job row to list.
36
+ *
37
+ * Optional, and only a host implementing {@link SchedulerHost.cron} needs
38
+ * it: without it the suite cannot tell a cron that ticked from one that was
39
+ * armed and never fired, which is the difference between a working schedule
40
+ * and a `setTimeout` that overflowed its 2^31-1 ms ceiling and fired
41
+ * immediately. Counting by function path rather than by id because a cron
42
+ * has no per-tick identity.
43
+ */
44
+ cronTicks?: (functionPath: string) => number;
33
45
  /** The shard directory under test. */
34
46
  directory: ShardDirectory;
47
+ /**
48
+ * Terminally dispose this host instance, as opposed to {@link
49
+ * ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
50
+ * `cleanup`) use as a per-test reset rather than a true teardown — the DO's
51
+ * storage has no explicit close a test can drive, so `cleanup` there just
52
+ * disarms the pending alarm and drops socket references for the next run.
53
+ *
54
+ * Optional: only a host with a real terminal dispose implements it. Where
55
+ * it exists, the suite calls it once and then asserts every surface that
56
+ * documents a post-close behaviour (`ShardHost.alarms`,
57
+ * `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
58
+ * closed with a `"platform closed: …"` error — the same "report the gap
59
+ * instead of asserting a false close" pattern `scheduler`/`kv` already use
60
+ * for hosts that don't implement a surface at all.
61
+ */
62
+ disposeTerminally?: () => void;
63
+ /**
64
+ * Declare that this host's concurrency boundary is the **dispatch**, not
65
+ * the SQL executor: the runtime refuses to deliver a second event to the
66
+ * shard while a mutation holds it, so two tasks never reach `shard.sql`
67
+ * concurrently in the first place.
68
+ *
69
+ * Cloudflare is the case. `runSerialized` is `blockConcurrencyWhile`, which
70
+ * closes the Durable Object's input gate; every other event — including
71
+ * timer continuations — is queued behind it until the mutation settles. Two
72
+ * consequences the TCK has to respect:
73
+ *
74
+ * - A read issued from inside the *same* event is not "a task outside the
75
+ * mutation" at all. It is the mutation's own task, sharing its
76
+ * `storage.transaction`, and it reads the uncommitted row. Measured
77
+ * against workerd: the row comes back.
78
+ * - A test cannot manufacture a second event either. The gate delivers
79
+ * queued continuations in scheduling order, so an outer `await sleep(n)`
80
+ * armed before the gate closed and due *earlier* than the mutation's own
81
+ * timer head-of-line blocks that timer — the closure never settles, the
82
+ * gate never opens, and the object deadlocks until the test times out.
83
+ *
84
+ * So on such a host the isolation leg asserts the half the adapter owns
85
+ * (nothing uncommitted survives the rollback) and reports the observation
86
+ * half as a gap, the same way {@link ConformanceHost.awaitAlarmFired}'s
87
+ * absence reports platform-owned alarm delivery. Enforcing the gate is
88
+ * workerd's test, not the adapter's.
89
+ */
90
+ isolatesByDispatch?: true;
35
91
  /**
36
92
  * The durable key-value store under test. Optional: a host that implements
37
93
  * only the reactive-engine half (`ShardHost`) has no KV surface to offer,
@@ -120,22 +176,5 @@ type VitestApi = {
120
176
  expect: typeof import("vitest").expect;
121
177
  it: typeof import("vitest").it;
122
178
  };
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
179
  declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
141
180
  export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
@@ -1,4 +1,4 @@
1
- import { f as ShardDirectory, j as ShardKvStore, n as SocketHandle, c as SchedulerHost, g as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-GABARI_Q.js";
1
+ import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-Dn14vebI.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.
@@ -30,8 +30,64 @@ interface ConformanceHost {
30
30
  * hosts that don't care.
31
31
  */
32
32
  createSocket?: () => unknown;
33
+ /**
34
+ * How many times this host has dispatched `functionPath` — the cron legs'
35
+ * only window into a schedule that has no job row to list.
36
+ *
37
+ * Optional, and only a host implementing {@link SchedulerHost.cron} needs
38
+ * it: without it the suite cannot tell a cron that ticked from one that was
39
+ * armed and never fired, which is the difference between a working schedule
40
+ * and a `setTimeout` that overflowed its 2^31-1 ms ceiling and fired
41
+ * immediately. Counting by function path rather than by id because a cron
42
+ * has no per-tick identity.
43
+ */
44
+ cronTicks?: (functionPath: string) => number;
33
45
  /** The shard directory under test. */
34
46
  directory: ShardDirectory;
47
+ /**
48
+ * Terminally dispose this host instance, as opposed to {@link
49
+ * ConformanceHost.cleanup}, which some hosts (Cloudflare's DO-backed
50
+ * `cleanup`) use as a per-test reset rather than a true teardown — the DO's
51
+ * storage has no explicit close a test can drive, so `cleanup` there just
52
+ * disarms the pending alarm and drops socket references for the next run.
53
+ *
54
+ * Optional: only a host with a real terminal dispose implements it. Where
55
+ * it exists, the suite calls it once and then asserts every surface that
56
+ * documents a post-close behaviour (`ShardHost.alarms`,
57
+ * `SchedulerHost.schedule`, `SocketHost.accept`/`setTag`/`removeTag`) fails
58
+ * closed with a `"platform closed: …"` error — the same "report the gap
59
+ * instead of asserting a false close" pattern `scheduler`/`kv` already use
60
+ * for hosts that don't implement a surface at all.
61
+ */
62
+ disposeTerminally?: () => void;
63
+ /**
64
+ * Declare that this host's concurrency boundary is the **dispatch**, not
65
+ * the SQL executor: the runtime refuses to deliver a second event to the
66
+ * shard while a mutation holds it, so two tasks never reach `shard.sql`
67
+ * concurrently in the first place.
68
+ *
69
+ * Cloudflare is the case. `runSerialized` is `blockConcurrencyWhile`, which
70
+ * closes the Durable Object's input gate; every other event — including
71
+ * timer continuations — is queued behind it until the mutation settles. Two
72
+ * consequences the TCK has to respect:
73
+ *
74
+ * - A read issued from inside the *same* event is not "a task outside the
75
+ * mutation" at all. It is the mutation's own task, sharing its
76
+ * `storage.transaction`, and it reads the uncommitted row. Measured
77
+ * against workerd: the row comes back.
78
+ * - A test cannot manufacture a second event either. The gate delivers
79
+ * queued continuations in scheduling order, so an outer `await sleep(n)`
80
+ * armed before the gate closed and due *earlier* than the mutation's own
81
+ * timer head-of-line blocks that timer — the closure never settles, the
82
+ * gate never opens, and the object deadlocks until the test times out.
83
+ *
84
+ * So on such a host the isolation leg asserts the half the adapter owns
85
+ * (nothing uncommitted survives the rollback) and reports the observation
86
+ * half as a gap, the same way {@link ConformanceHost.awaitAlarmFired}'s
87
+ * absence reports platform-owned alarm delivery. Enforcing the gate is
88
+ * workerd's test, not the adapter's.
89
+ */
90
+ isolatesByDispatch?: true;
35
91
  /**
36
92
  * The durable key-value store under test. Optional: a host that implements
37
93
  * only the reactive-engine half (`ShardHost`) has no KV surface to offer,
@@ -120,22 +176,5 @@ type VitestApi = {
120
176
  expect: typeof import("vitest").expect;
121
177
  it: typeof import("vitest").it;
122
178
  };
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
179
  declare const defineHostContractSuite: (name: string, factory: ConformanceHostFactory, vitest: VitestApi) => void;
141
180
  export { ConformanceHost as C, ReferenceHost as R, type VitestApi, ConformanceHostFactory as a, createReferenceHost as c, defineHostContractSuite };
@@ -1 +1 @@
1
- import{resolveShard as h}from"../packem_shared/resolveShard-uGhAKuTB.mjs";const E=(n,w,k)=>{const{describe:u,expect:t,it:i}=k;u(`host contract: ${n}`,()=>{const m=async()=>w(),l=s=>s.createSocket?.()??{},c=async s=>{const e=await m();try{await s(e)}finally{e.cleanup?.()}};u("ShardHost",()=>{i("serializes mutations so no two closures interleave",async()=>{t.assertions(1),await c(async s=>{const e=[];await Promise.all([s.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(r=>{setTimeout(r,10)}),e.push("a-end")}),s.shard.runSerialized(async()=>{e.push("b-start"),await new Promise(r=>{setTimeout(r,5)}),e.push("b-end")})]);const a=e.join("").includes("a-starta-end"),o=e.join("").includes("b-startb-end");t(a&&o).toBe(!0)})}),i("rolls back a transaction that throws",async()=>{t.assertions(2),await c(async s=>{await s.shard.transaction(async()=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),s.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await t(s.shard.transaction(async()=>{throw s.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=s.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();t(e).toHaveLength(0)})}),i("observes its own writes inside a transaction",async()=>{t.assertions(1),await c(async s=>{const e=await s.shard.transaction(async()=>(s.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),s.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),s.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));t(e).toBe("hello")})}),i("keeps overlapping transactions atomic",async()=>{t.assertions(2),await c(async s=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=s.shard.transaction(async()=>{s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(r=>{setTimeout(r,20)}),s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),a=s.shard.transaction(async()=>{s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,a]);const o=s.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();t(o.map(r=>r.id)).toStrictEqual(["A","B","C"]),await t(s.shard.transaction(async()=>{throw s.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),i("returns a cursor that buffers, yields one row, and iterates",async()=>{t.assertions(3),await c(async s=>{await s.shard.transaction(async()=>{s.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),s.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),s.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),t(s.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),t(s.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),t([...s.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),i("reports a pending alarm, and clears it once fired",async()=>{t.assertions(2),await c(async s=>{const e=Date.now()+50;if(await s.shard.alarms.set(e),t(await s.shard.alarms.get()).toBe(e),s.awaitAlarmFired===void 0){t(await s.shard.alarms.get()).toBe(e);return}await s.awaitAlarmFired(e),t(await s.shard.alarms.get()).toBeNull()})}),i("deletes a pending alarm",async()=>{t.assertions(1),await c(async s=>{await s.shard.alarms.set(Date.now()+1e4),await s.shard.alarms.delete(),t(await s.shard.alarms.get()).toBeNull()})})}),u("SocketHost",()=>{i("accepts a socket and can send/close",async()=>{await c(async s=>{const e=s.socket.accept(l(s),{user:"ada"});t(s.socket.idFor(e)).toBeDefined(),e.send("hello"),t(s.socket.getSockets().map(a=>s.socket.idFor(a))).toContain(s.socket.idFor(e)),s.readFrames!==void 0&&t(s.readFrames(e)).toStrictEqual(["hello"]),t(()=>{e.close(1e3,"done")}).not.toThrow()})}),i("round-trips an attachment on a live socket",async()=>{t.assertions(1),await c(async s=>{const e={roomId:"room-1",roles:["admin"]},a=s.socket.accept(l(s),e);t(a.deserializeAttachment()).toEqual(e)})}),i("round-trips attachments across a recycle",async s=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){s.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const a={roomId:"room-1",roles:["admin"]},o=e.socket.accept(l(e),a);e.simulateRecycle();const r=e.restoreSocket(e.socket.idFor(o),a);t(r.deserializeAttachment()).toEqual(a)})}),i("keeps idFor stable across repeated calls within a wake",async()=>{t.assertions(1),await c(async s=>{const e=s.socket.accept(l(s),{}),a=s.socket.idFor(e),o=s.socket.idFor(e);t(o).toBe(a)})}),i("keeps idFor stable across a recycle",async s=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){s.skip(`${n} does not implement simulateRecycle/restoreSocket`);return}t.assertions(1);const a={roomId:"room-1",roles:["admin"]},o=e.socket.accept(l(e),a),r=e.socket.idFor(o);e.simulateRecycle();const d=e.restoreSocket(r,a);t(e.socket.idFor(d)).toBe(r)})}),i("returns exactly the sockets carrying an accept-time tag",async()=>{t.assertions(4),await c(async s=>{const e=s.socket.accept(l(s),{},["room-a"]),a=s.socket.accept(l(s),{},["room-b"]),o=s.socket.accept(l(s),{}),r=d=>s.socket.idFor(d);t(s.socket.getSockets("room-a").map(r)).toStrictEqual([r(e)]),t(s.socket.getSockets("room-b").map(r)).toStrictEqual([r(a)]),t(s.socket.getSockets("room-c").map(r)).toStrictEqual([]),t(s.socket.getSockets().map(r)).toContain(r(o))})}),i("accepts the portable budget of nine caller tags",async()=>{t.assertions(9);const s=await m(),e=Array.from({length:9},(r,d)=>`tag-${String(d)}`),a=s.socket.accept(l(s),{},e),o=r=>s.socket.idFor(r);for(const r of e)t(s.socket.getSockets(r).map(o)).toStrictEqual([o(a)]);s.cleanup?.()}),i("resolves a raw socket back to its handle",async()=>{t.assertions(2),await c(async s=>{const e=l(s),a=s.socket.accept(e,{}),o=s.socket.handleFor(e);t(o!==void 0&&s.socket.idFor(o)).toBe(s.socket.idFor(a)),t(s.socket.handleFor(l(s))).toBeUndefined()})}),i("reports a plausible outbound queue depth, if any",async()=>{t.assertions(1),await c(async s=>{const e=s.socket.accept(l(s),{}),{bufferedAmount:a}=e;t(a===void 0||typeof a=="number"&&a>=0).toBe(!0)})}),i("retags a live socket when the host declares mutable tags",async s=>{await c(async e=>{if(e.socket.setTag===void 0){s.skip(`${n} does not implement mutable socket tags (setTag)`);return}t.assertions(2);const a=e.socket.accept(l(e),{});e.socket.setTag(a,"room-a"),t(e.socket.getSockets("room-a").map(o=>e.socket.idFor(o))).toStrictEqual([e.socket.idFor(a)]),e.socket.removeTag?.(a,"room-a"),t(e.socket.getSockets("room-a").map(o=>e.socket.idFor(o))).toStrictEqual([])})})}),u("ShardDirectory",()=>{i("resolves shard keys deterministically",async()=>{t.assertions(1),await c(async s=>{const e=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/")),a=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/"));await t(e.text()).resolves.toBe(await a.text())})}),i("dispatches fetch to a resolved stub",async()=>{t.assertions(1),await c(async s=>{const e=await h(s.directory,"tenant-42").fetch(new Request("http://localhost/"));t(e).toBeInstanceOf(Response)})})}),u("SchedulerHost",()=>{i("schedules a job for a future timestamp",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);const a=Date.now(),o=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});t(o.scheduledFor).toBeGreaterThanOrEqual(a+50),t(o.id).toBeDefined()})}),i("dispatches a scheduled job at least once",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}const a=e.scheduler.deadLetter!==void 0;if(!a&&e.awaitJobDispatched===void 0){s.skip(`${n} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(a&&t(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const o=e.scheduler.list!==void 0;t.assertions(1+(a?1:0)+(o?1:0));const r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await t(e.awaitJobDispatched(r.id)).resolves.toBe(!0),o){const d=await e.scheduler.list?.();t(d?.some(p=>p.id===r.id)).toBe(!1)}})}),i("cancels a scheduled job",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(1);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),o=await e.scheduler.cancel(a.id);t(o).toBe(!0)})}),i("reports a second cancel of the same job as false",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(2);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});t(await e.scheduler.cancel(a.id)).toBe(!0),t(await e.scheduler.cancel(a.id)).toBe(!1)})}),i("gives two identical schedules independently cancellable ids",async s=>{await c(async e=>{if(e.scheduler===void 0){s.skip(`${n} does not implement SchedulerHost`);return}t.assertions(3);const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),o=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4});t(o.id).not.toBe(a.id),t(await e.scheduler.cancel(a.id)).toBe(!0),t(await e.scheduler.cancel(o.id)).toBe(!0)})}),i("lists a pending job with a zero attempt count",async s=>{await c(async e=>{if(e.scheduler?.list===void 0){s.skip(`${n} does not implement SchedulerHost.list`);return}t.assertions(2);const a=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:1e4}),o=(await e.scheduler.list()).find(r=>r.id===a.id);t(o?.functionPath).toBe("tasks/remind"),t(o?.attempts).toBe(0)})}),i("keeps the pending and dead-letter listings disjoint",async s=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){s.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(2);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(a.id);const o=await e.scheduler.list(),r=await e.scheduler.deadLetter.list();t(o.some(d=>d.id===a.id)).toBe(!1),t(r.some(d=>d.id===a.id)).toBe(!0)})}),i("returns a requeued job to the pending set with a fresh budget",async s=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){s.skip(`${n} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}t.assertions(4);const a=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(a.id),t(await e.scheduler.deadLetter.requeue(a.id)).toBe(!0);const o=(await e.scheduler.list()).find(d=>d.id===a.id);t(o).toBeDefined(),t(o?.attempts).toBe(0);const r=await e.scheduler.deadLetter.list();t(r.some(d=>d.id===a.id)).toBe(!1)})}),i("reports a requeue of an unparked job as false",async s=>{await c(async e=>{if(e.scheduler?.deadLetter===void 0){s.skip(`${n} does not implement scheduler.deadLetter`);return}t.assertions(1),t(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})})}),u("ShardKvStore",()=>{i("reads back a written value",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:token-1",{userId:"ada"}),t(await e.kv.get("s:token-1")).toEqual({userId:"ada"}),t(await e.kv.get("s:missing")).toBeUndefined()})}),i("deletes a key idempotently",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} does not implement ShardKvStore`);return}t.assertions(3),await e.kv.put("k",1),t(await e.kv.delete("k")).toBe(!0),t(await e.kv.delete("k")).toBe(!1),t(await e.kv.get("k")).toBeUndefined()})}),i("enumerates exactly the keys under a prefix",async s=>{await c(async e=>{if(e.kv===void 0){s.skip(`${n} does not implement ShardKvStore`);return}t.assertions(2),await e.kv.put("s:a",1),await e.kv.put("s:b",2),await e.kv.put("other",3);const a=await e.kv.list({prefix:"s:"}),o=await e.kv.list();t([...a.keys()].toSorted((r,d)=>r.localeCompare(d))).toStrictEqual(["s:a","s:b"]),t(o.size).toBe(3)})})})})};export{E as defineHostContractSuite};
1
+ import{resolveShard as E}from"../packem_shared/resolveShard-BzKOUEO4.mjs";const f=/platform closed/u,h=d=>new Promise(k=>{setTimeout(k,d)}),b=(d,k,v)=>{const{describe:w,expect:a,it:o}=v;w(`host contract: ${d}`,()=>{const p=async()=>k(),l=t=>t.createSocket?.()??{},c=async t=>{const e=await p();try{await t(e)}finally{e.cleanup?.()}};w("ShardHost",()=>{o("serializes mutations so no two closures interleave",async()=>{a.assertions(1),await c(async t=>{const e=[];await Promise.all([t.shard.runSerialized(async()=>{e.push("a-start"),await new Promise(i=>{setTimeout(i,10)}),e.push("a-end")}),t.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");a(s&&r).toBe(!0)})}),o("rolls back a transaction that throws",async()=>{a.assertions(2),await c(async t=>{await t.shard.transaction(async()=>{t.shard.sql.exec("CREATE TABLE IF NOT EXISTS rollback_test (id INTEGER PRIMARY KEY)"),t.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (1)")}),await a(t.shard.transaction(async()=>{throw t.shard.sql.exec("INSERT INTO rollback_test (id) VALUES (2)"),new Error("boom")})).rejects.toThrow("boom");const e=t.shard.sql.exec("SELECT id FROM rollback_test WHERE id = 2").toArray();a(e).toHaveLength(0)})}),o("never lets a task outside a mutation observe its uncommitted writes",async t=>{a.hasAssertions(),await c(async e=>{if(e.isolatesByDispatch===!0){t.skip(`${d} isolates concurrent tasks at the dispatch boundary, which no in-isolate read can stand in for`);return}e.shard.sql.exec("CREATE TABLE IF NOT EXISTS isolation_test (id INTEGER PRIMARY KEY)");let s;const r=new Promise(m=>{s=m}),i=e.shard.runSerialized(async()=>e.shard.transaction(async()=>{throw e.shard.sql.exec("INSERT INTO isolation_test (id) VALUES (1)"),s(),await h(20),new Error("boom")}));await r;let n,u;try{n=e.shard.sql.exec("SELECT id FROM isolation_test").toArray()}catch(m){n=[],u=m}a(n).toStrictEqual([]),u!==void 0&&a(u).toMatchObject({code:"SHARD_UNAVAILABLE",status:503,type:"VisulimaError"}),await a(i).rejects.toThrow("boom"),a(e.shard.sql.exec("SELECT id FROM isolation_test").toArray()).toStrictEqual([])})}),o("rejects with the value the closure threw, and stays usable",async()=>{a.assertions(3),await c(async t=>{const e=Object.assign(new Error("closure failed"),{code:"NOT_FOUND",status:404});await a(t.shard.transaction(()=>Promise.reject(e))).rejects.toBe(e),await a(t.shard.runSerialized(()=>Promise.reject(e))).rejects.toBe(e),await a(t.shard.runSerialized(async()=>t.shard.transaction(async()=>"still here"))).resolves.toBe("still here")})}),o("observes its own writes inside a transaction",async()=>{a.assertions(1),await c(async t=>{const e=await t.shard.transaction(async()=>(t.shard.sql.exec("CREATE TABLE IF NOT EXISTS ryw_test (id INTEGER PRIMARY KEY, value TEXT)"),t.shard.sql.exec("INSERT INTO ryw_test (id, value) VALUES (1, 'hello')"),t.shard.sql.exec("SELECT value FROM ryw_test WHERE id = 1").toArray()[0]?.value));a(e).toBe("hello")})}),o("keeps overlapping transactions atomic",async()=>{a.assertions(2),await c(async t=>{t.shard.sql.exec("CREATE TABLE IF NOT EXISTS overlap_test (id TEXT PRIMARY KEY)");const e=t.shard.transaction(async()=>{t.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('A')"),await new Promise(i=>{setTimeout(i,20)}),t.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('B')")}),s=t.shard.transaction(async()=>{t.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('C')")});await Promise.all([e,s]);const r=t.shard.sql.exec("SELECT id FROM overlap_test ORDER BY id").toArray();a(r.map(i=>i.id)).toStrictEqual(["A","B","C"]),await a(t.shard.transaction(async()=>{throw t.shard.sql.exec("INSERT INTO overlap_test (id) VALUES ('D')"),new Error("boom")})).rejects.toThrow("boom")})}),o("returns a cursor that buffers, yields one row, and iterates",async()=>{a.assertions(3),await c(async t=>{await t.shard.transaction(async()=>{t.shard.sql.exec("CREATE TABLE IF NOT EXISTS cursor_test (id INTEGER PRIMARY KEY)"),t.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (1)"),t.shard.sql.exec("INSERT INTO cursor_test (id) VALUES (2)")}),a(t.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id").toArray()).toHaveLength(2),a(t.shard.sql.exec("SELECT id FROM cursor_test WHERE id = 1").one()).toEqual({id:1}),a([...t.shard.sql.exec("SELECT id FROM cursor_test ORDER BY id")]).toHaveLength(2)})}),o("reports a pending alarm, and clears it once fired",async()=>{a.assertions(2),await c(async t=>{const e=Date.now()+50;if(await t.shard.alarms.set(e),a(await t.shard.alarms.get()).toBe(e),t.awaitAlarmFired===void 0){a(await t.shard.alarms.get()).toBe(e);return}await t.awaitAlarmFired(e),a(await t.shard.alarms.get()).toBeNull()})}),o("deletes a pending alarm",async()=>{a.assertions(1),await c(async t=>{await t.shard.alarms.set(Date.now()+1e4),await t.shard.alarms.delete(),a(await t.shard.alarms.get()).toBeNull()})})}),w("SocketHost",()=>{o("accepts a socket and can send/close",async()=>{await c(async t=>{const e=t.socket.accept(l(t),{user:"ada"});a(t.socket.idFor(e)).toBeDefined(),e.send("hello"),a(t.socket.getSockets().map(s=>t.socket.idFor(s))).toContain(t.socket.idFor(e)),t.readFrames!==void 0&&a(t.readFrames(e)).toStrictEqual(["hello"]),a(()=>{e.close(1e3,"done")}).not.toThrow()})}),o("round-trips an attachment on a live socket",async()=>{a.assertions(1),await c(async t=>{const e={roomId:"room-1",roles:["admin"]},s=t.socket.accept(l(t),e);a(s.deserializeAttachment()).toEqual(e)})}),o("round-trips attachments across a recycle",async t=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){t.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}a.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);a(i.deserializeAttachment()).toEqual(s)})}),o("keeps idFor stable across repeated calls within a wake",async()=>{a.assertions(1),await c(async t=>{const e=t.socket.accept(l(t),{}),s=t.socket.idFor(e),r=t.socket.idFor(e);a(r).toBe(s)})}),o("keeps idFor stable across a recycle",async t=>{await c(async e=>{if(e.simulateRecycle===void 0||e.restoreSocket===void 0){t.skip(`${d} does not implement simulateRecycle/restoreSocket`);return}a.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);a(e.socket.idFor(n)).toBe(i)})}),o("returns exactly the sockets carrying an accept-time tag",async()=>{a.assertions(4),await c(async t=>{const e=t.socket.accept(l(t),{},["room-a"]),s=t.socket.accept(l(t),{},["room-b"]),r=t.socket.accept(l(t),{}),i=n=>t.socket.idFor(n);a(t.socket.getSockets("room-a").map(i)).toStrictEqual([i(e)]),a(t.socket.getSockets("room-b").map(i)).toStrictEqual([i(s)]),a(t.socket.getSockets("room-c").map(i)).toStrictEqual([]),a(t.socket.getSockets().map(i)).toContain(i(r))})}),o("accepts the portable budget of nine caller tags",async()=>{a.assertions(9);const t=await p(),e=Array.from({length:9},(i,n)=>`tag-${String(n)}`),s=t.socket.accept(l(t),{},e),r=i=>t.socket.idFor(i);for(const i of e)a(t.socket.getSockets(i).map(r)).toStrictEqual([r(s)]);t.cleanup?.()}),o("resolves a raw socket back to its handle",async()=>{a.assertions(2),await c(async t=>{const e=l(t),s=t.socket.accept(e,{}),r=t.socket.handleFor(e);a(r!==void 0&&t.socket.idFor(r)).toBe(t.socket.idFor(s)),a(t.socket.handleFor(l(t))).toBeUndefined()})}),o("reports a plausible outbound queue depth, if any",async()=>{a.assertions(1),await c(async t=>{const e=t.socket.accept(l(t),{}),{bufferedAmount:s}=e;a(s===void 0||typeof s=="number"&&s>=0).toBe(!0)})}),o("retags a live socket when the host declares mutable tags",async t=>{await c(async e=>{if(e.socket.setTag===void 0){t.skip(`${d} does not implement mutable socket tags (setTag)`);return}a.assertions(2);const s=e.socket.accept(l(e),{});e.socket.setTag(s,"room-a"),a(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([e.socket.idFor(s)]),e.socket.removeTag?.(s,"room-a"),a(e.socket.getSockets("room-a").map(r=>e.socket.idFor(r))).toStrictEqual([])})})}),w("ShardDirectory",()=>{o("resolves shard keys deterministically",async()=>{a.assertions(1),await c(async t=>{const e=await E(t.directory,"tenant-42").fetch(new Request("http://localhost/")),s=await E(t.directory,"tenant-42").fetch(new Request("http://localhost/"));await a(e.text()).resolves.toBe(await s.text())})}),o("dispatches fetch to a resolved stub",async()=>{a.assertions(1),await c(async t=>{const s=await E(t.directory,"tenant-42").fetch(new Request("http://localhost/"));a(s).toBeInstanceOf(Response)})})}),w("SchedulerHost",()=>{o("schedules a job for a future timestamp",async t=>{await c(async e=>{if(e.scheduler===void 0){t.skip(`${d} does not implement SchedulerHost`);return}a.assertions(2);const s=Date.now(),r=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:50});a(r.scheduledFor).toBeGreaterThanOrEqual(s+50),a(r.id).toBeDefined()})}),o("dispatches a scheduled job at least once",async t=>{await c(async e=>{if(e.scheduler===void 0){t.skip(`${d} does not implement SchedulerHost`);return}const s=e.scheduler.deadLetter!==void 0;if(!s&&e.awaitJobDispatched===void 0){t.skip(`${d} does not claim at-least-once delivery (no deadLetter) and supplies no awaitJobDispatched hook`);return}if(s&&a(e.awaitJobDispatched).toBeDefined(),e.awaitJobDispatched===void 0)return;const r=e.scheduler.list!==void 0;a.assertions(1+(s?1:0)+(r?1:0));const i=await e.scheduler.schedule("tasks/remind",{user:"ada"},{delayMs:30});if(await a(e.awaitJobDispatched(i.id)).resolves.toBe(!0),r){const n=await e.scheduler.list?.();a(n?.some(u=>u.id===i.id)).toBe(!1)}})}),o("cancels a scheduled job",async t=>{await c(async e=>{if(e.scheduler===void 0){t.skip(`${d} does not implement SchedulerHost`);return}a.assertions(1);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4}),r=await e.scheduler.cancel(s.id);a(r).toBe(!0)})}),o("reports a second cancel of the same job as false",async t=>{await c(async e=>{if(e.scheduler===void 0){t.skip(`${d} does not implement SchedulerHost`);return}a.assertions(2);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});a(await e.scheduler.cancel(s.id)).toBe(!0),a(await e.scheduler.cancel(s.id)).toBe(!1)})}),o("gives two identical schedules independently cancellable ids",async t=>{await c(async e=>{if(e.scheduler===void 0){t.skip(`${d} does not implement SchedulerHost`);return}a.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});a(r.id).not.toBe(s.id),a(await e.scheduler.cancel(s.id)).toBe(!0),a(await e.scheduler.cancel(r.id)).toBe(!0)})}),o("lists a pending job with a zero attempt count",async t=>{await c(async e=>{if(e.scheduler?.list===void 0){t.skip(`${d} does not implement SchedulerHost.list`);return}a.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);a(i?.functionPath).toBe("tasks/remind"),a(i?.attempts).toBe(0)})}),o("keeps the pending and dead-letter listings disjoint",async t=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){t.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}a.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();a(r.some(n=>n.id===s.id)).toBe(!1),a(i.some(n=>n.id===s.id)).toBe(!0)})}),o("returns a requeued job to the pending set with a fresh budget",async t=>{await c(async e=>{if(e.scheduler?.list===void 0||e.scheduler.deadLetter===void 0||e.simulateDeadLetter===void 0){t.skip(`${d} does not implement scheduler.list/deadLetter/simulateDeadLetter`);return}a.assertions(4);const s=await e.scheduler.schedule("tasks/remind",{},{delayMs:1e4});await e.simulateDeadLetter(s.id),a(await e.scheduler.deadLetter.requeue(s.id)).toBe(!0);const i=(await e.scheduler.list()).find(u=>u.id===s.id);a(i).toBeDefined(),a(i?.attempts).toBe(0);const n=await e.scheduler.deadLetter.list();a(n.some(u=>u.id===s.id)).toBe(!1)})}),o("reports a requeue of an unparked job as false",async t=>{await c(async e=>{if(e.scheduler?.deadLetter===void 0){t.skip(`${d} does not implement scheduler.deadLetter`);return}a.assertions(1),a(await e.scheduler.deadLetter.requeue("job-does-not-exist")).toBe(!1)})}),o("ticks a cron on schedule, and not before its next occurrence",async t=>{await c(async e=>{if(e.scheduler?.cron===void 0){t.skip(`${d} does not implement SchedulerHost.cron`);return}const{cronTicks:s}=e;if(s===void 0){a.fail(`${d} declares SchedulerHost.cron but no cronTicks — presence of cron is the claim that dynamic cron works, so it must be observable`);return}a.assertions(2),await e.scheduler.cron("* * * * * *","tasks/tick");const r=(new Date().getMonth()+6)%12+1;await e.scheduler.cron(`0 0 1 ${String(r)} *`,"tasks/far"),await new Promise(i=>{setTimeout(i,1200)}),a(s("tasks/tick")).toBeGreaterThanOrEqual(1),a(s("tasks/far")).toBe(0)})})}),w("ShardKvStore",()=>{o("reads back a written value",async t=>{await c(async e=>{if(e.kv===void 0){t.skip(`${d} does not implement ShardKvStore`);return}a.assertions(2),await e.kv.put("s:token-1",{userId:"ada"}),a(await e.kv.get("s:token-1")).toEqual({userId:"ada"}),a(await e.kv.get("s:missing")).toBeUndefined()})}),o("deletes a key idempotently",async t=>{await c(async e=>{if(e.kv===void 0){t.skip(`${d} does not implement ShardKvStore`);return}a.assertions(3),await e.kv.put("k",1),a(await e.kv.delete("k")).toBe(!0),a(await e.kv.delete("k")).toBe(!1),a(await e.kv.get("k")).toBeUndefined()})}),o("enumerates exactly the keys under a prefix",async t=>{await c(async e=>{if(e.kv===void 0){t.skip(`${d} does not implement ShardKvStore`);return}a.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();a([...s.keys()].toSorted((i,n)=>i.localeCompare(n))).toStrictEqual(["s:a","s:b"]),a(r.size).toBe(3)})})}),w("post-dispose",()=>{o("fails closed on every documented surface once the host is terminally disposed",async t=>{const e=await p();if(e.disposeTerminally===void 0){t.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),{}),u=l(e);e.disposeTerminally();const m=async y=>{await a(async()=>{await y()}).rejects.toThrow(f)},S=[()=>e.shard.alarms.set(Date.now()+1e3),()=>e.shard.alarms.delete(),()=>e.socket.accept(u,{}),...r===void 0?[]:[()=>{r(n,"room-a")}],...s===void 0?[]:[()=>{s(n,"room-a")}],...i===void 0?[]:[()=>i.schedule("tasks/remind",{},{delayMs:10})]];a.assertions(S.length);for(const y of S)await m(y)})})})};export{b as defineHostContractSuite};