@mooloo-ai/eutaxy 0.0.0-stage → 0.2.0
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/LICENSE +202 -0
- package/README.md +16 -2
- package/dist/aws.d.mts +15464 -0
- package/dist/aws.d.mts.map +1 -0
- package/dist/aws.mjs +24057 -0
- package/dist/aws.mjs.map +1 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +17 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/checksum-DhS6ofSm.mjs +6049 -0
- package/dist/checksum-DhS6ofSm.mjs.map +1 -0
- package/dist/cli-DnPkIL8h.mjs +2 -0
- package/dist/cli.d.mts +8 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +2 -0
- package/dist/cloudflare.d.mts +141 -0
- package/dist/cloudflare.d.mts.map +1 -0
- package/dist/cloudflare.mjs +847 -0
- package/dist/cloudflare.mjs.map +1 -0
- package/dist/es5-DtZuPEgt.mjs +1721 -0
- package/dist/es5-DtZuPEgt.mjs.map +1 -0
- package/dist/event-streams-BANyO9xt.mjs +801 -0
- package/dist/event-streams-BANyO9xt.mjs.map +1 -0
- package/dist/explore-C5fDFOoY.d.mts +63 -0
- package/dist/explore-C5fDFOoY.d.mts.map +1 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/platform-CGVkTa16.d.mts +267 -0
- package/dist/platform-CGVkTa16.d.mts.map +1 -0
- package/dist/rolldown-runtime-C1I3wEg2.mjs +43 -0
- package/dist/src-B66we2mG.mjs +311 -0
- package/dist/src-B66we2mG.mjs.map +1 -0
- package/dist/src-BP6Iqc_9.mjs +117 -0
- package/dist/src-BP6Iqc_9.mjs.map +1 -0
- package/dist/src-BrCsF5RZ.mjs +1286 -0
- package/dist/src-BrCsF5RZ.mjs.map +1 -0
- package/dist/src-s6uYyC0B.mjs +89 -0
- package/dist/src-s6uYyC0B.mjs.map +1 -0
- package/dist/verdict-evEM5Gxx.mjs +12 -0
- package/dist/verdict-evEM5Gxx.mjs.map +1 -0
- package/package.json +76 -3
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import { i as canonicalJson } from "./src-s6uYyC0B.mjs";
|
|
2
|
+
import { a as Run, d as validateScenario, i as CrashSignal, l as settleOn, m as getPath, o as installScriptedFetch, s as patchGlobals, t as explore, u as defaultBounds } from "./src-BrCsF5RZ.mjs";
|
|
3
|
+
import { t as describeVerdict } from "./verdict-evEM5Gxx.mjs";
|
|
4
|
+
import { n as cloudflareRegressionTest } from "./src-B66we2mG.mjs";
|
|
5
|
+
// packages/cloudflare/src/check.ts
|
|
6
|
+
/** Explore a scenario on a platform and return its verdict. */
|
|
7
|
+
function check(spec, platform, options = { seed: spec.name }) {
|
|
8
|
+
return explore(spec, platform, options);
|
|
9
|
+
}
|
|
10
|
+
/** For use from a test: passes only when the scenario is BOUNDED. */
|
|
11
|
+
async function expectHolds(spec, platform, options) {
|
|
12
|
+
const verdict = await check(spec, platform, options);
|
|
13
|
+
if (verdict.kind !== "BOUNDED") throw new Error(describeVerdict(verdict));
|
|
14
|
+
}
|
|
15
|
+
// packages/cloudflare/src/json.ts
|
|
16
|
+
/** Reduce any runtime value to JSON data for the effect log; never throws. */
|
|
17
|
+
function toJson(value, depth = 0) {
|
|
18
|
+
if (value === null || value === void 0) return null;
|
|
19
|
+
switch (typeof value) {
|
|
20
|
+
case "boolean":
|
|
21
|
+
case "string": return value;
|
|
22
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
23
|
+
case "bigint": return value.toString();
|
|
24
|
+
case "object": {
|
|
25
|
+
if (depth > 8) return "<nested>";
|
|
26
|
+
if (Array.isArray(value)) return value.map((v) => toJson(v, depth + 1));
|
|
27
|
+
if (value instanceof ArrayBuffer) return `<bytes ${value.byteLength}>`;
|
|
28
|
+
if (ArrayBuffer.isView(value)) return `<bytes ${value.byteLength}>`;
|
|
29
|
+
if (value instanceof Date) return value.toISOString();
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const k of Object.keys(value).sort()) out[k] = toJson(value[k], depth + 1);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
default: return `<${typeof value}>`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// packages/cloudflare/src/d1.ts
|
|
38
|
+
/**
|
|
39
|
+
* The D1 binding proxy. Every statement execution is an effect point: the
|
|
40
|
+
* engine decides the fault, the real Miniflare-backed D1 performs the query,
|
|
41
|
+
* and the log records call, result and fault in the documented shape.
|
|
42
|
+
*/
|
|
43
|
+
const WRITE = [
|
|
44
|
+
"crash_before",
|
|
45
|
+
"crash_after",
|
|
46
|
+
"ambiguous",
|
|
47
|
+
"error"
|
|
48
|
+
];
|
|
49
|
+
const READ = [
|
|
50
|
+
"crash_before",
|
|
51
|
+
"crash_after",
|
|
52
|
+
"error"
|
|
53
|
+
];
|
|
54
|
+
function point(ctx, target, op, args, allowed) {
|
|
55
|
+
return {
|
|
56
|
+
invocation: ctx.currentInvocation(),
|
|
57
|
+
target,
|
|
58
|
+
op,
|
|
59
|
+
args,
|
|
60
|
+
allowed
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function metaOf(result) {
|
|
64
|
+
return {
|
|
65
|
+
changes: result.meta.changes ?? 0,
|
|
66
|
+
lastRowId: result.meta.last_row_id ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var StatementProxy = class StatementProxy {
|
|
70
|
+
real;
|
|
71
|
+
target;
|
|
72
|
+
ctx;
|
|
73
|
+
sql;
|
|
74
|
+
params;
|
|
75
|
+
constructor(real, target, ctx, sql, params) {
|
|
76
|
+
this.real = real;
|
|
77
|
+
this.target = target;
|
|
78
|
+
this.ctx = ctx;
|
|
79
|
+
this.sql = sql;
|
|
80
|
+
this.params = params;
|
|
81
|
+
}
|
|
82
|
+
/** The real statement, for `batch()`. */
|
|
83
|
+
get statement() {
|
|
84
|
+
return this.real;
|
|
85
|
+
}
|
|
86
|
+
args() {
|
|
87
|
+
return {
|
|
88
|
+
sql: this.sql,
|
|
89
|
+
params: toJson(this.params)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
bind(...values) {
|
|
93
|
+
return new StatementProxy(this.real.bind(...values), this.target, this.ctx, this.sql, values);
|
|
94
|
+
}
|
|
95
|
+
run() {
|
|
96
|
+
return this.ctx.hooks.effect(point(this.ctx, this.target, "run", this.args(), WRITE), () => this.real.run(), (r) => metaOf(r));
|
|
97
|
+
}
|
|
98
|
+
all() {
|
|
99
|
+
return this.ctx.hooks.effect(point(this.ctx, this.target, "all", this.args(), READ), () => this.real.all(), (r) => ({
|
|
100
|
+
rows: r.results.length,
|
|
101
|
+
changes: r.meta.changes ?? 0
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
first(colName) {
|
|
105
|
+
return this.ctx.hooks.effect(point(this.ctx, this.target, "first", {
|
|
106
|
+
...this.args(),
|
|
107
|
+
column: colName ?? null
|
|
108
|
+
}, READ), () => colName === void 0 ? this.real.first() : this.real.first(colName), (r) => ({ row: toJson(r) }));
|
|
109
|
+
}
|
|
110
|
+
raw(options) {
|
|
111
|
+
return this.ctx.hooks.effect(point(this.ctx, this.target, "raw", this.args(), READ), () => options ? this.real.raw(options) : this.real.raw(), (r) => ({ rows: r.length }));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/** Wrap a D1 binding. Methods not modelled fall through to the real binding and mark the run unsupported. */
|
|
115
|
+
function proxyD1(real, binding, ctx) {
|
|
116
|
+
const target = `D1:${binding}`;
|
|
117
|
+
const modelled = {
|
|
118
|
+
prepare(sql) {
|
|
119
|
+
return new StatementProxy(real.prepare(sql), target, ctx, sql, []);
|
|
120
|
+
},
|
|
121
|
+
batch(statements) {
|
|
122
|
+
const unwrapped = statements.map((s) => s instanceof StatementProxy ? s.statement : s);
|
|
123
|
+
const args = { statements: statements.map((s) => s instanceof StatementProxy ? {
|
|
124
|
+
sql: s.sql,
|
|
125
|
+
params: toJson(s.params)
|
|
126
|
+
} : "<unproxied statement>") };
|
|
127
|
+
return ctx.hooks.effect(point(ctx, target, "batch", args, WRITE), () => real.batch(unwrapped), (rs) => ({
|
|
128
|
+
changes: rs.reduce((n, r) => n + (r.meta.changes ?? 0), 0),
|
|
129
|
+
statements: rs.length
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
exec(sql) {
|
|
133
|
+
return ctx.hooks.effect(point(ctx, target, "exec", { sql }, WRITE), () => real.exec(sql), (r) => ({
|
|
134
|
+
count: r.count,
|
|
135
|
+
duration: 0
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return new Proxy(real, { get(obj, prop, receiver) {
|
|
140
|
+
if (prop in modelled) return modelled[prop];
|
|
141
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
142
|
+
if (typeof value === "function") {
|
|
143
|
+
ctx.hooks.unsupported(`D1 ${binding}.${String(prop)} is not modelled`);
|
|
144
|
+
return value.bind(obj);
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
} });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Statements in a schema file. Splits on semicolons outside string literals,
|
|
151
|
+
* quoted identifiers and comments, and keeps CREATE TRIGGER bodies (and CASE
|
|
152
|
+
* expressions) whole by tracking BEGIN/CASE ... END nesting.
|
|
153
|
+
*/
|
|
154
|
+
function splitSql(sql) {
|
|
155
|
+
const out = [];
|
|
156
|
+
let cur = "";
|
|
157
|
+
let depth = 0;
|
|
158
|
+
let i = 0;
|
|
159
|
+
const isWordChar = (c) => c !== void 0 && /[A-Za-z0-9_]/.test(c);
|
|
160
|
+
while (i < sql.length) {
|
|
161
|
+
const c = sql[i];
|
|
162
|
+
const next = sql[i + 1];
|
|
163
|
+
if (c === "-" && next === "-") {
|
|
164
|
+
while (i < sql.length && sql[i] !== "\n") i++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === "/" && next === "*") {
|
|
168
|
+
const end = sql.indexOf("*/", i + 2);
|
|
169
|
+
i = end === -1 ? sql.length : end + 2;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (c === "'" || c === "\"" || c === "`" || c === "[") {
|
|
173
|
+
const close = c === "[" ? "]" : c;
|
|
174
|
+
let j = i + 1;
|
|
175
|
+
for (;;) {
|
|
176
|
+
const k = sql.indexOf(close, j);
|
|
177
|
+
if (k === -1) {
|
|
178
|
+
j = sql.length;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (close !== "]" && sql[k + 1] === close) {
|
|
182
|
+
j = k + 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
j = k + 1;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
cur += sql.slice(i, j);
|
|
189
|
+
i = j;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (isWordChar(c) && !isWordChar(sql[i - 1])) {
|
|
193
|
+
let j = i;
|
|
194
|
+
while (isWordChar(sql[j])) j++;
|
|
195
|
+
const word = sql.slice(i, j);
|
|
196
|
+
const upper = word.toUpperCase();
|
|
197
|
+
if (upper === "BEGIN" || upper === "CASE") depth++;
|
|
198
|
+
else if (upper === "END" && depth > 0) depth--;
|
|
199
|
+
cur += word;
|
|
200
|
+
i = j;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === ";" && depth === 0) {
|
|
204
|
+
const statement = cur.trim();
|
|
205
|
+
if (statement.length > 0) out.push(statement);
|
|
206
|
+
cur = "";
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
cur += c;
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
const last = cur.trim();
|
|
214
|
+
if (last.length > 0) out.push(last);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** Drop every user table and re-apply the schema, using the real binding (not an effect). */
|
|
218
|
+
async function resetD1(real, schema) {
|
|
219
|
+
const tables = await real.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '\\_cf\\_%' ESCAPE '\\'").all();
|
|
220
|
+
for (const { name } of tables.results) await real.prepare(`DROP TABLE IF EXISTS "${name.replace(/"/g, "\"\"")}"`).run();
|
|
221
|
+
for (const statement of splitSql(schema)) await real.prepare(statement).run();
|
|
222
|
+
}
|
|
223
|
+
/** Every user table's rows, sorted canonically, read through the real binding (not an effect). */
|
|
224
|
+
async function snapshotD1(real) {
|
|
225
|
+
const tables = await real.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '\\_cf\\_%' ESCAPE '\\' ORDER BY name").all();
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const { name } of tables.results) out[name] = (await real.prepare(`SELECT * FROM "${name.replace(/"/g, "\"\"")}"`).all()).results.map((r) => toJson(r)).map((j) => [canonicalJson(j), j]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([, j]) => j);
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
// packages/cloudflare/src/platform.ts
|
|
231
|
+
const ACK = ["crash_before", "crash_after"];
|
|
232
|
+
const CUT_OFF = ["cut_off"];
|
|
233
|
+
function isD1$1(value) {
|
|
234
|
+
return typeof value === "object" && value !== null && typeof value.prepare === "function" && typeof value.batch === "function";
|
|
235
|
+
}
|
|
236
|
+
function isBindingLike(value) {
|
|
237
|
+
if (typeof value === "function") return true;
|
|
238
|
+
if (typeof value !== "object" || value === null) return false;
|
|
239
|
+
const proto = Object.getPrototypeOf(value);
|
|
240
|
+
return [...Object.getOwnPropertyNames(value), ...Object.getOwnPropertyNames(proto ?? {})].some((k) => k !== "constructor" && typeof value[k] === "function");
|
|
241
|
+
}
|
|
242
|
+
/** Deep-copy JSON data so a handler that mutates its message cannot leak into other runs. */
|
|
243
|
+
function cloneJson(value) {
|
|
244
|
+
return JSON.parse(JSON.stringify(value));
|
|
245
|
+
}
|
|
246
|
+
const assumptions = [
|
|
247
|
+
"platform model: Cloudflare Queues at-least-once delivery; the first ack or retry decision per message wins; unacked messages are redelivered up to max_retries then dead-lettered",
|
|
248
|
+
"D1 statements run against Miniflare's SQLite; each run/first/all/raw/batch/exec is one effect",
|
|
249
|
+
"message.timestamp is the virtual clock's time at delivery",
|
|
250
|
+
"only D1 bindings are proxied; touching any other binding makes the run UNSUPPORTED",
|
|
251
|
+
"state invariants read a snapshot of every D1 binding named in setup, taken after the last event",
|
|
252
|
+
"work passed to ctx.waitUntil runs after the handler returns and never affects acknowledgement; a cut_off fault at that point loses work not yet started, and an effect in flight commits without its continuation"
|
|
253
|
+
];
|
|
254
|
+
function createCloudflarePlatform(options) {
|
|
255
|
+
const env = options.env;
|
|
256
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
257
|
+
let currentInvocation = "";
|
|
258
|
+
let hooksRef = null;
|
|
259
|
+
const ctx = {
|
|
260
|
+
get hooks() {
|
|
261
|
+
if (!hooksRef) throw new Error("no run in progress");
|
|
262
|
+
return hooksRef;
|
|
263
|
+
},
|
|
264
|
+
currentInvocation: () => currentInvocation
|
|
265
|
+
};
|
|
266
|
+
const d1Proxies = /* @__PURE__ */ new Map();
|
|
267
|
+
const proxiedEnv = new Proxy(env, { get(target, prop, receiver) {
|
|
268
|
+
const value = Reflect.get(target, prop, receiver);
|
|
269
|
+
if (typeof prop !== "string") return value;
|
|
270
|
+
if (isD1$1(value)) {
|
|
271
|
+
let p = d1Proxies.get(prop);
|
|
272
|
+
if (!p) {
|
|
273
|
+
p = proxyD1(value, prop, ctx);
|
|
274
|
+
d1Proxies.set(prop, p);
|
|
275
|
+
}
|
|
276
|
+
return p;
|
|
277
|
+
}
|
|
278
|
+
if (isBindingLike(value)) ctx.hooks.unsupported(`binding ${prop} is not proxied`);
|
|
279
|
+
return value;
|
|
280
|
+
} });
|
|
281
|
+
/** Platform-level actions (dead-lettering) are recorded under an invocation no fault can crash. */
|
|
282
|
+
const platformInvocations = /* @__PURE__ */ new WeakMap();
|
|
283
|
+
function platformInvocation(hooks) {
|
|
284
|
+
let id = platformInvocations.get(hooks);
|
|
285
|
+
if (id === void 0) {
|
|
286
|
+
id = "platform";
|
|
287
|
+
hooks.startInvocation(id);
|
|
288
|
+
platformInvocations.set(hooks, id);
|
|
289
|
+
}
|
|
290
|
+
return id;
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
name: "cloudflare",
|
|
294
|
+
assumptions,
|
|
295
|
+
async reset(spec) {
|
|
296
|
+
for (const step of spec.setup) {
|
|
297
|
+
if (step.kind !== "d1") continue;
|
|
298
|
+
const binding = String(step["binding"]);
|
|
299
|
+
const schema = options.schemas[binding];
|
|
300
|
+
if (schema === void 0) throw new Error(`no schema provided for D1 binding ${binding}`);
|
|
301
|
+
const real = env[binding];
|
|
302
|
+
if (!isD1$1(real)) throw new Error(`env.${binding} is not a D1 binding`);
|
|
303
|
+
await resetD1(real, schema);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
restrictEvents(spec, keepIds) {
|
|
307
|
+
const drive = spec.drive;
|
|
308
|
+
return {
|
|
309
|
+
...spec,
|
|
310
|
+
drive: {
|
|
311
|
+
...drive,
|
|
312
|
+
messages: drive.messages.filter((m) => keepIds.includes(m.id))
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
async finish(spec, hooks) {
|
|
317
|
+
for (const step of spec.setup) {
|
|
318
|
+
if (step.kind !== "d1") continue;
|
|
319
|
+
const binding = String(step["binding"]);
|
|
320
|
+
const real = env[binding];
|
|
321
|
+
if (!isD1$1(real)) continue;
|
|
322
|
+
const tables = await snapshotD1(real);
|
|
323
|
+
hooks.snapshot(platformInvocation(hooks), `D1:${binding}`, { tables });
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
initialEvents(spec) {
|
|
327
|
+
const drive = spec.drive;
|
|
328
|
+
if (drive.kind !== "queue") throw new Error(`drive ${drive.kind} is not supported by the Cloudflare platform yet`);
|
|
329
|
+
return drive.messages.map((m) => ({
|
|
330
|
+
id: m.id,
|
|
331
|
+
attempt: 1,
|
|
332
|
+
data: m.body
|
|
333
|
+
}));
|
|
334
|
+
},
|
|
335
|
+
async deliver(event, hooks, spec) {
|
|
336
|
+
const drive = spec.drive;
|
|
337
|
+
const queueTarget = `Queue:${drive.queue}`;
|
|
338
|
+
const invocation = `${event.id}#${event.attempt}`;
|
|
339
|
+
hooksRef = hooks;
|
|
340
|
+
currentInvocation = invocation;
|
|
341
|
+
const inv = hooks.startInvocation(invocation);
|
|
342
|
+
/** Cloudflare honours the first ack or retry decision per message; later ones are ignored. */
|
|
343
|
+
const state = { decision: null };
|
|
344
|
+
const background = [];
|
|
345
|
+
/** Records the call; the result says whether it took effect (the first decision wins). */
|
|
346
|
+
const decide = (op, args) => {
|
|
347
|
+
hooks.effectSync({
|
|
348
|
+
invocation,
|
|
349
|
+
target: queueTarget,
|
|
350
|
+
op,
|
|
351
|
+
args,
|
|
352
|
+
allowed: ACK
|
|
353
|
+
}, () => {
|
|
354
|
+
if (state.decision !== null) return false;
|
|
355
|
+
state.decision = op;
|
|
356
|
+
return true;
|
|
357
|
+
}, (effective) => ({ effective }));
|
|
358
|
+
};
|
|
359
|
+
const message = {
|
|
360
|
+
id: event.id,
|
|
361
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
362
|
+
body: cloneJson(event.data),
|
|
363
|
+
attempts: event.attempt,
|
|
364
|
+
ack: () => decide("ack", {
|
|
365
|
+
messageId: event.id,
|
|
366
|
+
implicit: false,
|
|
367
|
+
body: event.data
|
|
368
|
+
}),
|
|
369
|
+
retry: () => decide("retry", {
|
|
370
|
+
messageId: event.id,
|
|
371
|
+
body: event.data
|
|
372
|
+
})
|
|
373
|
+
};
|
|
374
|
+
const batch = {
|
|
375
|
+
queue: drive.queue,
|
|
376
|
+
messages: [message],
|
|
377
|
+
metadata: { metrics: {
|
|
378
|
+
backlogCount: 0,
|
|
379
|
+
backlogBytes: 0
|
|
380
|
+
} },
|
|
381
|
+
ackAll: () => message.ack(),
|
|
382
|
+
retryAll: () => message.retry()
|
|
383
|
+
};
|
|
384
|
+
const execution = {
|
|
385
|
+
waitUntil: (promise) => {
|
|
386
|
+
background.push(promise.then(() => void 0, () => void 0));
|
|
387
|
+
},
|
|
388
|
+
passThroughOnException: () => {},
|
|
389
|
+
props: {}
|
|
390
|
+
};
|
|
391
|
+
const handler = options.handler.queue;
|
|
392
|
+
if (!handler) throw new Error("the Worker has no queue() handler");
|
|
393
|
+
const outcome = Promise.resolve().then(() => handler.call(options.handler, batch, proxiedEnv, execution)).then(() => "completed", (err) => err instanceof CrashSignal ? "crashed" : "errored");
|
|
394
|
+
let status = await Promise.race([hooks.settle(outcome, invocation), inv.crashed.then(() => "crashed")]);
|
|
395
|
+
if (status === "completed" && state.decision === null) try {
|
|
396
|
+
decide("ack", {
|
|
397
|
+
messageId: event.id,
|
|
398
|
+
implicit: true,
|
|
399
|
+
body: event.data
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err instanceof CrashSignal) status = "crashed";
|
|
403
|
+
else throw err;
|
|
404
|
+
}
|
|
405
|
+
if (background.length > 0 && !inv.isCrashed()) try {
|
|
406
|
+
hooks.effectSync({
|
|
407
|
+
invocation,
|
|
408
|
+
target: queueTarget,
|
|
409
|
+
op: "background",
|
|
410
|
+
args: { pending: background.length },
|
|
411
|
+
allowed: CUT_OFF
|
|
412
|
+
}, () => null, () => null);
|
|
413
|
+
await Promise.race([hooks.settle(Promise.all(background), invocation), inv.crashed]);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (!(err instanceof CrashSignal)) throw err;
|
|
416
|
+
status = "crashed";
|
|
417
|
+
}
|
|
418
|
+
const followUps = [];
|
|
419
|
+
if (state.decision !== "ack") {
|
|
420
|
+
const redeliveries = event.attempt - 1;
|
|
421
|
+
if (event.attempt <= maxRetries && redeliveries < spec.bounds.maxRedeliveries) followUps.push({
|
|
422
|
+
...event,
|
|
423
|
+
attempt: event.attempt + 1
|
|
424
|
+
});
|
|
425
|
+
else hooks.effectSync({
|
|
426
|
+
invocation: platformInvocation(hooks),
|
|
427
|
+
target: queueTarget,
|
|
428
|
+
op: "dead_letter",
|
|
429
|
+
args: {
|
|
430
|
+
messageId: event.id,
|
|
431
|
+
attempts: event.attempt
|
|
432
|
+
},
|
|
433
|
+
allowed: []
|
|
434
|
+
}, () => null, () => null);
|
|
435
|
+
}
|
|
436
|
+
currentInvocation = "";
|
|
437
|
+
return {
|
|
438
|
+
status,
|
|
439
|
+
followUps
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// packages/cloudflare/src/differential.ts
|
|
445
|
+
/**
|
|
446
|
+
* Differential check: the same fault-free scenario run once through Eutaxy's
|
|
447
|
+
* proxies and once against the raw bindings, with the queue model applied by
|
|
448
|
+
* hand on the raw side. Any difference in the per-attempt decisions or in
|
|
449
|
+
* the final D1 state is a bug in the proxies or the platform model, not in
|
|
450
|
+
* the Worker. The raw side shares Eutaxy's *reading* of Cloudflare's queue
|
|
451
|
+
* semantics; what it does not share is any proxy code.
|
|
452
|
+
*/
|
|
453
|
+
/** The raw side cannot abandon an invocation the way the engine does; a stuck handler ends the comparison. */
|
|
454
|
+
var StuckError = class extends Error {
|
|
455
|
+
name = "StuckError";
|
|
456
|
+
constructor(message = "the handler is waiting on something outside the run (no timer due, no effect in flight)") {
|
|
457
|
+
super(message);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* The raw side's only wrapper: counts D1 calls in flight (so the settle loop
|
|
462
|
+
* knows real I/O is pending) and advances the clock as the proxies do. It
|
|
463
|
+
* records nothing and injects nothing.
|
|
464
|
+
*/
|
|
465
|
+
function countingD1(real, io, clock) {
|
|
466
|
+
const realOf = /* @__PURE__ */ new WeakMap();
|
|
467
|
+
const track = async (p) => {
|
|
468
|
+
io.n++;
|
|
469
|
+
try {
|
|
470
|
+
return await p;
|
|
471
|
+
} finally {
|
|
472
|
+
io.n--;
|
|
473
|
+
clock.advance(1);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const wrapStatement = (s) => {
|
|
477
|
+
const proxy = new Proxy(s, { get(t, prop, r) {
|
|
478
|
+
const v = Reflect.get(t, prop, r);
|
|
479
|
+
if (prop === "bind") return (...a) => wrapStatement(t.bind(...a));
|
|
480
|
+
if (prop === "run" || prop === "all" || prop === "first" || prop === "raw") return (...a) => track(v.apply(t, a));
|
|
481
|
+
return typeof v === "function" ? v.bind(t) : v;
|
|
482
|
+
} });
|
|
483
|
+
realOf.set(proxy, s);
|
|
484
|
+
return proxy;
|
|
485
|
+
};
|
|
486
|
+
return new Proxy(real, { get(t, prop, r) {
|
|
487
|
+
const v = Reflect.get(t, prop, r);
|
|
488
|
+
if (prop === "prepare") return (sql) => wrapStatement(t.prepare(sql));
|
|
489
|
+
if (prop === "batch") return (stmts) => track(t.batch(stmts.map((s) => realOf.get(s) ?? s)));
|
|
490
|
+
if (prop === "exec") return (sql) => track(t.exec(sql));
|
|
491
|
+
return typeof v === "function" ? v.bind(t) : v;
|
|
492
|
+
} });
|
|
493
|
+
}
|
|
494
|
+
function isD1(value) {
|
|
495
|
+
return typeof value === "object" && value !== null && typeof value.prepare === "function";
|
|
496
|
+
}
|
|
497
|
+
async function snapshotAll(spec, options) {
|
|
498
|
+
const env = options.env;
|
|
499
|
+
const out = {};
|
|
500
|
+
for (const step of spec.setup) {
|
|
501
|
+
if (step.kind !== "d1") continue;
|
|
502
|
+
const binding = String(step["binding"]);
|
|
503
|
+
const real = env[binding];
|
|
504
|
+
if (isD1(real)) out[binding] = await snapshotD1(real);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
/** The Eutaxy side: one run with no faults, driven like `runOnce` but keeping each delivery's status. */
|
|
509
|
+
async function eutaxySide(spec, options) {
|
|
510
|
+
const platform = createCloudflarePlatform(options);
|
|
511
|
+
const run = new Run({
|
|
512
|
+
mode: "replay",
|
|
513
|
+
schedule: {
|
|
514
|
+
seed: 0,
|
|
515
|
+
faults: []
|
|
516
|
+
},
|
|
517
|
+
bounds: spec.bounds,
|
|
518
|
+
faultProbability: 0
|
|
519
|
+
});
|
|
520
|
+
const attempts = [];
|
|
521
|
+
await platform.reset(spec);
|
|
522
|
+
run.install(spec.externals);
|
|
523
|
+
try {
|
|
524
|
+
const pending = [...platform.initialEvents(spec)];
|
|
525
|
+
while (pending.length > 0) {
|
|
526
|
+
const event = pending.shift();
|
|
527
|
+
const invocation = `${event.id}#${event.attempt}`;
|
|
528
|
+
const before = run.log.entries.length;
|
|
529
|
+
const result = await platform.deliver(event, run, spec);
|
|
530
|
+
const decision = run.log.entries.slice(before).find((e) => e.invocation === invocation && e.kind === "result" && (e.op === "ack" || e.op === "retry") && getPath(e.payload, "result.effective") === true)?.op;
|
|
531
|
+
attempts.push({
|
|
532
|
+
event: event.id,
|
|
533
|
+
attempt: event.attempt,
|
|
534
|
+
status: result.status === "completed" ? "completed" : "errored",
|
|
535
|
+
decision: decision ?? null
|
|
536
|
+
});
|
|
537
|
+
pending.push(...result.followUps);
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
run.uninstall();
|
|
541
|
+
}
|
|
542
|
+
await platform.finish?.(spec, run);
|
|
543
|
+
return {
|
|
544
|
+
attempts,
|
|
545
|
+
state: await snapshotAll(spec, options),
|
|
546
|
+
unsupported: run.unsupportedReasons
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/** The raw side: the handler called directly with the real bindings and plain message objects. */
|
|
550
|
+
async function rawSide(spec, options) {
|
|
551
|
+
const env = options.env;
|
|
552
|
+
const drive = spec.drive;
|
|
553
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
554
|
+
for (const step of spec.setup) {
|
|
555
|
+
if (step.kind !== "d1") continue;
|
|
556
|
+
const binding = String(step["binding"]);
|
|
557
|
+
const schema = options.schemas[binding];
|
|
558
|
+
const real = env[binding];
|
|
559
|
+
if (schema !== void 0 && isD1(real)) await resetD1(real, schema);
|
|
560
|
+
}
|
|
561
|
+
const handler = options.handler.queue;
|
|
562
|
+
if (!handler) throw new Error("the Worker has no queue() handler");
|
|
563
|
+
const attempts = [];
|
|
564
|
+
const pending = drive.messages.map((m) => ({
|
|
565
|
+
id: m.id,
|
|
566
|
+
attempt: 1,
|
|
567
|
+
data: m.body
|
|
568
|
+
}));
|
|
569
|
+
const patched = patchGlobals(0);
|
|
570
|
+
const unsupported = [];
|
|
571
|
+
const restoreFetch = installScriptedFetch(spec.externals, null, (r) => unsupported.push(r), () => patched.clock.advance(1));
|
|
572
|
+
const io = { n: 0 };
|
|
573
|
+
const counting = /* @__PURE__ */ new Map();
|
|
574
|
+
const countedEnv = new Proxy(env, { get(target, prop, receiver) {
|
|
575
|
+
const value = Reflect.get(target, prop, receiver);
|
|
576
|
+
if (typeof prop !== "string" || !isD1(value)) return value;
|
|
577
|
+
let c = counting.get(prop);
|
|
578
|
+
if (!c) {
|
|
579
|
+
c = countingD1(value, io, patched.clock);
|
|
580
|
+
counting.set(prop, c);
|
|
581
|
+
}
|
|
582
|
+
return c;
|
|
583
|
+
} });
|
|
584
|
+
const settle = (p) => settleOn(p, {
|
|
585
|
+
clock: patched.clock,
|
|
586
|
+
inFlight: () => io.n,
|
|
587
|
+
crashed: () => false,
|
|
588
|
+
stuck: () => {
|
|
589
|
+
throw new StuckError();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
try {
|
|
593
|
+
while (pending.length > 0) {
|
|
594
|
+
const event = pending.shift();
|
|
595
|
+
let decision = null;
|
|
596
|
+
const background = [];
|
|
597
|
+
const message = {
|
|
598
|
+
id: event.id,
|
|
599
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
600
|
+
body: JSON.parse(JSON.stringify(event.data)),
|
|
601
|
+
attempts: event.attempt,
|
|
602
|
+
ack: () => {
|
|
603
|
+
decision ??= "ack";
|
|
604
|
+
},
|
|
605
|
+
retry: () => {
|
|
606
|
+
decision ??= "retry";
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const batch = {
|
|
610
|
+
queue: drive.queue,
|
|
611
|
+
messages: [message],
|
|
612
|
+
metadata: { metrics: {
|
|
613
|
+
backlogCount: 0,
|
|
614
|
+
backlogBytes: 0
|
|
615
|
+
} },
|
|
616
|
+
ackAll: () => message.ack(),
|
|
617
|
+
retryAll: () => message.retry()
|
|
618
|
+
};
|
|
619
|
+
const ctx = {
|
|
620
|
+
waitUntil: (p) => {
|
|
621
|
+
background.push(p.then(() => void 0, () => void 0));
|
|
622
|
+
},
|
|
623
|
+
passThroughOnException: () => {},
|
|
624
|
+
props: {}
|
|
625
|
+
};
|
|
626
|
+
let status;
|
|
627
|
+
try {
|
|
628
|
+
await settle(Promise.resolve().then(() => handler.call(options.handler, batch, countedEnv, ctx)));
|
|
629
|
+
status = "completed";
|
|
630
|
+
} catch (err) {
|
|
631
|
+
if (err instanceof CrashSignal || err instanceof StuckError) throw err;
|
|
632
|
+
status = "errored";
|
|
633
|
+
}
|
|
634
|
+
if (status === "completed") decision ??= "ack";
|
|
635
|
+
await settle(Promise.all(background));
|
|
636
|
+
attempts.push({
|
|
637
|
+
event: event.id,
|
|
638
|
+
attempt: event.attempt,
|
|
639
|
+
status,
|
|
640
|
+
decision
|
|
641
|
+
});
|
|
642
|
+
if (decision !== "ack" && event.attempt <= maxRetries && event.attempt - 1 < spec.bounds.maxRedeliveries) pending.push({
|
|
643
|
+
...event,
|
|
644
|
+
attempt: event.attempt + 1
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
} finally {
|
|
648
|
+
restoreFetch();
|
|
649
|
+
patched.restore();
|
|
650
|
+
}
|
|
651
|
+
if (unsupported.length > 0) throw new StuckError(`raw side: ${unsupported.join("; ")}`);
|
|
652
|
+
return {
|
|
653
|
+
attempts,
|
|
654
|
+
state: await snapshotAll(spec, options)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/** Run both sides and compare. The Eutaxy side runs first; both start from the schema. */
|
|
658
|
+
async function runDifferential(spec, options) {
|
|
659
|
+
const { unsupported, ...eutaxy } = await eutaxySide(spec, options);
|
|
660
|
+
const raw = await rawSide(spec, options);
|
|
661
|
+
const differences = [];
|
|
662
|
+
if (unsupported.length > 0) differences.push(`the proxied run touched unsupported APIs: ${unsupported.join("; ")}`);
|
|
663
|
+
const a = canonicalJson(eutaxy.attempts);
|
|
664
|
+
const b = canonicalJson(raw.attempts);
|
|
665
|
+
if (a !== b) differences.push(`attempts differ:\n eutaxy ${a}\n raw ${b}`);
|
|
666
|
+
const bindings = /* @__PURE__ */ new Set([...Object.keys(eutaxy.state), ...Object.keys(raw.state)]);
|
|
667
|
+
for (const binding of [...bindings].sort()) {
|
|
668
|
+
const tables = /* @__PURE__ */ new Set([...Object.keys(eutaxy.state[binding] ?? {}), ...Object.keys(raw.state[binding] ?? {})]);
|
|
669
|
+
for (const table of [...tables].sort()) {
|
|
670
|
+
const x = canonicalJson(eutaxy.state[binding]?.[table] ?? []);
|
|
671
|
+
const y = canonicalJson(raw.state[binding]?.[table] ?? []);
|
|
672
|
+
if (x !== y) differences.push(`${binding}.${table} differs:\n eutaxy ${x}\n raw ${y}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
equal: differences.length === 0,
|
|
677
|
+
eutaxy,
|
|
678
|
+
raw,
|
|
679
|
+
differences,
|
|
680
|
+
unsupported
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
// packages/cloudflare/src/scenario.ts
|
|
684
|
+
/**
|
|
685
|
+
* Typed construction of scenario specs for Cloudflare Workers. Everything here
|
|
686
|
+
* produces the plain-data `ScenarioSpec` from core; this module only knows the
|
|
687
|
+
* Cloudflare shapes (queues, D1 bindings) and the effect conventions the
|
|
688
|
+
* proxies follow.
|
|
689
|
+
*/
|
|
690
|
+
function queue(name, messages) {
|
|
691
|
+
return {
|
|
692
|
+
kind: "queue",
|
|
693
|
+
queue: name,
|
|
694
|
+
messages: messages.map((m, i) => ({
|
|
695
|
+
id: m.id ?? `msg-${i + 1}`,
|
|
696
|
+
body: m.body
|
|
697
|
+
}))
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function d1Schema(binding, schemaFile) {
|
|
701
|
+
return {
|
|
702
|
+
kind: "d1",
|
|
703
|
+
binding,
|
|
704
|
+
schemaFile
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Effect conventions for D1 (what the proxies record):
|
|
709
|
+
* call payload { args: { sql, params } }
|
|
710
|
+
* result payload { args: { sql, params }, result: { changes, lastRowId } }
|
|
711
|
+
* A committed write is a `result` whose `result.changes` is at least 1.
|
|
712
|
+
*/
|
|
713
|
+
function d1Write(binding, op = "run") {
|
|
714
|
+
return {
|
|
715
|
+
target: `D1:${binding}`,
|
|
716
|
+
op,
|
|
717
|
+
kind: "result",
|
|
718
|
+
atLeast: { "result.changes": 1 }
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/** Dot path to the Nth bound parameter of a D1 statement, in an effect payload. */
|
|
722
|
+
function d1Param(index) {
|
|
723
|
+
return `args.params.${index}`;
|
|
724
|
+
}
|
|
725
|
+
function atMostOncePerKey(name, effect, key) {
|
|
726
|
+
return {
|
|
727
|
+
kind: "atMostOncePerKey",
|
|
728
|
+
name,
|
|
729
|
+
effect,
|
|
730
|
+
key
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function atLeastOnce(name, effect, key, expected) {
|
|
734
|
+
return {
|
|
735
|
+
kind: "atLeastOnce",
|
|
736
|
+
name,
|
|
737
|
+
effect,
|
|
738
|
+
key,
|
|
739
|
+
expected
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Every message's `bodyKey` value must appear at least once as `key` in a
|
|
744
|
+
* selected effect: nothing the queue delivered may be lost.
|
|
745
|
+
*/
|
|
746
|
+
function atLeastOncePerMessage(name, drive, effect, key, bodyKey) {
|
|
747
|
+
const expected = [];
|
|
748
|
+
for (const m of drive.messages) {
|
|
749
|
+
const v = getPath(m.body, bodyKey);
|
|
750
|
+
if (v === void 0) throw new Error(`message ${m.id} has no ${JSON.stringify(bodyKey)} in its body`);
|
|
751
|
+
expected.push(v);
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
...atLeastOnce(name, effect, key, expected),
|
|
755
|
+
perMessage: { bodyKey }
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/** Rows of a D1 table in the end-of-run snapshot, optionally filtered. */
|
|
759
|
+
function d1Rows(binding, table, where) {
|
|
760
|
+
return where ? {
|
|
761
|
+
target: `D1:${binding}`,
|
|
762
|
+
table,
|
|
763
|
+
where
|
|
764
|
+
} : {
|
|
765
|
+
target: `D1:${binding}`,
|
|
766
|
+
table
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/** Every row the query selects satisfies all conditions. */
|
|
770
|
+
function everyRow(name, query, holds) {
|
|
771
|
+
return {
|
|
772
|
+
kind: "everyRow",
|
|
773
|
+
name,
|
|
774
|
+
query,
|
|
775
|
+
holds
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
/** Every selected effect has a row whose column equals the effect's key. */
|
|
779
|
+
function implies(name, when, key, state, column) {
|
|
780
|
+
return {
|
|
781
|
+
kind: "implies",
|
|
782
|
+
name,
|
|
783
|
+
when,
|
|
784
|
+
key,
|
|
785
|
+
state,
|
|
786
|
+
column
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/** Selects the acknowledgement that took effect for each message (explicit or implicit; not one ignored after a retry). */
|
|
790
|
+
function queueAck(queueName) {
|
|
791
|
+
return {
|
|
792
|
+
target: `Queue:${queueName}`,
|
|
793
|
+
op: "ack",
|
|
794
|
+
kind: "result",
|
|
795
|
+
where: { "result.effective": true }
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Declare an external service: requests to `host` are answered from
|
|
800
|
+
* `responses` (every request gets 200 and an empty body when omitted), and
|
|
801
|
+
* `dedupeBy` names the header whose value makes repeated requests
|
|
802
|
+
* idempotent at that service.
|
|
803
|
+
*/
|
|
804
|
+
function external(name, host, options = {}) {
|
|
805
|
+
return {
|
|
806
|
+
name,
|
|
807
|
+
host,
|
|
808
|
+
guarantees: options.dedupeBy ? { dedupeBy: options.dedupeBy } : {},
|
|
809
|
+
...options.responses ? { responses: options.responses } : {}
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Selects the requests to an external that took effect: a request replayed
|
|
814
|
+
* under a dedupe key the external had already seen is not counted.
|
|
815
|
+
*/
|
|
816
|
+
function fetchCall(externalName, method) {
|
|
817
|
+
return {
|
|
818
|
+
target: `Fetch:${externalName}`,
|
|
819
|
+
...method ? { op: method.toUpperCase() } : {},
|
|
820
|
+
kind: "result",
|
|
821
|
+
where: { "result.effective": true }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/** Build and validate a scenario spec; throws with every problem listed. */
|
|
825
|
+
function defineScenario(input) {
|
|
826
|
+
const drive = "messages" in input.drive && input.drive.messages.every((m) => "id" in m && typeof m.id === "string") ? input.drive : queue(input.drive.queue, input.drive.messages);
|
|
827
|
+
const spec = {
|
|
828
|
+
version: 1,
|
|
829
|
+
name: input.name,
|
|
830
|
+
platform: "cloudflare",
|
|
831
|
+
worker: input.worker,
|
|
832
|
+
drive,
|
|
833
|
+
setup: input.setup ?? [],
|
|
834
|
+
externals: input.externals ?? [],
|
|
835
|
+
invariants: input.invariants,
|
|
836
|
+
bounds: {
|
|
837
|
+
...defaultBounds,
|
|
838
|
+
...input.bounds
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const errors = validateScenario(spec);
|
|
842
|
+
if (errors.length > 0) throw new Error(`invalid scenario ${JSON.stringify(input.name)}:\n- ${errors.join("\n- ")}`);
|
|
843
|
+
return spec;
|
|
844
|
+
}
|
|
845
|
+
export { atLeastOnce, atLeastOncePerMessage, atMostOncePerKey, check, cloudflareRegressionTest, createCloudflarePlatform, d1Param, d1Rows, d1Schema, d1Write, defineScenario, everyRow, expectHolds, external, fetchCall, implies, queue, queueAck, runDifferential, snapshotD1 };
|
|
846
|
+
|
|
847
|
+
//# sourceMappingURL=cloudflare.mjs.map
|