effect-cf 0.34.0 → 0.35.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 +10 -0
- package/dist/ComputerWorkspace.d.mts +11 -3
- package/dist/ComputerWorkspace.mjs +31 -9
- package/dist/{DurableObjectState-De4ZojhY.mjs → DurableObjectState-DcqElJ4e.mjs} +81 -50
- package/dist/{DurableObjectState-CrEunfQ-.d.mts → DurableObjectState-DpYdNZgT.d.mts} +35 -29
- package/dist/Vitest.d.mts +2 -2
- package/dist/Vitest.mjs +7 -10
- package/dist/{WorkflowBinding-BumVQTuW.d.mts → WorkflowBinding-Cn3AuUyP.d.mts} +1 -1
- package/dist/index.d.mts +12 -4
- package/dist/index.mjs +102 -77
- package/package.json +1 -1
- package/src/AnalyticsEngine.ts +8 -1
- package/src/CloudflareOtlp.ts +6 -3
- package/src/ComputerWorkspace.ts +53 -26
- package/src/DurableObject.ts +8 -9
- package/src/DurableObjectNamespace.ts +33 -5
- package/src/DurableObjectRpcWebSocket.ts +2 -27
- package/src/DurableObjectState.ts +18 -20
- package/src/DurableObjectStorage.ts +53 -35
- package/src/DurableObjectWebSocket.ts +23 -41
- package/src/RpcDefinition.ts +5 -15
- package/src/ServiceBinding.ts +37 -5
- package/src/Vitest.ts +18 -15
- package/src/Worker.ts +107 -54
- package/src/WorkersAi.ts +21 -14
- package/src/Workflow.ts +45 -49
- package/src/internal/NativeCallback.ts +55 -0
- package/src/internal/WorkerContext.ts +5 -3
package/src/Worker.ts
CHANGED
|
@@ -5,14 +5,20 @@ import {
|
|
|
5
5
|
Effect,
|
|
6
6
|
Layer,
|
|
7
7
|
type ManagedRuntime,
|
|
8
|
+
References,
|
|
8
9
|
type Scope,
|
|
9
10
|
Tracer,
|
|
10
11
|
} from "effect";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
HttpEffect,
|
|
14
|
+
HttpMiddleware,
|
|
15
|
+
HttpServerRequest,
|
|
16
|
+
HttpServerResponse,
|
|
17
|
+
} from "effect/unstable/http";
|
|
12
18
|
|
|
13
19
|
import { WorkerEnvironment, type WorkerEnv } from "./Environment";
|
|
14
20
|
import { fromMessage, fromMessageBatch, type QueueHandler } from "./Queue";
|
|
15
|
-
import
|
|
21
|
+
import * as RpcDefinition from "./RpcDefinition";
|
|
16
22
|
import type { ReceiverOptions, RpcInvocationInfo } from "./RpcTracing";
|
|
17
23
|
import * as Entrypoint from "./internal/Entrypoint";
|
|
18
24
|
import * as Runtime from "./internal/Runtime";
|
|
@@ -75,20 +81,13 @@ export type ReservedMethodName =
|
|
|
75
81
|
| "webSocketError";
|
|
76
82
|
|
|
77
83
|
const reservedMethodNames = new Set<string>([
|
|
78
|
-
|
|
79
|
-
"dup",
|
|
80
|
-
"fetch",
|
|
81
|
-
"connect",
|
|
84
|
+
...RpcDefinition.reservedMethodNames,
|
|
82
85
|
"queue",
|
|
83
86
|
"scheduled",
|
|
84
87
|
"tail",
|
|
85
88
|
"tailStream",
|
|
86
89
|
"test",
|
|
87
90
|
"trace",
|
|
88
|
-
"alarm",
|
|
89
|
-
"webSocketMessage",
|
|
90
|
-
"webSocketClose",
|
|
91
|
-
"webSocketError",
|
|
92
91
|
]);
|
|
93
92
|
|
|
94
93
|
type WorkerBaseContext<ROut> = ExecutionContext | WorkerContext | WorkerEnvironment | ROut;
|
|
@@ -164,10 +163,12 @@ export interface WorkerOptions<
|
|
|
164
163
|
/**
|
|
165
164
|
* Layer provided around each Cloudflare event handled by this Worker.
|
|
166
165
|
*
|
|
167
|
-
* The layer is built inside the event's Effect scope
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
166
|
+
* The layer is built inside the event's Effect scope. For fetch handlers that
|
|
167
|
+
* return an Effect `HttpServerResponse` stream, its scope remains open until
|
|
168
|
+
* the response body finishes or is canceled. Native `Response` values pass
|
|
169
|
+
* through unchanged, so their body streams do not carry this scoped-lifetime
|
|
170
|
+
* guarantee. Fetch, queue, and RPC handlers schedule a best-effort telemetry
|
|
171
|
+
* flush capped at two seconds.
|
|
171
172
|
*/
|
|
172
173
|
readonly eventLayer?: Layer.Layer<REvent, EventLayerError, WorkerBaseContext<RRuntime>>;
|
|
173
174
|
readonly fetch?: Effect.Effect<
|
|
@@ -238,9 +239,14 @@ export interface FetchHandler<Env extends WorkerEnv = WorkerEnv> {
|
|
|
238
239
|
) => Promise<Response>;
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
const renderFetchSuccess = <E, R>(
|
|
242
|
+
const renderFetchSuccess = <E, R, REvent, EventLayerError, EventLayerRequirements>(
|
|
242
243
|
effect: Effect.Effect<WorkerFetchSuccess, E, R>,
|
|
243
|
-
|
|
244
|
+
eventLayer: Layer.Layer<REvent, EventLayerError, EventLayerRequirements> | undefined,
|
|
245
|
+
): Effect.Effect<
|
|
246
|
+
Response,
|
|
247
|
+
E | EventLayerError,
|
|
248
|
+
Exclude<R | EventLayerRequirements, Scope.Scope> | HttpServerRequest.HttpServerRequest
|
|
249
|
+
> =>
|
|
244
250
|
Effect.suspend(() => {
|
|
245
251
|
// Native `Response` values bypass all HTTP response processing: WebSocket
|
|
246
252
|
// upgrade responses and app-level `HttpEffect.toHandled` results pass
|
|
@@ -249,26 +255,64 @@ const renderFetchSuccess = <E, R>(
|
|
|
249
255
|
let nativeResponse: Response | undefined;
|
|
250
256
|
let webResponse: Response | undefined;
|
|
251
257
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
nativeResponse = response;
|
|
258
|
+
const httpApp = Effect.flatMap(effect, (response) => {
|
|
259
|
+
if (response instanceof Response) {
|
|
260
|
+
nativeResponse = response;
|
|
256
261
|
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
return Effect.succeed(HttpServerResponse.empty());
|
|
263
|
+
}
|
|
259
264
|
|
|
260
|
-
|
|
265
|
+
return Effect.succeed(response);
|
|
266
|
+
});
|
|
267
|
+
const handleResponse = (
|
|
268
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
269
|
+
response: HttpServerResponse.HttpServerResponse,
|
|
270
|
+
) =>
|
|
271
|
+
nativeResponse !== undefined
|
|
272
|
+
? Effect.void
|
|
273
|
+
: Effect.map(Effect.context<never>(), (context) => {
|
|
274
|
+
webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
|
|
275
|
+
withoutBody: request.method === "HEAD",
|
|
276
|
+
context,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
const telemetryMiddleware = HttpMiddleware.make((self) =>
|
|
280
|
+
Effect.gen(function* () {
|
|
281
|
+
yield* Effect.addFinalizer(() => scheduleTelemetryFlush);
|
|
282
|
+
|
|
283
|
+
return yield* self;
|
|
261
284
|
}),
|
|
262
|
-
(request, response) =>
|
|
263
|
-
nativeResponse !== undefined
|
|
264
|
-
? Effect.void
|
|
265
|
-
: Effect.map(Effect.context<never>(), (context) => {
|
|
266
|
-
webResponse = HttpServerResponse.toWeb(HttpEffect.scopeTransferToStream(response), {
|
|
267
|
-
withoutBody: request.method === "HEAD",
|
|
268
|
-
context,
|
|
269
|
-
});
|
|
270
|
-
}),
|
|
271
285
|
);
|
|
286
|
+
const handled =
|
|
287
|
+
eventLayer === undefined
|
|
288
|
+
? HttpEffect.toHandled(httpApp, handleResponse, telemetryMiddleware)
|
|
289
|
+
: Effect.flatMap(References.TracerEnabled, (tracerEnabled) =>
|
|
290
|
+
HttpEffect.toHandled(
|
|
291
|
+
httpApp,
|
|
292
|
+
handleResponse,
|
|
293
|
+
HttpMiddleware.make((self) =>
|
|
294
|
+
Effect.provideService(
|
|
295
|
+
Effect.gen(function* () {
|
|
296
|
+
const context = yield* Layer.build(Layer.fresh(eventLayer));
|
|
297
|
+
|
|
298
|
+
yield* Effect.addFinalizer(() =>
|
|
299
|
+
Effect.provideContext(scheduleTelemetryFlush, context),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return yield* HttpMiddleware.tracer(self).pipe(
|
|
303
|
+
// The tracer schedules span completion. Let it publish on
|
|
304
|
+
// every exit with event references still installed, before
|
|
305
|
+
// the exporter scope finalizes.
|
|
306
|
+
Effect.onExit(() => Effect.yieldNow),
|
|
307
|
+
Effect.provideContext(context),
|
|
308
|
+
);
|
|
309
|
+
}),
|
|
310
|
+
References.TracerEnabled,
|
|
311
|
+
tracerEnabled,
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
).pipe(Effect.withTracerEnabled(false)),
|
|
315
|
+
);
|
|
272
316
|
|
|
273
317
|
// `toHandled` re-fails with the original cause after rendering the error
|
|
274
318
|
// response, so both branches return the response captured above.
|
|
@@ -282,13 +326,13 @@ const isWorkerOptions = <ROut, REvent, EventLayerError, Rpc extends WorkerRpc<RO
|
|
|
282
326
|
): options is WorkerOptions<ROut, REvent, EventLayerError, Rpc> => !Effect.isEffect(options);
|
|
283
327
|
|
|
284
328
|
/**
|
|
285
|
-
* Drains buffered OTLP telemetry
|
|
286
|
-
* response
|
|
329
|
+
* Drains buffered OTLP telemetry when the fetch Effect scope closes. Effect
|
|
330
|
+
* response streams transfer that scope to the body and schedule the flush when
|
|
331
|
+
* the body finishes or is canceled.
|
|
287
332
|
*
|
|
288
|
-
* The flush is handed to `ctx.waitUntil`
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* best-effort and silently settles within two seconds.
|
|
333
|
+
* The flush is handed to `ctx.waitUntil` instead of being awaited by the
|
|
334
|
+
* handler. It is a no-op when the context does not contain an
|
|
335
|
+
* `OtlpExporter.Flusher`, and silently settles within two seconds.
|
|
292
336
|
*/
|
|
293
337
|
const scheduleTelemetryFlush: Effect.Effect<void, never, WorkerContext> =
|
|
294
338
|
Telemetry.scheduleTelemetryFlush((flush) =>
|
|
@@ -296,8 +340,10 @@ const scheduleTelemetryFlush: Effect.Effect<void, never, WorkerContext> =
|
|
|
296
340
|
);
|
|
297
341
|
|
|
298
342
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
343
|
+
* Cloudflare creates a new `WorkerEntrypoint` instance for each native
|
|
344
|
+
* invocation, so this base layer is not shared across invocations. Its managed
|
|
345
|
+
* runtime has no deterministic disposal point in this adapter; use
|
|
346
|
+
* `eventLayer` for resources whose finalizers must run with an event.
|
|
301
347
|
*/
|
|
302
348
|
export function make<ROut, LayerError>(
|
|
303
349
|
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
@@ -361,12 +407,14 @@ export function make<
|
|
|
361
407
|
effect: Effect.Effect<A, E, RuntimeContext<ROut> | REvent | Scope.Scope>,
|
|
362
408
|
runOptions: RunOptions = {},
|
|
363
409
|
): Promise<A> {
|
|
364
|
-
|
|
410
|
+
// Fetch builds its event layer in the HTTP scope transferred to the body.
|
|
411
|
+
const eventLayer = runOptions.event === "fetch" ? undefined : options.eventLayer;
|
|
365
412
|
const parent = runOptions.rpc?.parent;
|
|
366
413
|
const parentSpan = parent === undefined ? undefined : Tracer.externalSpan(parent);
|
|
367
414
|
|
|
368
415
|
if (eventLayer === undefined) {
|
|
369
|
-
// SAFETY:
|
|
416
|
+
// SAFETY: fetch provides REvent inside its HTTP middleware; otherwise the
|
|
417
|
+
// public make overloads permit an absent event layer only when REvent is never.
|
|
370
418
|
return Runtime.runEventPromise(
|
|
371
419
|
this.runtime,
|
|
372
420
|
// @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
|
|
@@ -408,12 +456,11 @@ export function make<
|
|
|
408
456
|
Layer.succeed(WorkerContext, fromExecutionContext(ctx)),
|
|
409
457
|
);
|
|
410
458
|
|
|
411
|
-
//
|
|
459
|
+
// The request services wrap eventLayer construction so cached object handlers
|
|
460
|
+
// never build an event layer against a previous request's ExecutionContext.
|
|
461
|
+
// HttpEffect owns the inner scope and transfers it to streaming response bodies.
|
|
412
462
|
return this[RunSymbol](
|
|
413
|
-
renderFetchSuccess(fetchHandler).pipe(
|
|
414
|
-
Effect.tap(() => scheduleTelemetryFlush),
|
|
415
|
-
Effect.provide(requestServices),
|
|
416
|
-
) as Effect.Effect<Response, unknown, RuntimeContext<ROut> | REvent | Scope.Scope>,
|
|
463
|
+
renderFetchSuccess(fetchHandler, options.eventLayer).pipe(Effect.provide(requestServices)),
|
|
417
464
|
{ event: "fetch" },
|
|
418
465
|
);
|
|
419
466
|
}
|
|
@@ -427,7 +474,12 @@ export function make<
|
|
|
427
474
|
|
|
428
475
|
const messages = batch.messages.map((message) => fromMessage(message, message.body));
|
|
429
476
|
|
|
430
|
-
return this[RunSymbol](
|
|
477
|
+
return this[RunSymbol](
|
|
478
|
+
queueHandler(fromMessageBatch(batch, messages)).pipe(
|
|
479
|
+
Effect.onExit(() => scheduleTelemetryFlush),
|
|
480
|
+
),
|
|
481
|
+
{ event: "queue" },
|
|
482
|
+
);
|
|
431
483
|
}
|
|
432
484
|
}
|
|
433
485
|
|
|
@@ -452,12 +504,13 @@ interface FetchCapableInstance {
|
|
|
452
504
|
* Creates an `ExportedHandler`-compatible `{ fetch }` object backed by
|
|
453
505
|
* {@link make}.
|
|
454
506
|
*
|
|
455
|
-
* The Effect runtime is cached per `env` identity, so the
|
|
456
|
-
* once
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
507
|
+
* The Effect runtime is cached per `env` identity, so the base layer builds
|
|
508
|
+
* once for that cached handler. Request handlers and `eventLayer` receive fresh
|
|
509
|
+
* `ExecutionContext` and `WorkerContext` services. A service constructed by the
|
|
510
|
+
* cached base layer from either context instead captures the first request's
|
|
511
|
+
* value, so construct invocation-bound services in `eventLayer`. Cloudflare
|
|
512
|
+
* provides no isolate shutdown hook, so cached base-layer finalizers are not
|
|
513
|
+
* guaranteed to run.
|
|
461
514
|
*/
|
|
462
515
|
export function makeFetchHandler<
|
|
463
516
|
ROut,
|
package/src/WorkersAi.ts
CHANGED
|
@@ -186,7 +186,7 @@ export const embeddingResponse = (value: {
|
|
|
186
186
|
[embeddingDimensionsKey]: value[embeddingDimensionsKey] ?? [],
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
const decodeEmbeddingResponse = S.
|
|
189
|
+
const decodeEmbeddingResponse = S.decodeUnknownEffect(
|
|
190
190
|
S.Struct({
|
|
191
191
|
data: S.optional(S.Array(S.Array(S.Number))),
|
|
192
192
|
[embeddingDimensionsKey]: S.optional(S.Array(S.Number)),
|
|
@@ -212,21 +212,28 @@ export const makeClient =
|
|
|
212
212
|
() => ai.aiGatewayLogId,
|
|
213
213
|
),
|
|
214
214
|
run,
|
|
215
|
-
runEmbedding: Effect.fn("WorkersAi.runEmbedding")(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
runEmbedding: Effect.fn("WorkersAi.runEmbedding")(function* (
|
|
216
|
+
model: string,
|
|
217
|
+
input: WorkersAiEmbeddingInput,
|
|
218
|
+
options?: WorkersAiOptions,
|
|
219
|
+
) {
|
|
220
|
+
// SAFETY: every WorkersAiEmbeddingInput variant is a string-keyed Workers AI payload.
|
|
221
|
+
const modelInput = input as Parameters<WorkersAiBinding["run"]>[1];
|
|
219
222
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
const response = yield* run(model, modelInput, options);
|
|
224
|
+
const decoded = yield* decodeEmbeddingResponse(response).pipe(
|
|
225
|
+
Effect.mapError(
|
|
226
|
+
(cause) =>
|
|
227
|
+
new WorkersAiOperationError({
|
|
228
|
+
binding: definition.binding,
|
|
229
|
+
operation: "runEmbedding",
|
|
230
|
+
cause,
|
|
225
231
|
}),
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return embeddingResponse(decoded);
|
|
236
|
+
}),
|
|
230
237
|
models: Effect.fn("WorkersAi.models")((params?: WorkersAiModelsSearchParams) =>
|
|
231
238
|
tryWorkersAiPromise(definition.binding, "models", () => ai.models(params)),
|
|
232
239
|
),
|
package/src/Workflow.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Context,
|
|
15
15
|
Data,
|
|
16
16
|
Effect,
|
|
17
|
+
FiberSet,
|
|
17
18
|
Layer,
|
|
18
19
|
type ManagedRuntime,
|
|
19
20
|
Option,
|
|
@@ -40,8 +41,6 @@ export class WorkflowEvent extends Context.Service<WorkflowEvent, WorkflowEventS
|
|
|
40
41
|
"effect-cf/WorkflowEvent",
|
|
41
42
|
) {}
|
|
42
43
|
|
|
43
|
-
type RunWorkflowStepEffect = <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>;
|
|
44
|
-
|
|
45
44
|
export class WorkflowStepError extends Data.TaggedError("WorkflowStepError")<{
|
|
46
45
|
readonly step?: string;
|
|
47
46
|
readonly operation: string;
|
|
@@ -135,52 +134,52 @@ const exposeCloudflareNonRetryableError = <A, E, R>(
|
|
|
135
134
|
: Effect.failCause(cause);
|
|
136
135
|
});
|
|
137
136
|
|
|
138
|
-
const fromWorkflowStep = (
|
|
139
|
-
step: CloudflareWorkflowStep,
|
|
140
|
-
runPromise: RunWorkflowStepEffect,
|
|
141
|
-
): WorkflowStepService => ({
|
|
137
|
+
const fromWorkflowStep = (step: CloudflareWorkflowStep): WorkflowStepService => ({
|
|
142
138
|
raw: step,
|
|
143
139
|
do: <A, E, R>(name: string, effect: Effect.Effect<A, E, R>, config?: WorkflowStepConfig) =>
|
|
144
|
-
Effect.
|
|
145
|
-
Effect.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
do
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const context = yield* Effect.context<Exclude<R, WorkflowStepContext>>();
|
|
142
|
+
|
|
143
|
+
return yield* Effect.scoped(
|
|
144
|
+
Effect.gen(function* () {
|
|
145
|
+
// Cloudflare owns retry timing and exposes no step cancellation API.
|
|
146
|
+
// Join the current callback on interruption and prevent later retry
|
|
147
|
+
// callbacks from starting, without waiting for native retry delays.
|
|
148
|
+
const runPromise = yield* FiberSet.makeRuntimePromise<never, A, E>();
|
|
149
|
+
|
|
150
|
+
return yield* Effect.tryPromise({
|
|
151
|
+
try: () => {
|
|
152
|
+
const run = (stepContext: CloudflareWorkflowStepContext) => {
|
|
153
|
+
const stepEffect: Effect.Effect<
|
|
154
|
+
A,
|
|
155
|
+
E,
|
|
156
|
+
Exclude<R, WorkflowStepContext>
|
|
157
|
+
> = Effect.scoped(Effect.provideService(effect, WorkflowStepContext, stepContext));
|
|
158
|
+
|
|
159
|
+
return runPromise(
|
|
160
|
+
exposeCloudflareNonRetryableError(Effect.provideContext(stepEffect, context)),
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
// SAFETY: these overloads reproduce the Cloudflare WorkflowStep.do runtime signatures used here.
|
|
164
|
+
const rawStep = step as {
|
|
165
|
+
do(
|
|
166
|
+
name: string,
|
|
167
|
+
callback: (context: CloudflareWorkflowStepContext) => Promise<A>,
|
|
168
|
+
): Promise<A>;
|
|
169
|
+
do(
|
|
170
|
+
name: string,
|
|
171
|
+
config: WorkflowStepConfig,
|
|
172
|
+
callback: (context: CloudflareWorkflowStepContext) => Promise<A>,
|
|
173
|
+
): Promise<A>;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return config === undefined ? rawStep.do(name, run) : rawStep.do(name, config, run);
|
|
177
|
+
},
|
|
178
|
+
catch: (cause) => new WorkflowStepError({ step: name, operation: "do", cause }),
|
|
179
|
+
});
|
|
181
180
|
}),
|
|
182
|
-
)
|
|
183
|
-
),
|
|
181
|
+
);
|
|
182
|
+
}),
|
|
184
183
|
sleep: (name, duration) =>
|
|
185
184
|
Effect.tryPromise({
|
|
186
185
|
try: () => step.sleep(name, duration),
|
|
@@ -263,10 +262,7 @@ export const make = <ROut, LayerError, Payload = unknown, Result = unknown>(
|
|
|
263
262
|
): Promise<Result> {
|
|
264
263
|
const workflowServices = Layer.mergeAll(
|
|
265
264
|
Layer.succeed(WorkflowEvent, fromWorkflowEvent(event)),
|
|
266
|
-
Layer.succeed(
|
|
267
|
-
WorkflowStep,
|
|
268
|
-
fromWorkflowStep(step, (effect) => this.runtime.runPromise(effect)),
|
|
269
|
-
),
|
|
265
|
+
Layer.succeed(WorkflowStep, fromWorkflowStep(step)),
|
|
270
266
|
);
|
|
271
267
|
|
|
272
268
|
return Runtime.runEventPromise(this.runtime, options.run(event.payload), workflowServices);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Effect, Exit, FiberSet } from "effect";
|
|
2
|
+
|
|
3
|
+
type RunCallback<A, E, R> = (effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs an Effect callback owned by a native Promise operation.
|
|
7
|
+
*
|
|
8
|
+
* The callback keeps the caller's context, while a private scope owns its
|
|
9
|
+
* fiber. Closing that scope interrupts and joins the callback before awaiting
|
|
10
|
+
* the native operation's settlement.
|
|
11
|
+
*/
|
|
12
|
+
export const runNativeCallback = Effect.fnUntraced(function* <A, E, R, B>(
|
|
13
|
+
operation: (run: RunCallback<A, E, R>) => Promise<B>,
|
|
14
|
+
): Effect.fn.Return<B, unknown, R> {
|
|
15
|
+
const callerContext = yield* Effect.context<R>();
|
|
16
|
+
|
|
17
|
+
return yield* Effect.scoped(
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
let nativePromise: Promise<B> | undefined;
|
|
20
|
+
|
|
21
|
+
// Registered before the FiberSet so scope closure first interrupts and
|
|
22
|
+
// joins the callback, then waits for the native rollback/gate to settle.
|
|
23
|
+
yield* Effect.addFinalizer(() => {
|
|
24
|
+
const promise = nativePromise;
|
|
25
|
+
|
|
26
|
+
return promise === undefined
|
|
27
|
+
? Effect.void
|
|
28
|
+
: Effect.promise(() =>
|
|
29
|
+
promise.then(
|
|
30
|
+
() => undefined,
|
|
31
|
+
() => undefined,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
const runPromise = yield* FiberSet.makeRuntimePromise<never, Exit.Exit<A, E>, never>();
|
|
36
|
+
|
|
37
|
+
return yield* Effect.callback<B, unknown>((resume) => {
|
|
38
|
+
try {
|
|
39
|
+
nativePromise = operation((effect) =>
|
|
40
|
+
runPromise(Effect.provideContext(Effect.exit(effect), callerContext)),
|
|
41
|
+
);
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
resume(Effect.fail(cause));
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
nativePromise.then(
|
|
49
|
+
(value) => resume(Effect.succeed(value)),
|
|
50
|
+
(cause) => resume(Effect.fail(cause)),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
@@ -17,7 +17,9 @@ const failureHandler = <E, R>(
|
|
|
17
17
|
cause: Cause.Cause<E>,
|
|
18
18
|
options: WorkerContextWaitUntilOptions<E, R> | undefined,
|
|
19
19
|
) =>
|
|
20
|
-
|
|
20
|
+
Effect.suspend(
|
|
21
|
+
() => options?.onFailure?.(cause) ?? Effect.logError(`${label} failed`, Cause.pretty(cause)),
|
|
22
|
+
).pipe(
|
|
21
23
|
Effect.catchCause((handlerCause) =>
|
|
22
24
|
Effect.logError(
|
|
23
25
|
`${label} failure handler failed`,
|
|
@@ -42,7 +44,7 @@ export const makeWaitUntilScheduler = (
|
|
|
42
44
|
Effect.sync(() => {
|
|
43
45
|
const runHandler = (cause: Cause.Cause<E>) =>
|
|
44
46
|
runPromiseExit(
|
|
45
|
-
Effect.
|
|
47
|
+
Effect.provideContext(Effect.scoped(failureHandler(label, cause, options)), context),
|
|
46
48
|
).then((exit) => {
|
|
47
49
|
if (Exit.isFailure(exit)) {
|
|
48
50
|
console.error(`${label} failure handler failed`, Cause.pretty(exit.cause));
|
|
@@ -50,7 +52,7 @@ export const makeWaitUntilScheduler = (
|
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
register(
|
|
53
|
-
runPromiseExit(Effect.
|
|
55
|
+
runPromiseExit(Effect.provideContext(Effect.scoped(effect), context)).then(
|
|
54
56
|
async (exit) => {
|
|
55
57
|
if (Exit.isSuccess(exit)) {
|
|
56
58
|
return;
|