effect-cf 0.23.1 → 0.24.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/CHANGELOG.md +8 -0
- package/README.md +151 -0
- package/dist/{Hyperdrive-COyg7R26.d.mts → Binding-Bq_FPzlt.d.mts} +4 -36
- package/dist/DurableObjectState-8sgBJB0s.mjs +384 -0
- package/dist/Hyperdrive-pEMnh3W6.d.mts +36 -0
- package/dist/HyperdrivePg.d.mts +2 -1
- package/dist/Vitest.d.mts +192 -0
- package/dist/Vitest.mjs +184 -0
- package/dist/WorkflowBinding-BECGNTzu.d.mts +1081 -0
- package/dist/index.d.mts +80 -1153
- package/dist/index.mjs +2 -382
- package/package.json +10 -1
- package/src/Vitest.ts +500 -0
package/src/Vitest.ts
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-native helpers for tests running in Cloudflare's Vitest Workers pool.
|
|
3
|
+
*
|
|
4
|
+
* Import this module through the `effect-cf/vitest` subpath. It is intended to
|
|
5
|
+
* run inside the Workers test runtime, where `cloudflare:test` is available.
|
|
6
|
+
*/
|
|
7
|
+
import { env, type DurableObject as RpcDurableObject } from "cloudflare:workers";
|
|
8
|
+
import {
|
|
9
|
+
abortAllDurableObjects as abortAllDurableObjectsPromise,
|
|
10
|
+
adminSecretsStore as adminSecretsStoreNative,
|
|
11
|
+
applyD1Migrations as applyD1MigrationsPromise,
|
|
12
|
+
createExecutionContext,
|
|
13
|
+
createMessageBatch,
|
|
14
|
+
createPagesEventContext,
|
|
15
|
+
createScheduledController,
|
|
16
|
+
evictAllDurableObjects as evictAllDurableObjectsPromise,
|
|
17
|
+
evictDurableObject as evictDurableObjectPromise,
|
|
18
|
+
getQueueResult,
|
|
19
|
+
introspectWorkflow as introspectWorkflowPromise,
|
|
20
|
+
introspectWorkflowInstance as introspectWorkflowInstancePromise,
|
|
21
|
+
listDurableObjectIds as listDurableObjectIdsPromise,
|
|
22
|
+
reset as resetPromise,
|
|
23
|
+
runDurableObjectAlarm as runDurableObjectAlarmPromise,
|
|
24
|
+
runInDurableObject as runInDurableObjectPromise,
|
|
25
|
+
waitOnExecutionContext,
|
|
26
|
+
} from "cloudflare:test";
|
|
27
|
+
import { ConfigProvider, Effect, Exit, Layer } from "effect";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
DurableObjectState,
|
|
31
|
+
type DurableObjectStateService,
|
|
32
|
+
fromDurableObjectState,
|
|
33
|
+
} from "./DurableObjectState";
|
|
34
|
+
import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
|
|
35
|
+
import type { WorkflowInstanceStatusName } from "./WorkflowBinding";
|
|
36
|
+
|
|
37
|
+
const currentEnv: WorkerEnv = env;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Provides the Workers pool environment as both {@link WorkerEnvironment} and
|
|
41
|
+
* the active Effect `ConfigProvider`.
|
|
42
|
+
*/
|
|
43
|
+
export const layer: Layer.Layer<WorkerEnvironment> = Layer.mergeAll(
|
|
44
|
+
Layer.succeed(WorkerEnvironment, currentEnv),
|
|
45
|
+
ConfigProvider.layer(WorkerConfig.providerFromEnv(currentEnv)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Constructor shape shared by Effect-backed Worker entrypoints. */
|
|
49
|
+
export interface WorkerConstructor<Instance> {
|
|
50
|
+
new (context: globalThis.ExecutionContext, env: WorkerEnv): Instance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates an `ExecutionContext` and drains all `waitUntil` work before the
|
|
55
|
+
* returned effect completes, including when `use` fails or is interrupted.
|
|
56
|
+
*/
|
|
57
|
+
export const withExecutionContext = Effect.fn("effect-cf/vitest/withExecutionContext")(function* <
|
|
58
|
+
A,
|
|
59
|
+
E,
|
|
60
|
+
R,
|
|
61
|
+
>(use: (context: globalThis.ExecutionContext) => Effect.Effect<A, E, R>) {
|
|
62
|
+
return yield* Effect.acquireUseRelease(Effect.sync(createExecutionContext), use, (context) =>
|
|
63
|
+
Effect.promise(() => waitOnExecutionContext(context)),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
interface FetchWorker {
|
|
68
|
+
fetch(request: Request): Promise<Response>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Invokes an Effect-backed Worker fetch handler and drains its `waitUntil`
|
|
73
|
+
* work before returning the response.
|
|
74
|
+
*/
|
|
75
|
+
export const fetch = Effect.fn("effect-cf/vitest/fetch")(function* <Instance extends FetchWorker>(
|
|
76
|
+
Worker: WorkerConstructor<Instance>,
|
|
77
|
+
request: Request,
|
|
78
|
+
workerEnv: WorkerEnv = currentEnv,
|
|
79
|
+
) {
|
|
80
|
+
return yield* withExecutionContext(
|
|
81
|
+
Effect.fnUntraced(function* (context) {
|
|
82
|
+
const worker = new Worker(context, workerEnv);
|
|
83
|
+
|
|
84
|
+
return yield* Effect.promise(() => worker.fetch(request));
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** Modules-format scheduled handler accepted by {@link scheduled}. */
|
|
90
|
+
export type ScheduledHandler = NonNullable<globalThis.ExportedHandler<WorkerEnv>["scheduled"]>;
|
|
91
|
+
|
|
92
|
+
/** Options used to construct a scheduled event controller. */
|
|
93
|
+
export interface ScheduledOptions {
|
|
94
|
+
readonly scheduledTime?: number | Date;
|
|
95
|
+
readonly cron?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Invokes a modules-format scheduled handler and drains its `waitUntil` work.
|
|
100
|
+
*/
|
|
101
|
+
export const scheduled = Effect.fn("effect-cf/vitest/scheduled")(function* (
|
|
102
|
+
handler: ScheduledHandler,
|
|
103
|
+
options?: ScheduledOptions,
|
|
104
|
+
workerEnv: WorkerEnv = currentEnv,
|
|
105
|
+
) {
|
|
106
|
+
const controller = createScheduledController(options);
|
|
107
|
+
|
|
108
|
+
yield* withExecutionContext((context) =>
|
|
109
|
+
Effect.promise(async () => handler(controller, workerEnv, context)),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return controller;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
type AnyPagesFunction = globalThis.PagesFunction<WorkerEnv, string, any>;
|
|
116
|
+
|
|
117
|
+
interface PagesEventContextInitBase {
|
|
118
|
+
readonly request: Request<unknown, IncomingRequestCfProperties>;
|
|
119
|
+
readonly functionPath?: string;
|
|
120
|
+
readonly next?: (request: Request) => Response | Promise<Response>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type PagesEventContextInitParams<Context> =
|
|
124
|
+
Context extends globalThis.EventContext<unknown, infer Params, unknown>
|
|
125
|
+
? [Params] extends [never]
|
|
126
|
+
? { readonly params?: Record<string, never> }
|
|
127
|
+
: { readonly params: Record<Params, string | Array<string>> }
|
|
128
|
+
: never;
|
|
129
|
+
|
|
130
|
+
type PagesEventContextInitData<Context> =
|
|
131
|
+
Context extends globalThis.EventContext<unknown, string, infer Data>
|
|
132
|
+
? Data extends Record<string, never>
|
|
133
|
+
? { readonly data?: Data }
|
|
134
|
+
: { readonly data: Data }
|
|
135
|
+
: never;
|
|
136
|
+
|
|
137
|
+
/** Initialization accepted by Pool Workers for a Pages Function. */
|
|
138
|
+
export type PagesEventContextInit<Function extends AnyPagesFunction> = PagesEventContextInitBase &
|
|
139
|
+
PagesEventContextInitParams<Parameters<Function>[0]> &
|
|
140
|
+
PagesEventContextInitData<Parameters<Function>[0]>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Invokes a Pages Function with a typed event context and drains its
|
|
144
|
+
* `waitUntil` work, including when the function fails.
|
|
145
|
+
*/
|
|
146
|
+
export const pages = Effect.fn("effect-cf/vitest/pages")(function* <
|
|
147
|
+
Function extends AnyPagesFunction,
|
|
148
|
+
>(handler: Function, init: PagesEventContextInit<Function>) {
|
|
149
|
+
return yield* Effect.acquireUseRelease(
|
|
150
|
+
Effect.sync(() =>
|
|
151
|
+
createPagesEventContext<Function>(
|
|
152
|
+
init as Parameters<typeof createPagesEventContext<Function>>[0],
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
(context) => Effect.promise(async () => handler(context)),
|
|
156
|
+
(context) => Effect.promise(() => waitOnExecutionContext(context)),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
interface QueueWorker {
|
|
161
|
+
queue(batch: globalThis.MessageBatch): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Input message accepted by Pool Workers' `createMessageBatch` helper. */
|
|
165
|
+
export type QueueMessage<Body> = {
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly timestamp: Date;
|
|
168
|
+
readonly attempts: number;
|
|
169
|
+
} & ({ readonly body: Body } | { readonly serializedBody: ArrayBuffer | ArrayBufferView });
|
|
170
|
+
|
|
171
|
+
/** Acknowledgement and retry result returned by Pool Workers. */
|
|
172
|
+
export type QueueResult = Awaited<ReturnType<typeof getQueueResult>>;
|
|
173
|
+
|
|
174
|
+
/** Result of invoking a queue consumer through {@link queue}. */
|
|
175
|
+
export interface QueueRunResult<Body> {
|
|
176
|
+
readonly batch: globalThis.MessageBatch<Body>;
|
|
177
|
+
readonly result: QueueResult;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Invokes an Effect-backed queue consumer, drains its `waitUntil` work, and
|
|
182
|
+
* returns Pool Workers' acknowledgement and retry result.
|
|
183
|
+
*/
|
|
184
|
+
export const queue = Effect.fn("effect-cf/vitest/queue")(function* <
|
|
185
|
+
Body,
|
|
186
|
+
Instance extends QueueWorker,
|
|
187
|
+
>(
|
|
188
|
+
Worker: WorkerConstructor<Instance>,
|
|
189
|
+
queueName: string,
|
|
190
|
+
messages: ReadonlyArray<QueueMessage<Body>>,
|
|
191
|
+
workerEnv: WorkerEnv = currentEnv,
|
|
192
|
+
): Effect.fn.Return<QueueRunResult<Body>> {
|
|
193
|
+
return yield* withExecutionContext(
|
|
194
|
+
Effect.fnUntraced(function* (context) {
|
|
195
|
+
const batch = createMessageBatch<Body>(queueName, [...messages]);
|
|
196
|
+
const worker = new Worker(context, workerEnv);
|
|
197
|
+
|
|
198
|
+
yield* Effect.promise(() => worker.queue(batch));
|
|
199
|
+
|
|
200
|
+
const result = yield* Effect.promise(() => getQueueResult(batch, context));
|
|
201
|
+
|
|
202
|
+
return { batch, result } satisfies QueueRunResult<Body>;
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Runs an Effect inside the I/O context of a Durable Object instance.
|
|
209
|
+
*
|
|
210
|
+
* The caller's Effect context is preserved, and the instance's native state is
|
|
211
|
+
* provided as {@link DurableObjectState}. A wrapped state service is also
|
|
212
|
+
* passed to `use` for direct access.
|
|
213
|
+
*/
|
|
214
|
+
export const runInDurableObject = Effect.fn("effect-cf/vitest/runInDurableObject")(function* <
|
|
215
|
+
Object extends RpcDurableObject,
|
|
216
|
+
A,
|
|
217
|
+
E,
|
|
218
|
+
R,
|
|
219
|
+
>(
|
|
220
|
+
stub: globalThis.DurableObjectStub<Object>,
|
|
221
|
+
use: (instance: Object, state: DurableObjectStateService) => Effect.Effect<A, E, R>,
|
|
222
|
+
): Effect.fn.Return<A, E, Exclude<R, DurableObjectState>> {
|
|
223
|
+
const context = yield* Effect.context<Exclude<R, DurableObjectState>>();
|
|
224
|
+
const exit = yield* Effect.promise(() =>
|
|
225
|
+
runInDurableObjectPromise(stub, (instance, nativeState) => {
|
|
226
|
+
const state = fromDurableObjectState(nativeState);
|
|
227
|
+
const effect = Effect.provideService(use(instance, state), DurableObjectState, state);
|
|
228
|
+
|
|
229
|
+
return Effect.runPromiseExitWith(context)(effect);
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return yield* exit;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/** Immediately runs and removes a scheduled Durable Object alarm. */
|
|
237
|
+
export const runDurableObjectAlarm = Effect.fn("effect-cf/vitest/runDurableObjectAlarm")(function* (
|
|
238
|
+
stub: globalThis.DurableObjectStub<RpcDurableObject>,
|
|
239
|
+
) {
|
|
240
|
+
return yield* Effect.promise(() => runDurableObjectAlarmPromise(stub));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** Options controlling how Durable Object eviction handles WebSockets. */
|
|
244
|
+
export interface DurableObjectEvictionOptions {
|
|
245
|
+
readonly webSockets?: "close" | "hibernate";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Gracefully evicts one running Durable Object while preserving durable storage. */
|
|
249
|
+
export const evictDurableObject = Effect.fn("effect-cf/vitest/evictDurableObject")(function* (
|
|
250
|
+
stub: globalThis.DurableObjectStub<RpcDurableObject>,
|
|
251
|
+
options?: DurableObjectEvictionOptions,
|
|
252
|
+
) {
|
|
253
|
+
yield* Effect.promise(() => evictDurableObjectPromise(stub, options));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/** Lists all IDs created in a Durable Object namespace. */
|
|
257
|
+
export const listDurableObjectIds = Effect.fn("effect-cf/vitest/listDurableObjectIds")(function* <
|
|
258
|
+
Object extends RpcDurableObject | undefined,
|
|
259
|
+
>(namespace: globalThis.DurableObjectNamespace<Object>) {
|
|
260
|
+
return yield* Effect.promise(() => listDurableObjectIdsPromise(namespace));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/** Deletes data from all bindings attached to the current test Worker. */
|
|
264
|
+
export const reset = Effect.fn("effect-cf/vitest/reset")(function* () {
|
|
265
|
+
yield* Effect.promise(resetPromise);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/** Aborts all Durable Object instances without deleting their persisted data. */
|
|
269
|
+
export const abortAllDurableObjects = Effect.fn("effect-cf/vitest/abortAllDurableObjects")(
|
|
270
|
+
function* () {
|
|
271
|
+
yield* Effect.promise(abortAllDurableObjectsPromise);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/** Gracefully evicts all running Durable Objects while preserving durable storage. */
|
|
276
|
+
export const evictAllDurableObjects = Effect.fn("effect-cf/vitest/evictAllDurableObjects")(
|
|
277
|
+
function* (options?: DurableObjectEvictionOptions) {
|
|
278
|
+
yield* Effect.promise(() => evictAllDurableObjectsPromise(options));
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
/** A D1 migration accepted by Pool Workers. */
|
|
283
|
+
export interface D1Migration {
|
|
284
|
+
readonly name: string;
|
|
285
|
+
readonly queries: ReadonlyArray<string>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Applies all D1 migrations that have not already been recorded. */
|
|
289
|
+
export const applyD1Migrations = Effect.fn("effect-cf/vitest/applyD1Migrations")(function* (
|
|
290
|
+
database: globalThis.D1Database,
|
|
291
|
+
migrations: ReadonlyArray<D1Migration>,
|
|
292
|
+
migrationsTableName?: string,
|
|
293
|
+
) {
|
|
294
|
+
yield* Effect.promise(() =>
|
|
295
|
+
applyD1MigrationsPromise(
|
|
296
|
+
database,
|
|
297
|
+
migrations.map(({ name, queries }) => ({ name, queries: [...queries] })),
|
|
298
|
+
migrationsTableName,
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
type SecretsStoreBinding = Parameters<typeof adminSecretsStoreNative>[0];
|
|
304
|
+
type NativeSecretsStoreAdmin = ReturnType<typeof adminSecretsStoreNative>;
|
|
305
|
+
|
|
306
|
+
/** Secret metadata returned by {@link SecretsStoreAdmin.list}. */
|
|
307
|
+
export interface SecretsStoreSecret {
|
|
308
|
+
readonly name: string;
|
|
309
|
+
readonly metadata?: { readonly uuid: string };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Effect-native wrapper around Pool Workers' Secrets Store admin API. */
|
|
313
|
+
export interface SecretsStoreAdmin {
|
|
314
|
+
readonly raw: NativeSecretsStoreAdmin;
|
|
315
|
+
readonly create: (value: string) => Effect.Effect<string>;
|
|
316
|
+
readonly update: (value: string, id: string) => Effect.Effect<string>;
|
|
317
|
+
readonly duplicate: (id: string, newName: string) => Effect.Effect<string>;
|
|
318
|
+
readonly delete: (id: string) => Effect.Effect<void>;
|
|
319
|
+
readonly list: Effect.Effect<Array<SecretsStoreSecret>>;
|
|
320
|
+
readonly get: (id: string) => Effect.Effect<string>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Acquires an Effect-native admin client for a Secrets Store binding. */
|
|
324
|
+
export const adminSecretsStore = Effect.fn("effect-cf/vitest/adminSecretsStore")(function* (
|
|
325
|
+
binding: SecretsStoreBinding,
|
|
326
|
+
): Effect.fn.Return<SecretsStoreAdmin> {
|
|
327
|
+
const admin = yield* Effect.sync(() => adminSecretsStoreNative(binding));
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
raw: admin,
|
|
331
|
+
create: (value) => Effect.promise(() => admin.create(value)),
|
|
332
|
+
update: (value, id) => Effect.promise(() => admin.update(value, id)),
|
|
333
|
+
duplicate: (id, newName) => Effect.promise(() => admin.duplicate(id, newName)),
|
|
334
|
+
delete: (id) => Effect.promise(() => admin.delete(id)),
|
|
335
|
+
list: Effect.promise(() => admin.list()),
|
|
336
|
+
get: (id) => Effect.promise(() => admin.get(id)),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
type WorkflowBinding = Parameters<typeof introspectWorkflowPromise>[0];
|
|
341
|
+
type NativeWorkflowIntrospector = Awaited<ReturnType<typeof introspectWorkflowPromise>>;
|
|
342
|
+
type NativeWorkflowInstanceIntrospector = Awaited<
|
|
343
|
+
ReturnType<typeof introspectWorkflowInstancePromise>
|
|
344
|
+
>;
|
|
345
|
+
type NativeWorkflowInstanceModifier = Parameters<
|
|
346
|
+
Parameters<NativeWorkflowInstanceIntrospector["modify"]>[0]
|
|
347
|
+
>[0];
|
|
348
|
+
|
|
349
|
+
/** Identifies one occurrence of a Workflow step or sleep. */
|
|
350
|
+
export interface WorkflowStepTarget {
|
|
351
|
+
readonly name: string;
|
|
352
|
+
readonly index?: number;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Event delivered to a mocked Workflow `waitForEvent` operation. */
|
|
356
|
+
export interface WorkflowMockEvent {
|
|
357
|
+
readonly type: string;
|
|
358
|
+
readonly payload: unknown;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Effect-native Workflow behavior modifier. */
|
|
362
|
+
export interface WorkflowInstanceModifier {
|
|
363
|
+
readonly disableSleeps: (steps?: ReadonlyArray<WorkflowStepTarget>) => Effect.Effect<void>;
|
|
364
|
+
readonly disableRetryDelays: (steps?: ReadonlyArray<WorkflowStepTarget>) => Effect.Effect<void>;
|
|
365
|
+
readonly mockStepResult: (step: WorkflowStepTarget, result: unknown) => Effect.Effect<void>;
|
|
366
|
+
readonly mockStepError: (
|
|
367
|
+
step: WorkflowStepTarget,
|
|
368
|
+
error: Error,
|
|
369
|
+
times?: number,
|
|
370
|
+
) => Effect.Effect<void>;
|
|
371
|
+
readonly forceStepTimeout: (step: WorkflowStepTarget, times?: number) => Effect.Effect<void>;
|
|
372
|
+
readonly mockEvent: (event: WorkflowMockEvent) => Effect.Effect<void>;
|
|
373
|
+
readonly forceEventTimeout: (step: WorkflowStepTarget) => Effect.Effect<void>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Effect-native introspector for one Workflow instance. */
|
|
377
|
+
export interface WorkflowInstanceIntrospector {
|
|
378
|
+
readonly raw: NativeWorkflowInstanceIntrospector;
|
|
379
|
+
readonly modify: <A, E, R>(
|
|
380
|
+
use: (modifier: WorkflowInstanceModifier) => Effect.Effect<A, E, R>,
|
|
381
|
+
) => Effect.Effect<A, E, R>;
|
|
382
|
+
readonly waitForStepResult: (step: WorkflowStepTarget) => Effect.Effect<unknown>;
|
|
383
|
+
readonly waitForStatus: (status: WorkflowInstanceStatusName) => Effect.Effect<void>;
|
|
384
|
+
readonly getOutput: Effect.Effect<unknown>;
|
|
385
|
+
readonly getError: Effect.Effect<{ readonly name: string; readonly message: string }>;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Effect-native introspector for subsequently created Workflow instances. */
|
|
389
|
+
export interface WorkflowIntrospector {
|
|
390
|
+
readonly raw: NativeWorkflowIntrospector;
|
|
391
|
+
readonly modifyAll: <A, E, R>(
|
|
392
|
+
use: (modifier: WorkflowInstanceModifier) => Effect.Effect<A, E, R>,
|
|
393
|
+
) => Effect.Effect<A, E, R>;
|
|
394
|
+
readonly get: Effect.Effect<ReadonlyArray<WorkflowInstanceIntrospector>>;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const wrapWorkflowModifier = (
|
|
398
|
+
modifier: NativeWorkflowInstanceModifier,
|
|
399
|
+
): WorkflowInstanceModifier => ({
|
|
400
|
+
disableSleeps: (steps) =>
|
|
401
|
+
Effect.promise(() => modifier.disableSleeps(steps === undefined ? undefined : [...steps])),
|
|
402
|
+
disableRetryDelays: (steps) =>
|
|
403
|
+
Effect.promise(() => modifier.disableRetryDelays(steps === undefined ? undefined : [...steps])),
|
|
404
|
+
mockStepResult: (step, result) => Effect.promise(() => modifier.mockStepResult(step, result)),
|
|
405
|
+
mockStepError: (step, error, times) =>
|
|
406
|
+
Effect.promise(() => modifier.mockStepError(step, error, times)),
|
|
407
|
+
forceStepTimeout: (step, times) =>
|
|
408
|
+
Effect.promise(async () => {
|
|
409
|
+
await modifier.forceStepTimeout(step, times);
|
|
410
|
+
}),
|
|
411
|
+
mockEvent: (event) => Effect.promise(() => modifier.mockEvent(event)),
|
|
412
|
+
forceEventTimeout: (step) => Effect.promise(() => modifier.forceEventTimeout(step)),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const runWorkflowModifier = Effect.fnUntraced(function* <A, E, R>(
|
|
416
|
+
register: (use: (modifier: NativeWorkflowInstanceModifier) => Promise<void>) => Promise<unknown>,
|
|
417
|
+
use: (modifier: WorkflowInstanceModifier) => Effect.Effect<A, E, R>,
|
|
418
|
+
): Effect.fn.Return<A, E, R> {
|
|
419
|
+
const context = yield* Effect.context<R>();
|
|
420
|
+
const exit = yield* Effect.promise(async () => {
|
|
421
|
+
let callbackExit: Exit.Exit<A, E> | undefined;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await register(async (modifier) => {
|
|
425
|
+
callbackExit = await Effect.runPromiseExitWith(context)(
|
|
426
|
+
use(wrapWorkflowModifier(modifier)),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (Exit.isFailure(callbackExit)) {
|
|
430
|
+
throw callbackExit;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
} catch (cause) {
|
|
434
|
+
if (callbackExit !== undefined && Exit.isFailure(callbackExit)) {
|
|
435
|
+
return callbackExit;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
throw cause;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (callbackExit === undefined) {
|
|
442
|
+
throw new Error("Pool Workers did not invoke the Workflow modifier callback");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return callbackExit;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return yield* exit;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const wrapWorkflowInstanceIntrospector = (
|
|
452
|
+
introspector: NativeWorkflowInstanceIntrospector,
|
|
453
|
+
): WorkflowInstanceIntrospector => ({
|
|
454
|
+
raw: introspector,
|
|
455
|
+
modify: (use) => runWorkflowModifier((callback) => introspector.modify(callback), use),
|
|
456
|
+
waitForStepResult: (step) => Effect.promise(() => introspector.waitForStepResult(step)),
|
|
457
|
+
waitForStatus: (status) => Effect.promise(() => introspector.waitForStatus(status)),
|
|
458
|
+
getOutput: Effect.promise(() => introspector.getOutput()),
|
|
459
|
+
getError: Effect.promise(() => introspector.getError()),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const wrapWorkflowIntrospector = (
|
|
463
|
+
introspector: NativeWorkflowIntrospector,
|
|
464
|
+
): WorkflowIntrospector => ({
|
|
465
|
+
raw: introspector,
|
|
466
|
+
modifyAll: (use) => runWorkflowModifier((callback) => introspector.modifyAll(callback), use),
|
|
467
|
+
get: Effect.promise(async () => {
|
|
468
|
+
const instances = await introspector.get();
|
|
469
|
+
|
|
470
|
+
return instances.map(wrapWorkflowInstanceIntrospector);
|
|
471
|
+
}),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Acquires an Effect-native Workflow introspector that is disposed when the
|
|
476
|
+
* surrounding Effect scope closes.
|
|
477
|
+
*/
|
|
478
|
+
export const introspectWorkflow = Effect.fn("effect-cf/vitest/introspectWorkflow")(function* (
|
|
479
|
+
workflow: WorkflowBinding,
|
|
480
|
+
) {
|
|
481
|
+
const introspector = yield* Effect.acquireDisposable(
|
|
482
|
+
Effect.promise(() => introspectWorkflowPromise(workflow)),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
return wrapWorkflowIntrospector(introspector);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Acquires an Effect-native Workflow instance introspector that is disposed
|
|
490
|
+
* when the surrounding Effect scope closes.
|
|
491
|
+
*/
|
|
492
|
+
export const introspectWorkflowInstance = Effect.fn("effect-cf/vitest/introspectWorkflowInstance")(
|
|
493
|
+
function* (workflow: WorkflowBinding, instanceId: string) {
|
|
494
|
+
const introspector = yield* Effect.acquireDisposable(
|
|
495
|
+
Effect.promise(() => introspectWorkflowInstancePromise(workflow, instanceId)),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return wrapWorkflowInstanceIntrospector(introspector);
|
|
499
|
+
},
|
|
500
|
+
);
|