@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
JavaScript
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