UNPKG

@cloudflare/vitest-pool-workers

Version:

Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime

986 lines (973 loc) 44.1 kB
import assert from "node:assert"; import { DurableObject, WorkerEntrypoint, WorkflowEntrypoint, env, env as env$1, exports } from "cloudflare:workers"; import { AsyncLocalStorage } from "node:async_hooks"; import workerdUnsafe from "workerd:unsafe"; //#region src/worker/fetch-mock.ts const originalFetch = fetch; globalThis.fetch = async (input, init) => { return originalFetch.call(globalThis, input, init); }; //#endregion //#region src/worker/d1.ts function isD1Database(v) { return typeof v === "object" && v !== null && v.constructor.name === "D1Database" && "prepare" in v && typeof v.prepare === "function" && "batch" in v && typeof v.batch === "function" && "exec" in v && typeof v.exec === "function"; } function isD1Migration(v) { return typeof v === "object" && v !== null && "name" in v && typeof v.name === "string" && "queries" in v && Array.isArray(v.queries) && v.queries.every((query) => typeof query === "string"); } function isD1Migrations(v) { return Array.isArray(v) && v.every(isD1Migration); } async function applyD1Migrations(db, migrations, migrationsTableName = "d1_migrations") { if (!isD1Database(db)) throw new TypeError("Failed to execute 'applyD1Migrations': parameter 1 is not of type 'D1Database'."); if (!isD1Migrations(migrations)) throw new TypeError("Failed to execute 'applyD1Migrations': parameter 2 is not of type 'D1Migration[]'."); if (typeof migrationsTableName !== "string") throw new TypeError("Failed to execute 'applyD1Migrations': parameter 3 is not of type 'string'."); const schema = `CREATE TABLE IF NOT EXISTS ${migrationsTableName} ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL );`; await db.prepare(schema).run(); const appliedMigrationNames = (await db.prepare(`SELECT name FROM ${migrationsTableName};`).all()).results.map(({ name }) => name); const insertMigrationStmt = db.prepare(`INSERT INTO ${migrationsTableName} (name) VALUES (?);`); for (const migration of migrations) { if (appliedMigrationNames.includes(migration.name)) continue; const queries = migration.queries.map((query) => db.prepare(query)); queries.push(insertMigrationStmt.bind(migration.name)); await db.batch(queries); } } //#endregion //#region src/worker/env.ts /** * For reasons that aren't clear to me, just `SELF = exports.default` ends up with SELF being * undefined in a test. This Proxy solution works. */ const SELF = new Proxy({}, { get(_, p) { const target = exports.default; const value = target[p]; return typeof value === "function" ? value.bind(target) : value; } }); function getSerializedOptions() { assert(typeof __vitest_worker__ === "object", "Expected global Vitest state"); const options = __vitest_worker__.providedContext.cloudflarePoolOptions; assert(options !== void 0, "Expected serialised options, got keys: " + Object.keys(__vitest_worker__.providedContext).join(", ")); const parsedOptions = JSON.parse(options); return { ...parsedOptions, durableObjectBindingDesignators: new Map(parsedOptions.durableObjectBindingDesignators) }; } function getResolvedMainPath(forBindingType) { const options = getSerializedOptions(); if (options.main === void 0) throw new Error(`Using ${forBindingType} bindings to the current worker requires \`poolOptions.workers.main\` to be set to your worker's entrypoint: ${JSON.stringify(options)}`); return options.main; } //#endregion //#region src/worker/durable-objects.ts const CF_KEY_ACTION = "vitestPoolWorkersDurableObjectAction"; let nextActionId = 0; const kUseResponse = Symbol("kUseResponse"); const actionResults = /* @__PURE__ */ new Map(); function isDurableObjectNamespace(v) { return v instanceof Object && /^(?:Loopback)?DurableObjectNamespace$/.test(v.constructor.name) && "newUniqueId" in v && typeof v.newUniqueId === "function" && "idFromName" in v && typeof v.idFromName === "function" && "idFromString" in v && typeof v.idFromString === "function" && "get" in v && typeof v.get === "function"; } function isDurableObjectStub(v) { return typeof v === "object" && v !== null && (v.constructor.name === "DurableObject" || v.constructor.name === "WorkerRpc") && "fetch" in v && typeof v.fetch === "function" && "id" in v && typeof v.id === "object"; } let sameIsolatedNamespaces; function getSameIsolateNamespaces() { if (sameIsolatedNamespaces !== void 0) return sameIsolatedNamespaces; sameIsolatedNamespaces = []; const options = getSerializedOptions(); if (options.durableObjectBindingDesignators === void 0) return sameIsolatedNamespaces; for (const [key, designator] of options.durableObjectBindingDesignators) { if (designator.scriptName !== void 0) continue; const namespace = env$1[key] ?? exports?.[key]; assert(isDurableObjectNamespace(namespace), `Expected ${key} to be a DurableObjectNamespace binding`); sameIsolatedNamespaces.push(namespace); } return sameIsolatedNamespaces; } function assertSameIsolate(stub) { const idString = stub.id.toString(); const namespaces = getSameIsolateNamespaces(); for (const namespace of namespaces) try { namespace.idFromString(idString); return; } catch {} throw new Error("Durable Object test helpers can only be used with stubs pointing to objects defined within the same worker."); } async function runInStub(stub, callback) { const id = nextActionId++; actionResults.set(id, callback); const response = await stub.fetch("http://x", { cf: { [CF_KEY_ACTION]: id }, redirect: "manual" }); assert(actionResults.has(id), `Expected action result for ${id}`); const result = actionResults.get(id); actionResults.delete(id); if (result === kUseResponse) return response; else if (response.ok) return result; else throw result; } async function runInDurableObject(stub, callback) { if (!isDurableObjectStub(stub)) throw new TypeError("Failed to execute 'runInDurableObject': parameter 1 is not of type 'DurableObjectStub'."); if (typeof callback !== "function") throw new TypeError("Failed to execute 'runInDurableObject': parameter 2 is not of type 'function'."); assertSameIsolate(stub); return runInStub(stub, callback); } async function runAlarm(instance, state) { if (await state.storage.getAlarm() === null) return false; await state.storage.deleteAlarm(); await instance.alarm?.(); return true; } async function runDurableObjectAlarm(stub) { if (!isDurableObjectStub(stub)) throw new TypeError("Failed to execute 'runDurableObjectAlarm': parameter 1 is not of type 'DurableObjectStub'."); return await runInDurableObject(stub, runAlarm); } /** * Internal method for running `callback` inside the I/O context of the * Runner Durable Object. * * Tests run in this context by default. This is required for performing * operations that use Vitest's RPC mechanism as the Durable Object * owns the RPC WebSocket. For example, importing modules or sending logs. * Trying to perform those operations from a different context (e.g. within * a `export default { fetch() {} }` handler or user Durable Object's `fetch()` * handler) without using this function will result in a `Cannot perform I/O on * behalf of a different request` error. */ function runInRunnerObject(callback) { return runInStub(env$1["__VITEST_POOL_WORKERS_RUNNER_OBJECT"].get("singleton"), callback); } async function maybeHandleRunRequest(request, instance, state) { const actionId = request.cf?.[CF_KEY_ACTION]; if (actionId === void 0) return; assert(typeof actionId === "number", `Expected numeric ${CF_KEY_ACTION}`); try { const callback = actionResults.get(actionId); assert(typeof callback === "function", `Expected callback for ${actionId}`); const result = await callback(instance, state); if (result instanceof Response) { actionResults.set(actionId, kUseResponse); return result; } else actionResults.set(actionId, result); return new Response(null, { status: 204 }); } catch (e) { actionResults.set(actionId, e); return new Response(null, { status: 500 }); } } async function listDurableObjectIds(namespace) { if (!isDurableObjectNamespace(namespace)) throw new TypeError("Failed to execute 'listDurableObjectIds': parameter 1 is not of type 'DurableObjectNamespace'."); const boundName = Object.entries(env$1).find((entry) => namespace === entry[1])?.[0]; assert(boundName !== void 0, "Expected to find bound name for namespace"); const options = getSerializedOptions(); const designator = options.durableObjectBindingDesignators?.get(boundName); assert(designator !== void 0, "Expected to find designator for namespace"); let uniqueKey = designator.unsafeUniqueKey; if (uniqueKey === void 0) uniqueKey = `${designator.scriptName ?? options.selfName}-${designator.className}`; const url = `http://placeholder/durable-objects?unique_key=${encodeURIComponent(uniqueKey)}`; const res = await env$1.__VITEST_POOL_WORKERS_LOOPBACK_SERVICE.fetch(url); assert.strictEqual(res.status, 200); const ids = await res.json(); assert(Array.isArray(ids)); return ids.map((id) => { assert(typeof id === "string"); return namespace.idFromString(id); }); } //#endregion //#region src/worker/wait-until.ts /** * In production, Workers have a 30-second limit for `waitUntil` promises. * We use the same limit here. If promises are still pending after this, * they almost certainly indicate a bug (e.g. a `waitUntil` promise that * will never resolve). We log a warning and move on so the test suite * doesn't hang indefinitely. */ let WAIT_UNTIL_TIMEOUT = 3e4; /** @internal — only exposed for tests */ function setWaitUntilTimeout(ms) { WAIT_UNTIL_TIMEOUT = ms; } const kTimedOut = Symbol("kTimedOut"); /** * Empty array and wait for all promises to resolve until no more added. * If a single promise rejects, the rejection will be passed-through. * If multiple promises reject, the rejections will be aggregated. * * If any batch of promises hasn't settled after {@link WAIT_UNTIL_TIMEOUT}ms, * a warning is logged and the remaining promises are abandoned. */ async function waitForWaitUntil(waitUntil) { const errors = []; while (waitUntil.length > 0) { const batch = waitUntil.splice(0); let timeoutId; const result = await Promise.race([Promise.allSettled(batch).then((results) => ({ results })), new Promise((resolve) => timeoutId = setTimeout(() => resolve(kTimedOut), WAIT_UNTIL_TIMEOUT))]); clearTimeout(timeoutId); if (result === kTimedOut) { __console.warn(`[vitest-pool-workers] ${batch.length} waitUntil promise(s) did not resolve within ${WAIT_UNTIL_TIMEOUT / 1e3}s and will be abandoned. This normally means your Worker's waitUntil handler has a bug that prevents it from settling (e.g. a fetch that never completes or a missing resolve/reject call).`); waitUntil.length = 0; break; } for (const settled of result.results) if (settled.status === "rejected") errors.push(settled.reason); } if (errors.length === 1) throw errors[0]; else if (errors.length > 1) throw new AggregateError(errors); } const globalWaitUntil = []; function registerGlobalWaitUntil(promise) { globalWaitUntil.push(promise); } function waitForGlobalWaitUntil() { return waitForWaitUntil(globalWaitUntil); } const handlerContextStore = new AsyncLocalStorage(); function registerHandlerAndGlobalWaitUntil(promise) { const handlerContext = handlerContextStore.getStore(); if (handlerContext === void 0) registerGlobalWaitUntil(promise); else handlerContext.waitUntil(promise); } //#endregion //#region src/worker/patch-ctx.ts const patchedHandlerContexts = /* @__PURE__ */ new WeakSet(); /** * Executes the given callback within the provided ExecutionContext, * patching the context to ensure that: * * - waitUntil calls are registered globally * - ctx.exports shows a warning if accessing missing exports */ function patchAndRunWithHandlerContext(ctx, callback) { if (!patchedHandlerContexts.has(ctx)) { patchedHandlerContexts.add(ctx); const originalWaitUntil = ctx.waitUntil; ctx.waitUntil = (promise) => { registerGlobalWaitUntil(promise); return originalWaitUntil.call(ctx, promise); }; if (isCtxExportsEnabled(ctx.exports)) Object.defineProperty(ctx, "exports", { value: getCtxExportsProxy(ctx.exports) }); } return handlerContextStore.run(ctx, callback); } /** * Creates a proxy to the `ctx.exports` object that will warn the user if they attempt * to access an undefined property. This could be a valid mistake by the user or * it could mean that our static analysis of the main Worker's exports missed something. */ function getCtxExportsProxy(exports$1) { return new Proxy(exports$1, { get(target, p) { if (p in target) return target[p]; console.warn(`Attempted to access 'ctx.exports.${p}', which was not defined for the main Worker.\nCheck that '${p}' is exported as an entry-point from the Worker.\nThe '@cloudflare/vitest-pool-workers' integration tries to infer these exports by analyzing the source code of the main Worker.\n`); } }); } /** * Returns true if `ctx.exports` is enabled via compatibility flags. */ function isCtxExportsEnabled(exports$1) { return globalThis.Cloudflare?.compatibilityFlags.enable_ctx_exports && exports$1 !== void 0; } //#endregion //#region src/worker/entrypoints.ts /** * Internal method for importing a module using Vite's transformation and * execution pipeline. Can be called from any I/O context, and will ensure the * request is run from within the `__VITEST_POOL_WORKERS_RUNNER_DURABLE_OBJECT__`. */ async function importModule(specifier) { /** * We need to run this import inside the Runner Object, or we get errors like: * - The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/ * - Cannot perform I/O on behalf of a different Durable Object. I/O objects (such as streams, request/response bodies, and others) created in the context of one Durable Object cannot be accessed from a different Durable Object in the same isolate. This is a limitation of Cloudflare Workers which allows us to improve overall performance. */ return runInRunnerObject(() => { return __vitest_mocker__.moduleRunner.import(specifier); }); } const IGNORED_KEYS = ["self"]; /** * Create a class extending `superClass` with a `Proxy` as a `prototype`. * Unknown accesses on the `prototype` will defer to `getUnknownPrototypeKey()`. * `workerd` will only look for RPC methods/properties on the prototype, not the * instance. This helps avoid accidentally exposing things over RPC, but makes * things a little trickier for us... */ function createProxyPrototypeClass(superClass, getUnknownPrototypeKey) { function Class(...args) { Class.prototype = new Proxy(Class.prototype, { get(target, key, receiver) { const value = Reflect.get(target, key, receiver); if (value !== void 0) return value; if (typeof key === "symbol" || IGNORED_KEYS.includes(key)) return; return getUnknownPrototypeKey.call(receiver, key); } }); return Reflect.construct(superClass, args, Class); } Reflect.setPrototypeOf(Class.prototype, superClass.prototype); Reflect.setPrototypeOf(Class, superClass); return Class; } /** * Only properties and methods declared on the prototype can be accessed over * RPC. This function gets a property from the prototype if it's defined, and * throws a helpful error message if not. Note we need to distinguish between a * property that returns `undefined` and something not being defined at all. */ function getRPCProperty(ctor, instance, key) { if (!Reflect.has(ctor.prototype, key)) { const quotedKey = JSON.stringify(key); const instanceHasKey = Reflect.has(instance, key); let message = ""; if (instanceHasKey) message = [ `The RPC receiver's prototype does not implement ${quotedKey}, but the receiver instance does.`, "Only properties and methods defined on the prototype can be accessed over RPC.", `Ensure properties are declared like \`get ${key}() { ... }\` instead of \`${key} = ...\`,`, `and methods are declared like \`${key}() { ... }\` instead of \`${key} = () => { ... }\`.` ].join("\n"); else message = `The RPC receiver does not implement ${quotedKey}.`; throw new TypeError(message); } return Reflect.get(ctor.prototype, key, instance); } /** * When calling RPC methods dynamically, we don't know whether the `property` * returned from `getSELFRPCProperty()` or `getDurableObjectRPCProperty()` below * is just a property or a method. If we just returned `property`, but the * client tried to call it as a method, `workerd` would throw an "x is not a * function" error. * * Instead, we return a *callable, custom thenable*. This behaves like a * function and a `Promise`! If `workerd` calls it, we'll wait for the promise * to resolve then forward the call. Otherwise, this just appears like a regular * async property. Note all client calls are async, so converting sync * properties and methods to async is fine here. * * Unfortunately, wrapping `property` with a `Proxy` and an `apply()` trap gives * `TypeError: Method Promise.prototype.then called on incompatible receiver #<Promise>`. :( */ function getRPCPropertyCallableThenable(key, property, queueOwner) { const fn = async function(...args) { return enqueueRPCInvocation(queueOwner, async (release) => { try { const maybeFn = await property; if (typeof maybeFn === "function") return maybeFn(...args); else throw new TypeError(`${JSON.stringify(key)} is not a function.`); } finally { release(); } }); }; fn.then = (onFulfilled, onRejected) => property.then(onFulfilled, onRejected); fn.catch = (onRejected) => property.catch(onRejected); fn.finally = (onFinally) => property.finally(onFinally); return fn; } const rpcInvocationQueues = /* @__PURE__ */ new WeakMap(); /** * Preserve the order in which dynamically-wrapped RPC methods begin executing. * * Resolving a property like `stub.method` may need to import user modules or * instantiate wrapper objects. If several calls are fired synchronously, those * async steps can otherwise complete out of order before the actual user method * is invoked. The queue is released as soon as invocation starts, so async RPC * completions can still run concurrently. */ async function enqueueRPCInvocation(owner, callback) { const previous = rpcInvocationQueues.get(owner) ?? Promise.resolve(); let releaseStarted; const started = new Promise((resolve) => { releaseStarted = resolve; }); const release = () => { const releaseStartedFn = releaseStarted; if (releaseStartedFn !== void 0) releaseStartedFn(); }; const result = previous.catch(() => {}).then(() => callback(release)); rpcInvocationQueues.set(owner, started); return result; } function getEntrypointState(instance) { return instance; } const WORKER_ENTRYPOINT_KEYS = [ "connect", "tailStream", "fetch", "tail", "trace", "scheduled", "queue", "test", "email" ]; const DURABLE_OBJECT_KEYS = [ "connect", "fetch", "alarm", "webSocketMessage", "webSocketClose", "webSocketError" ]; /** * Get the export to use for `entrypoint`. This is used for the `SELF` service * binding in `cloudflare:test`, which sets `entrypoint` to "default". * This requires importing the `main` module with Vite. */ async function getWorkerEntrypointExport(env$2, entrypoint) { const mainPath = getResolvedMainPath("service"); const mainModule = await importModule(mainPath); const entrypointValue = typeof mainModule === "object" && mainModule !== null && entrypoint in mainModule && mainModule[entrypoint]; if (!entrypointValue) { const message = `${mainPath} does not export a ${entrypoint} entrypoint. \`@cloudflare/vitest-pool-workers\` does not support service workers or named entrypoints for \`SELF\`.\nIf you're using service workers, please migrate to the modules format: https://developers.cloudflare.com/workers/reference/migrate-to-module-workers.`; throw new TypeError(message); } return { mainPath, entrypointValue }; } /** * Get a property named `key` from the user's `WorkerEntrypoint`. `wrapper` here * is an instance of a `WorkerEntrypoint` wrapper (i.e. the return value of * `createWorkerEntrypointWrapper()`). This requires importing the `main` module * with Vite, so will always return a `Promise.` */ async function getWorkerEntrypointRPCProperty(wrapper, entrypoint, key) { const { ctx } = getEntrypointState(wrapper); const { mainPath, entrypointValue } = await getWorkerEntrypointExport(env$1, entrypoint); return patchAndRunWithHandlerContext(ctx, () => { const env$2 = env$1; const expectedWorkerEntrypointMessage = `Expected ${entrypoint} export of ${mainPath} to be a subclass of \`WorkerEntrypoint\` for RPC`; if (typeof entrypointValue !== "function") throw new TypeError(expectedWorkerEntrypointMessage); const ctor = entrypointValue; const instance = new ctor(ctx, env$2); if (!(instance instanceof WorkerEntrypoint)) throw new TypeError(expectedWorkerEntrypointMessage); const value = getRPCProperty(ctor, instance, key); if (typeof value === "function") return (...args) => patchAndRunWithHandlerContext(ctx, () => value.apply(instance, args)); else return value; }); } function createWorkerEntrypointWrapper(entrypoint) { const Wrapper = createProxyPrototypeClass(WorkerEntrypoint, function(key) { if (DURABLE_OBJECT_KEYS.includes(key)) return; return getRPCPropertyCallableThenable(key, getWorkerEntrypointRPCProperty(this, entrypoint, key), this); }); for (const key of WORKER_ENTRYPOINT_KEYS) Wrapper.prototype[key] = async function(thing) { const { mainPath, entrypointValue } = await getWorkerEntrypointExport(this.env, entrypoint); return patchAndRunWithHandlerContext(this.ctx, () => { if (typeof entrypointValue === "object" && entrypointValue !== null) { const maybeFn = entrypointValue[key]; if (typeof maybeFn === "function") return maybeFn.call(entrypointValue, thing, env$1, this.ctx); else { const message = `Expected ${entrypoint} export of ${mainPath} to define a \`${key}()\` function`; throw new TypeError(message); } } else if (typeof entrypointValue === "function") { const instance = new entrypointValue(this.ctx, env$1); if (!(instance instanceof WorkerEntrypoint)) { const message = `Expected ${entrypoint} export of ${mainPath} to be a subclass of \`WorkerEntrypoint\``; throw new TypeError(message); } const maybeFn = instance[key]; if (typeof maybeFn === "function") return maybeFn.call(instance, thing); else { const message = `Expected ${entrypoint} export of ${mainPath} to define a \`${key}()\` method`; throw new TypeError(message); } } else { const message = `Expected ${entrypoint} export of ${mainPath} to be an object or a class, got ${entrypointValue}`; throw new TypeError(message); } }); }; return Wrapper; } const kInstanceConstructor = Symbol("kInstanceConstructor"); const kInstance = Symbol("kInstance"); const kEnsureInstance = Symbol("kEnsureInstance"); async function getDurableObjectRPCProperty(wrapper, className, key) { const { mainPath, instanceCtor, instance } = await wrapper[kEnsureInstance](); if (!(instance instanceof DurableObject)) { const message = `Expected ${className} exported by ${mainPath} be a subclass of \`DurableObject\` for RPC`; throw new TypeError(message); } const value = getRPCProperty(instanceCtor, instance, key); if (typeof value === "function") return value.bind(instance); else return value; } function createDurableObjectWrapper(className) { const Wrapper = createProxyPrototypeClass(DurableObject, function(key) { if (WORKER_ENTRYPOINT_KEYS.includes(key)) return; return getRPCPropertyCallableThenable(key, getDurableObjectRPCProperty(this, className, key), this); }); Wrapper.prototype[kEnsureInstance] = async function() { const { ctx, env: env$2 } = getEntrypointState(this); const mainPath = getResolvedMainPath("Durable Object"); const constructor = (await importModule(mainPath))[className]; if (typeof constructor !== "function") throw new TypeError(`${mainPath} does not export a ${className} Durable Object`); this[kInstanceConstructor] ??= constructor; if (this[kInstanceConstructor] !== constructor) { await ctx.blockConcurrencyWhile(() => { throw new Error(`${mainPath} changed, invalidating this Durable Object. Please retry the \`DurableObjectStub#fetch()\` call.`); }); assert.fail("Unreachable"); } if (this[kInstance] === void 0) { this[kInstance] = new this[kInstanceConstructor](ctx, env$2); await ctx.blockConcurrencyWhile(async () => {}); } return { mainPath, instanceCtor: this[kInstanceConstructor], instance: this[kInstance] }; }; Wrapper.prototype.fetch = async function(request) { const { ctx } = getEntrypointState(this); const { mainPath, instance } = await this[kEnsureInstance](); const response = await maybeHandleRunRequest(request, instance, ctx); if (response !== void 0) return response; if (instance.fetch === void 0) { const message = `${className} exported by ${mainPath} does not define a \`fetch()\` method`; throw new TypeError(message); } return instance.fetch(request); }; for (const key of DURABLE_OBJECT_KEYS) { if (key === "fetch") continue; Wrapper.prototype[key] = async function(...args) { const { mainPath, instance } = await this[kEnsureInstance](); const maybeFn = instance[key]; if (typeof maybeFn === "function") return maybeFn.apply(instance, args); else { const message = `${className} exported by ${mainPath} does not define a \`${key}()\` method`; throw new TypeError(message); } }; } return Wrapper; } function createWorkflowEntrypointWrapper(entrypoint) { const Wrapper = createProxyPrototypeClass(WorkflowEntrypoint, function(key) { if (!["run"].includes(key)) return; return getRPCPropertyCallableThenable(key, getWorkerEntrypointRPCProperty(this, entrypoint, key), this); }); Wrapper.prototype.run = async function(...args) { const { mainPath, entrypointValue } = await getWorkerEntrypointExport(env$1, entrypoint); if (typeof entrypointValue === "function") { const instance = new entrypointValue(this.ctx, env$1); if (!(instance instanceof WorkflowEntrypoint)) { const message = `Expected ${entrypoint} export of ${mainPath} to be a subclass of \`WorkflowEntrypoint\``; throw new TypeError(message); } const maybeFn = instance["run"]; if (typeof maybeFn === "function") return patchAndRunWithHandlerContext(this.ctx, () => maybeFn.call(instance, ...args)); else { const message = `Expected ${entrypoint} export of ${mainPath} to define a \`run()\` method, but got ${typeof maybeFn}`; throw new TypeError(message); } } else { const message = `Expected ${entrypoint} export of ${mainPath} to be a subclass of \`WorkflowEntrypoint\`, but got ${entrypointValue}`; throw new TypeError(message); } }; return Wrapper; } //#endregion //#region src/worker/events.ts const kConstructFlag = Symbol("kConstructFlag"); const kWaitUntil = Symbol("kWaitUntil"); var ExecutionContext = class ExecutionContext { [kWaitUntil] = []; constructor(flag) { if (flag !== kConstructFlag) throw new TypeError("Illegal constructor"); } exports = isCtxExportsEnabled(exports) ? getCtxExportsProxy(exports) : void 0; waitUntil(promise) { if (!(this instanceof ExecutionContext)) throw new TypeError("Illegal invocation"); this[kWaitUntil].push(promise); registerGlobalWaitUntil(promise); } passThroughOnException() {} }; function createExecutionContext() { return new ExecutionContext(kConstructFlag); } function isExecutionContextLike(v) { return typeof v === "object" && v !== null && kWaitUntil in v && Array.isArray(v[kWaitUntil]); } async function waitOnExecutionContext(ctx) { if (!isExecutionContextLike(ctx)) throw new TypeError("Failed to execute 'getWaitUntil': parameter 1 is not of type 'ExecutionContext'.\nYou must call 'createExecutionContext()' or 'createPagesEventContext()' to get an 'ExecutionContext' instance."); return waitForWaitUntil(ctx[kWaitUntil]); } var ScheduledController = class ScheduledController { scheduledTime; cron; constructor(flag, options) { if (flag !== kConstructFlag) throw new TypeError("Illegal constructor"); const scheduledTime = Number(options?.scheduledTime ?? Date.now()); const cron = String(options?.cron ?? ""); Object.defineProperties(this, { scheduledTime: { get() { return scheduledTime; } }, cron: { get() { return cron; } } }); } noRetry() { if (!(this instanceof ScheduledController)) throw new TypeError("Illegal invocation"); } }; function createScheduledController(options) { if (options !== void 0 && typeof options !== "object") throw new TypeError("Failed to execute 'createScheduledController': parameter 1 is not of type 'ScheduledOptions'."); return new ScheduledController(kConstructFlag, options); } const kRetry = Symbol("kRetry"); const kAck = Symbol("kAck"); const kRetryAll = Symbol("kRetryAll"); const kAckAll = Symbol("kAckAll"); var QueueMessage = class QueueMessage { #controller; id; timestamp; body; attempts; [kRetry] = false; [kAck] = false; constructor(flag, controller, message) { if (flag !== kConstructFlag) throw new TypeError("Illegal constructor"); this.#controller = controller; const id = String(message.id); let timestamp; if (typeof message.timestamp === "number") timestamp = new Date(message.timestamp); else if (message.timestamp instanceof Date) timestamp = new Date(message.timestamp.getTime()); else throw new TypeError("Incorrect type for the 'timestamp' field on 'ServiceBindingQueueMessage': the provided value is not of type 'date'."); let attempts; if (typeof message.attempts === "number") attempts = message.attempts; else throw new TypeError("Incorrect type for the 'attempts' field on 'ServiceBindingQueueMessage': the provided value is not of type 'number'."); if ("serializedBody" in message) throw new TypeError("Cannot use `serializedBody` with `createMessageBatch()`"); const body = structuredClone(message.body); Object.defineProperties(this, { id: { get() { return id; } }, timestamp: { get() { return timestamp; } }, body: { get() { return body; } }, attempts: { get() { return attempts; } } }); } retry() { if (!(this instanceof QueueMessage)) throw new TypeError("Illegal invocation"); if (this.#controller[kRetryAll]) return; if (this.#controller[kAckAll]) { console.warn(`Received a call to retry() on message ${this.id} after ackAll() was already called. Calling retry() on a message after calling ackAll() has no effect.`); return; } if (this[kAck]) { console.warn(`Received a call to retry() on message ${this.id} after ack() was already called. Calling retry() on a message after calling ack() has no effect.`); return; } this[kRetry] = true; } ack() { if (!(this instanceof QueueMessage)) throw new TypeError("Illegal invocation"); if (this.#controller[kAckAll]) return; if (this.#controller[kRetryAll]) { console.warn(`Received a call to ack() on message ${this.id} after retryAll() was already called. Calling ack() on a message after calling retryAll() has no effect.`); return; } if (this[kRetry]) { console.warn(`Received a call to ack() on message ${this.id} after retry() was already called. Calling ack() on a message after calling retry() has no effect.`); return; } this[kAck] = true; } }; var QueueController = class QueueController { queue; messages; metadata; [kRetryAll] = false; [kAckAll] = false; constructor(flag, queueOption, messagesOption) { if (flag !== kConstructFlag) throw new TypeError("Illegal constructor"); const queue = String(queueOption); const messages = messagesOption.map((message) => new QueueMessage(kConstructFlag, this, message)); const metadata = { metrics: { backlogCount: 0, backlogBytes: 0, oldestMessageTimestamp: /* @__PURE__ */ new Date(0) } }; Object.defineProperties(this, { queue: { get() { return queue; } }, messages: { get() { return messages; } }, metadata: { get() { return metadata; } } }); } retryAll() { if (!(this instanceof QueueController)) throw new TypeError("Illegal invocation"); if (this[kAckAll]) { console.warn("Received a call to retryAll() after ackAll() was already called. Calling retryAll() after calling ackAll() has no effect."); return; } this[kRetryAll] = true; } ackAll() { if (!(this instanceof QueueController)) throw new TypeError("Illegal invocation"); if (this[kRetryAll]) { console.warn("Received a call to ackAll() after retryAll() was already called. Calling ackAll() after calling retryAll() has no effect."); return; } this[kAckAll] = true; } }; function createMessageBatch(queueName, messages) { if (arguments.length === 0) throw new TypeError("Failed to execute 'createMessageBatch': parameter 1 is not of type 'string'."); if (!Array.isArray(messages)) throw new TypeError("Failed to execute 'createMessageBatch': parameter 2 is not of type 'Array'."); return new QueueController(kConstructFlag, queueName, messages); } async function getQueueResult(batch, ctx) { if (!(batch instanceof QueueController)) throw new TypeError("Failed to execute 'getQueueResult': parameter 1 is not of type 'MessageBatch'.\nYou must call 'createMessageBatch()' to get a 'MessageBatch' instance."); if (!(ctx instanceof ExecutionContext)) throw new TypeError("Failed to execute 'getQueueResult': parameter 2 is not of type 'ExecutionContext'.\nYou must call 'createExecutionContext()' to get an 'ExecutionContext' instance."); await waitOnExecutionContext(ctx); const retryMessages = []; const explicitAcks = []; for (const message of batch.messages) { if (message[kRetry]) retryMessages.push({ msgId: message.id }); if (message[kAck]) explicitAcks.push(message.id); } return { outcome: "ok", retryBatch: { retry: batch[kRetryAll] }, ackAll: batch[kAckAll], retryMessages, explicitAcks }; } function hasASSETSServiceBinding(value) { return "ASSETS" in value && typeof value.ASSETS === "object" && value.ASSETS !== null && "fetch" in value.ASSETS && typeof value.ASSETS.fetch === "function"; } function createPagesEventContext(opts) { if (typeof opts !== "object" || opts === null) throw new TypeError("Failed to execute 'createPagesEventContext': parameter 1 is not of type 'EventContextInit'."); if (!(opts.request instanceof Request)) throw new TypeError("Incorrect type for the 'request' field on 'EventContextInit': the provided value is not of type 'Request'."); if (opts.functionPath !== void 0 && typeof opts.functionPath !== "string") throw new TypeError("Incorrect type for the 'functionPath' field on 'EventContextInit': the provided value is not of type 'string'."); if (opts.next !== void 0 && typeof opts.next !== "function") throw new TypeError("Incorrect type for the 'next' field on 'EventContextInit': the provided value is not of type 'function'."); if (opts.params !== void 0 && !(typeof opts.params === "object" && opts.params !== null)) throw new TypeError("Incorrect type for the 'params' field on 'EventContextInit': the provided value is not of type 'object'."); if (opts.data !== void 0 && !(typeof opts.data === "object" && opts.data !== null)) throw new TypeError("Incorrect type for the 'data' field on 'EventContextInit': the provided value is not of type 'object'."); if (!hasASSETSServiceBinding(env)) throw new TypeError("Cannot call `createPagesEventContext()` without defining `ASSETS` service binding"); const ctx = createExecutionContext(); return { request: opts.next ? opts.request.clone() : opts.request, functionPath: opts.functionPath ?? "", [kWaitUntil]: ctx[kWaitUntil], waitUntil: ctx.waitUntil.bind(ctx), passThroughOnException: ctx.passThroughOnException.bind(ctx), async next(nextInput, nextInit) { if (opts.next === void 0) throw new TypeError("Cannot call `EventContext#next()` without including `next` property in 2nd argument to `createPagesEventContext()`"); if (nextInput === void 0) return opts.next(opts.request); else { if (typeof nextInput === "string") nextInput = new URL(nextInput, opts.request.url).toString(); const nextRequest = new Request(nextInput, nextInit); return opts.next(nextRequest); } }, env, params: opts.params ?? {}, data: opts.data ?? {} }; } //#endregion //#region src/worker/reset.ts async function reset() { await workerdUnsafe.deleteAllDurableObjects(); } async function abortAllDurableObjects() { await workerdUnsafe.abortAllDurableObjects(); } //#endregion //#region src/worker/secrets-store.ts const ADMIN_API = "SecretsStoreSecret::admin_api"; /** * Returns the admin API for a secrets store binding, allowing tests to * create, update, and delete secrets that would otherwise be read-only. * * ```ts * import { adminSecretsStore } from "cloudflare:test"; * * const admin = adminSecretsStore(env.MY_SECRET); * await admin.create("my-secret-value"); * ``` */ function adminSecretsStore(binding) { if (typeof binding !== "object" || binding === null || typeof binding[ADMIN_API] !== "function") throw new TypeError("Failed to execute 'adminSecretsStore': parameter 1 is not a secrets store binding."); return binding[ADMIN_API](); } //#endregion //#region ../workflows-shared/src/instance.ts let InstanceStatus = /* @__PURE__ */ function(InstanceStatus$1) { InstanceStatus$1[InstanceStatus$1["Queued"] = 0] = "Queued"; InstanceStatus$1[InstanceStatus$1["Running"] = 1] = "Running"; InstanceStatus$1[InstanceStatus$1["Paused"] = 2] = "Paused"; InstanceStatus$1[InstanceStatus$1["Errored"] = 3] = "Errored"; InstanceStatus$1[InstanceStatus$1["Terminated"] = 4] = "Terminated"; InstanceStatus$1[InstanceStatus$1["Complete"] = 5] = "Complete"; InstanceStatus$1[InstanceStatus$1["WaitingForPause"] = 6] = "WaitingForPause"; InstanceStatus$1[InstanceStatus$1["Waiting"] = 7] = "Waiting"; return InstanceStatus$1; }({}); function instanceStatusName(status) { switch (status) { case InstanceStatus.Queued: return "queued"; case InstanceStatus.Running: return "running"; case InstanceStatus.Paused: return "paused"; case InstanceStatus.Errored: return "errored"; case InstanceStatus.Terminated: return "terminated"; case InstanceStatus.Complete: return "complete"; case InstanceStatus.WaitingForPause: return "waitingForPause"; case InstanceStatus.Waiting: return "waiting"; default: return "unknown"; } } //#endregion //#region src/worker/workflows.ts async function introspectWorkflowInstance(workflow, instanceId) { if (!workflow || !instanceId) throw new Error("[WorkflowIntrospector] Workflow binding and instance id are required."); return new WorkflowInstanceIntrospectorHandle(workflow, instanceId); } var WorkflowInstanceIntrospectorHandle = class { #workflow; #instanceId; #instanceModifier; #instanceModifierPromise; constructor(workflow, instanceId) { this.#workflow = workflow; this.#instanceId = instanceId; this.#instanceModifierPromise = workflow.unsafeGetInstanceModifier(instanceId).then((res) => { this.#instanceModifier = res; this.#instanceModifierPromise = void 0; return this.#instanceModifier; }); } async modify(fn) { if (this.#instanceModifierPromise !== void 0) this.#instanceModifier = await this.#instanceModifierPromise; if (this.#instanceModifier === void 0) throw new Error("could not apply modifications due to internal error. Retrying the test may resolve the issue."); await fn(this.#instanceModifier); return this; } async waitForStepResult(step) { return await this.#workflow.unsafeWaitForStepResult(this.#instanceId, step.name, step.index); } async waitForStatus(status) { if (status === instanceStatusName(InstanceStatus.Queued)) return; await this.#workflow.unsafeWaitForStatus(this.#instanceId, status); } async getOutput() { return await this.#workflow.unsafeGetOutputOrError(this.#instanceId, true); } async getError() { return await this.#workflow.unsafeGetOutputOrError(this.#instanceId, false); } async dispose() { await this.#workflow.unsafeAbort(this.#instanceId, "Instance dispose"); } async [Symbol.asyncDispose]() { await this.dispose(); } }; async function introspectWorkflow(workflow) { if (!workflow) throw new Error("[WorkflowIntrospector] Workflow binding is required."); const modifierCallbacks = []; const instanceIntrospectors = []; const bindingName = await workflow.unsafeGetBindingName(); const originalWorkflow = env$1[bindingName]; const introspectAndModifyInstance = async (instanceId) => { try { await runInRunnerObject(async () => { const introspector = await introspectWorkflowInstance(workflow, instanceId); instanceIntrospectors.push(introspector); for (const callback of modifierCallbacks) await introspector.modify(callback); }); } catch (error) { console.error(`[WorkflowIntrospector] Error during introspection for instance ${instanceId}:`, error); throw new Error(`[WorkflowIntrospector] Failed to introspect Workflow instance ${instanceId}.`); } }; const createWorkflowProxyGetHandler = () => { return (target, property) => { if (property === "create") return new Proxy(target[property], { async apply(func, thisArg, argArray) { if (!Object.hasOwn(argArray[0] ?? {}, "id")) argArray = [{ id: crypto.randomUUID(), ...argArray[0] ?? {} }]; const instanceId = argArray[0].id; await introspectAndModifyInstance(instanceId); return target[property](...argArray); } }); if (property === "createBatch") return new Proxy(target[property], { async apply(func, thisArg, argArray) { for (const [index, arg] of argArray[0]?.entries() ?? []) if (!Object.hasOwn(arg, "id")) argArray[0][index] = { id: crypto.randomUUID(), ...arg }; await Promise.all(argArray[0].map((options) => introspectAndModifyInstance(options.id))); const createPromises = (argArray[0] ?? []).map((arg) => target["create"](arg)); return Promise.all(createPromises); } }); return target[property]; }; }; const dispose = () => { env$1[bindingName] = originalWorkflow; }; const proxyGetHandler = createWorkflowProxyGetHandler(); env$1[bindingName] = new Proxy(originalWorkflow, { get: proxyGetHandler }); return new WorkflowIntrospectorHandle(workflow, modifierCallbacks, instanceIntrospectors, dispose); } var WorkflowIntrospectorHandle = class { workflow; #modifierCallbacks; #instanceIntrospectors; #disposeCallback; constructor(workflow, modifierCallbacks, instanceIntrospectors, disposeCallback) { this.workflow = workflow; this.#modifierCallbacks = modifierCallbacks; this.#instanceIntrospectors = instanceIntrospectors; this.#disposeCallback = disposeCallback; } async modifyAll(fn) { this.#modifierCallbacks.push(fn); } get() { return this.#instanceIntrospectors; } async dispose() { this.#disposeCallback(); const introspectors = this.#instanceIntrospectors; this.#modifierCallbacks = []; this.#instanceIntrospectors = []; await Promise.all(introspectors.map((introspector) => introspector.dispose())); } async [Symbol.asyncDispose]() { await this.dispose(); } }; //#endregion export { SELF, abortAllDurableObjects, adminSecretsStore, applyD1Migrations, createDurableObjectWrapper, createExecutionContext, createMessageBatch, createPagesEventContext, createScheduledController, createWorkerEntrypointWrapper, createWorkflowEntrypointWrapper, env, getQueueResult, getResolvedMainPath, getSerializedOptions, handlerContextStore, introspectWorkflow, introspectWorkflowInstance, listDurableObjectIds, maybeHandleRunRequest, registerGlobalWaitUntil, registerHandlerAndGlobalWaitUntil, reset, runDurableObjectAlarm, runInDurableObject, runInRunnerObject, setWaitUntilTimeout, waitForGlobalWaitUntil, waitForWaitUntil, waitOnExecutionContext }; //# sourceMappingURL=test-internal.mjs.map