@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.
- 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 +57 -18
- package/dist/conformance/suite.d.ts +57 -18
- package/dist/conformance/suite.mjs +1 -1
- package/dist/index.d.mts +453 -52
- package/dist/index.d.ts +453 -52
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-CeHBgVVq.mjs +1 -0
- package/dist/packem_shared/createReferenceHost-B9XE4Gpl.mjs +1 -0
- package/dist/packem_shared/resolveShard-BzKOUEO4.mjs +1 -0
- package/dist/packem_shared/{socket-host.d-GABARI_Q.d.ts → socket-host.d-Dn14vebI.d.mts} +121 -43
- package/dist/packem_shared/{socket-host.d-GABARI_Q.d.mts → socket-host.d-Dn14vebI.d.ts} +121 -43
- package/package.json +1 -1
- package/dist/packem_shared/CLOUDFLARE_CAPABILITIES-DMLgo_TI.mjs +0 -1
- package/dist/packem_shared/createReferenceHost-fs0Q8_VA.mjs +0 -1
- package/dist/packem_shared/resolveShard-uGhAKuTB.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-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-
|
|
2
|
+
import "../packem_shared/socket-host.d-Dn14vebI.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createReferenceHost as o}from"../packem_shared/createReferenceHost-
|
|
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 {
|
|
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 {
|
|
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};
|