effect-orpc 0.4.0 → 0.5.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.
@@ -3,13 +3,14 @@ import type {
3
3
  Context,
4
4
  Middleware,
5
5
  MiddlewareNextFnOptions,
6
+ MiddlewareOptions,
6
7
  MiddlewareOutputFn,
7
8
  MiddlewareResult,
8
9
  ProcedureHandler,
9
10
  ProcedureHandlerOptions,
10
11
  } from "@orpc/server";
11
12
  import type { Promisable } from "@orpc/shared";
12
- import { Cause, Effect, Exit, Option } from "effect";
13
+ import { Cause, Effect, Exit, FiberRefs, Option } from "effect";
13
14
 
14
15
  import { runWithFiberRefs } from "./fiber-context-bridge";
15
16
  import type { EffectRuntimeRunner } from "./runtime-source";
@@ -32,6 +33,35 @@ import type {
32
33
 
33
34
  type EffectTag = import("effect").Context.Tag<any, any>;
34
35
 
36
+ const HybridContinuationSymbol = Symbol("effect-orpc/HybridContinuation");
37
+
38
+ type MiddlewareNextTracker<T> = ReturnType<
39
+ typeof createMiddlewareNextTracker<T>
40
+ >;
41
+
42
+ type EffectMiddlewareRuntimeOptions<
43
+ TCurrentContext extends Context,
44
+ TOutput,
45
+ TEffectErrorMap extends EffectErrorMap,
46
+ TRequirementsProvided,
47
+ TMeta extends Meta,
48
+ > = EffectMiddlewareOptions<
49
+ TCurrentContext,
50
+ TOutput,
51
+ TEffectErrorMap,
52
+ TRequirementsProvided,
53
+ TMeta
54
+ > & {
55
+ readonly nextTracker: MiddlewareNextTracker<
56
+ EffectMiddlewareResult<Context, TOutput>
57
+ >;
58
+ readonly output: EffectMiddlewareOutput<
59
+ TOutput,
60
+ TEffectErrorMap,
61
+ TRequirementsProvided
62
+ >;
63
+ };
64
+
35
65
  function toORPCErrorFromCause(
36
66
  cause: Cause.Cause<unknown>,
37
67
  ): ORPCError<string, unknown> {
@@ -126,8 +156,7 @@ export function createEffectProcedureHandler<
126
156
  const spanName = spanConfig?.name ?? opts.path.join(".");
127
157
  const captureStackTrace =
128
158
  spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
129
- const resolver = Effect.fnUntraced(effectFn as any);
130
- const handlerEffect = resolver(effectOpts);
159
+ const handlerEffect = callEffectCallback(effectFn, effectOpts);
131
160
  const tracedEffect = Effect.withSpan(
132
161
  runEffectPipeline({
133
162
  baseOptions: effectOpts,
@@ -136,10 +165,11 @@ export function createEffectProcedureHandler<
136
165
  Effect.map(
137
166
  context === effectOpts.context
138
167
  ? handlerEffect
139
- : resolver({ ...effectOpts, context }),
168
+ : callEffectCallback(effectFn, { ...effectOpts, context }),
140
169
  (output) => ({ output, context: {} }),
141
170
  ),
142
171
  input: opts.input,
172
+ runner,
143
173
  steps: effectSteps,
144
174
  }),
145
175
  spanName,
@@ -204,6 +234,7 @@ export function createEffectPipelineMiddleware<
204
234
  ),
205
235
  ) as any,
206
236
  input,
237
+ runner,
207
238
  steps,
208
239
  });
209
240
  const exit = await runner.runPromiseExit(effect, {
@@ -216,6 +247,341 @@ export function createEffectPipelineMiddleware<
216
247
  };
217
248
  }
218
249
 
250
+ export function createEffectOrORPCMiddleware<
251
+ TCurrentContext extends Context,
252
+ TOutContext extends Context,
253
+ TInput,
254
+ TOutput,
255
+ TEffectErrorMap extends EffectErrorMap,
256
+ TRequirementsProvided,
257
+ TRuntimeError,
258
+ TMeta extends Meta,
259
+ >(options: {
260
+ runner: EffectRuntimeRunner<TRequirementsProvided, TRuntimeError>;
261
+ effectErrorMap: TEffectErrorMap;
262
+ middleware: EffectMiddleware<
263
+ TCurrentContext,
264
+ TOutContext,
265
+ TInput,
266
+ TOutput,
267
+ TEffectErrorMap,
268
+ TRequirementsProvided,
269
+ TMeta
270
+ >;
271
+ }): Middleware<TCurrentContext, TOutContext, TInput, TOutput, any, TMeta> {
272
+ const { runner, effectErrorMap, middleware } = options;
273
+
274
+ return async (opts, input, output) => {
275
+ const effectOptions = makeEffectMiddlewareOptions<
276
+ TCurrentContext,
277
+ TOutput,
278
+ TEffectErrorMap,
279
+ TRequirementsProvided,
280
+ TMeta
281
+ >({
282
+ effectErrorMap,
283
+ final: opts.next,
284
+ options: opts,
285
+ output,
286
+ });
287
+
288
+ return runEffectOrORPCMiddlewareCallback<
289
+ TCurrentContext,
290
+ TOutContext,
291
+ TInput,
292
+ TOutput,
293
+ TEffectErrorMap,
294
+ TRequirementsProvided,
295
+ TMeta
296
+ >({
297
+ effectOptions,
298
+ input,
299
+ middleware,
300
+ nativeNext: opts.next,
301
+ runner,
302
+ signal: opts.signal,
303
+ });
304
+ };
305
+ }
306
+
307
+ async function runEffectOrORPCMiddlewareCallback<
308
+ TCurrentContext extends Context,
309
+ TOutContext extends Context,
310
+ TInput,
311
+ TOutput,
312
+ TEffectErrorMap extends EffectErrorMap,
313
+ TRequirementsProvided,
314
+ TMeta extends Meta,
315
+ >(options: {
316
+ effectOptions: EffectMiddlewareRuntimeOptions<
317
+ TCurrentContext,
318
+ TOutput,
319
+ TEffectErrorMap,
320
+ TRequirementsProvided,
321
+ TMeta
322
+ >;
323
+ input: TInput;
324
+ middleware: EffectMiddleware<
325
+ TCurrentContext,
326
+ TOutContext,
327
+ TInput,
328
+ TOutput,
329
+ TEffectErrorMap,
330
+ TRequirementsProvided,
331
+ TMeta
332
+ >;
333
+ nativeNext: () => Promisable<MiddlewareResult<TOutContext, TOutput>>;
334
+ runner: EffectRuntimeRunner<TRequirementsProvided, unknown>;
335
+ signal: AbortSignal | undefined;
336
+ }): Promise<MiddlewareResult<TOutContext, TOutput>> {
337
+ const result = options.middleware(
338
+ options.effectOptions,
339
+ options.input,
340
+ options.effectOptions.output,
341
+ );
342
+
343
+ const classified = classifyEffectOrORPCMiddlewareResult<
344
+ TOutContext,
345
+ TOutput,
346
+ TRequirementsProvided
347
+ >(result);
348
+
349
+ switch (classified._tag) {
350
+ case "nativeContinuation":
351
+ return classified.result;
352
+ case "nativeGuardOnly":
353
+ return options.nativeNext();
354
+ case "nativeResult":
355
+ return classified.result;
356
+ case "effect": {
357
+ const exit = await options.runner.runPromiseExit(
358
+ runEffectMiddlewareResult({
359
+ autoNext: () => options.effectOptions.next(),
360
+ effect: classified.effect,
361
+ nextInvoked: options.effectOptions.nextTracker.nextInvoked,
362
+ nextResult: options.effectOptions.nextTracker.nextResult,
363
+ }),
364
+ { signal: options.signal },
365
+ );
366
+
367
+ if (Exit.isFailure(exit)) throw toORPCErrorFromCause(exit.cause);
368
+
369
+ return exit.value as MiddlewareResult<TOutContext, TOutput>;
370
+ }
371
+ }
372
+ }
373
+
374
+ type ClassifiedEffectOrORPCMiddlewareResult<
375
+ TOutContext extends Context,
376
+ TOutput,
377
+ TRequirementsProvided,
378
+ > =
379
+ | {
380
+ readonly _tag: "nativeContinuation";
381
+ readonly result: MiddlewareResult<TOutContext, TOutput>;
382
+ }
383
+ | { readonly _tag: "nativeGuardOnly" }
384
+ | {
385
+ readonly _tag: "nativeResult";
386
+ readonly result: MiddlewareResult<TOutContext, TOutput>;
387
+ }
388
+ | {
389
+ readonly _tag: "effect";
390
+ readonly effect: Effect.Effect<
391
+ EffectMiddlewareResult<TOutContext, TOutput> | void,
392
+ unknown,
393
+ TRequirementsProvided
394
+ >;
395
+ };
396
+
397
+ function classifyEffectOrORPCMiddlewareResult<
398
+ TOutContext extends Context,
399
+ TOutput,
400
+ TRequirementsProvided,
401
+ >(
402
+ result: unknown,
403
+ ): ClassifiedEffectOrORPCMiddlewareResult<
404
+ TOutContext,
405
+ TOutput,
406
+ TRequirementsProvided
407
+ > {
408
+ if (isHybridContinuation(result)) {
409
+ return {
410
+ _tag: "nativeContinuation",
411
+ result: result as MiddlewareResult<TOutContext, TOutput>,
412
+ };
413
+ }
414
+
415
+ if (Effect.isEffect(result) || isGeneratorIterator(result)) {
416
+ return {
417
+ _tag: "effect",
418
+ effect: effectFromCallbackResult(result),
419
+ };
420
+ }
421
+
422
+ if (result === undefined) {
423
+ return { _tag: "nativeGuardOnly" };
424
+ }
425
+
426
+ return {
427
+ _tag: "nativeResult",
428
+ result: result as MiddlewareResult<TOutContext, TOutput>,
429
+ };
430
+ }
431
+
432
+ function runEffectMiddlewareResult<
433
+ TContext extends Context,
434
+ TOutput,
435
+ TRequirementsProvided,
436
+ >(options: {
437
+ effect: Effect.Effect<
438
+ EffectMiddlewareResult<TContext, TOutput> | void,
439
+ unknown,
440
+ TRequirementsProvided
441
+ >;
442
+ nextInvoked: boolean;
443
+ nextResult: EffectMiddlewareResult<TContext, TOutput> | undefined;
444
+ autoNext: () => Effect.Effect<
445
+ EffectMiddlewareResult<TContext, TOutput>,
446
+ unknown,
447
+ TRequirementsProvided
448
+ >;
449
+ }): Effect.Effect<
450
+ EffectMiddlewareResult<TContext, TOutput>,
451
+ unknown,
452
+ TRequirementsProvided
453
+ > {
454
+ return Effect.flatMap(options.effect, (result) =>
455
+ resolveEffectMiddlewareContinuation({
456
+ autoNext: options.autoNext,
457
+ nextInvoked: options.nextInvoked,
458
+ nextResult: options.nextResult,
459
+ result,
460
+ }),
461
+ );
462
+ }
463
+
464
+ function makeEffectMiddlewareOptions<
465
+ TCurrentContext extends Context,
466
+ TOutput,
467
+ TEffectErrorMap extends EffectErrorMap,
468
+ TRequirementsProvided,
469
+ TMeta extends Meta,
470
+ >(options: {
471
+ effectErrorMap: TEffectErrorMap;
472
+ final: (
473
+ ...rest: [MiddlewareNextFnOptions<Context>?]
474
+ ) => Promisable<MiddlewareResult<Context, TOutput>>;
475
+ options: Omit<
476
+ MiddlewareOptions<
477
+ TCurrentContext,
478
+ TOutput,
479
+ EffectErrorConstructorMap<TEffectErrorMap>,
480
+ TMeta
481
+ >,
482
+ "next"
483
+ >;
484
+ output: MiddlewareOutputFn<TOutput>;
485
+ }): EffectMiddlewareRuntimeOptions<
486
+ TCurrentContext,
487
+ TOutput,
488
+ TEffectErrorMap,
489
+ TRequirementsProvided,
490
+ TMeta
491
+ > {
492
+ const nextTracker =
493
+ createMiddlewareNextTracker<EffectMiddlewareResult<Context, TOutput>>();
494
+
495
+ const effectOptions = {
496
+ context: options.options.context,
497
+ path: options.options.path,
498
+ procedure: options.options.procedure,
499
+ signal: options.options.signal,
500
+ lastEventId: options.options.lastEventId,
501
+ errors: createEffectErrorConstructorMap(options.effectErrorMap),
502
+ next: nextTracker.wrapNext((...rest: [MiddlewareNextFnOptions<Context>?]) =>
503
+ makeHybridMiddlewareContinuation({
504
+ final: options.final,
505
+ nextTracker,
506
+ rest,
507
+ }),
508
+ ),
509
+ nextTracker,
510
+ output: makeEffectMiddlewareOutput<
511
+ TOutput,
512
+ TEffectErrorMap,
513
+ TRequirementsProvided
514
+ >(options.output),
515
+ };
516
+
517
+ return effectOptions;
518
+ }
519
+
520
+ function makeHybridMiddlewareContinuation<TOutput>(options: {
521
+ final: (
522
+ ...rest: [MiddlewareNextFnOptions<Context>?]
523
+ ) => Promisable<MiddlewareResult<Context, TOutput>>;
524
+ nextTracker: MiddlewareNextTracker<EffectMiddlewareResult<Context, TOutput>>;
525
+ rest: [MiddlewareNextFnOptions<Context>?];
526
+ }): Effect.Effect<EffectMiddlewareResult<Context, TOutput>> {
527
+ const nextContext = options.rest[0]?.context ?? {};
528
+ const toEffectResult = (
529
+ result: any,
530
+ ): EffectMiddlewareResult<Context, TOutput> => ({
531
+ output: result.output,
532
+ context: nextContext,
533
+ });
534
+ const effect = Effect.map(
535
+ withCurrentFiberContext(() => options.final(...options.rest) as any),
536
+ toEffectResult,
537
+ ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput>>;
538
+
539
+ return makeHybridContinuation(effect, {
540
+ onResult: options.nextTracker.setNextResult,
541
+ run: () => options.final(...options.rest) as any,
542
+ toEffectResult,
543
+ });
544
+ }
545
+
546
+ function makeHybridContinuation<TORPCResult, TEffectResult>(
547
+ effect: Effect.Effect<TEffectResult>,
548
+ options: {
549
+ run: () => Promisable<TORPCResult>;
550
+ toEffectResult: (result: TORPCResult) => TEffectResult;
551
+ onResult?: (result: TEffectResult) => void;
552
+ },
553
+ ): Effect.Effect<TEffectResult> {
554
+ markHybridContinuation(effect);
555
+ attachPromiseLikeContinuation(effect, (onFulfilled, onRejected) =>
556
+ Promise.resolve(options.run() as any)
557
+ .then(options.toEffectResult)
558
+ .then((result) => {
559
+ options.onResult?.(result);
560
+ return result;
561
+ })
562
+ .then(onFulfilled, onRejected),
563
+ );
564
+ return effect;
565
+ }
566
+
567
+ function markHybridContinuation(value: object): void {
568
+ Object.defineProperty(value, HybridContinuationSymbol, {
569
+ configurable: true,
570
+ value: true,
571
+ });
572
+ }
573
+
574
+ function attachPromiseLikeContinuation(
575
+ value: object,
576
+ then: (onFulfilled: any, onRejected: any) => PromiseLike<unknown>,
577
+ ): void {
578
+ // oxlint-disable-next-line unicorn/no-thenable -- the hybrid continuation intentionally satisfies Effect and oRPC promise protocols.
579
+ Object.defineProperty(value, "then", {
580
+ configurable: true,
581
+ value: then,
582
+ });
583
+ }
584
+
219
585
  export function createEffectProviderMiddleware<
220
586
  TCurrentContext extends Context,
221
587
  TInput,
@@ -246,12 +612,14 @@ export function createEffectProviderMiddleware<
246
612
  TEffectErrorMap,
247
613
  TMeta
248
614
  >(opts, input, effectErrorMap);
249
- const effect = Effect.flatMap(provider(effectOpts), (service) =>
250
- Effect.provideService(
251
- withCurrentFiberContext(() => opts.next()),
252
- tag,
253
- service,
254
- ),
615
+ const effect = Effect.flatMap(
616
+ callEffectCallback(provider, effectOpts),
617
+ (service) =>
618
+ Effect.provideService(
619
+ withCurrentFiberContext(() => opts.next()),
620
+ tag,
621
+ service,
622
+ ),
255
623
  );
256
624
  const exit = await runner.runPromiseExit(effect, {
257
625
  signal: opts.signal,
@@ -293,16 +661,18 @@ export function createEffectOptionalProviderMiddleware<
293
661
  TEffectErrorMap,
294
662
  TMeta
295
663
  >(opts, input, effectErrorMap);
296
- const effect = Effect.flatMap(provider(effectOpts), (service) =>
297
- Option.match(service, {
298
- onNone: () => withCurrentFiberContext(() => opts.next()),
299
- onSome: (value) =>
300
- Effect.provideService(
301
- withCurrentFiberContext(() => opts.next()),
302
- tag,
303
- value,
304
- ),
305
- }),
664
+ const effect = Effect.flatMap(
665
+ callEffectCallback(provider, effectOpts),
666
+ (service) =>
667
+ Option.match(service, {
668
+ onNone: () => withCurrentFiberContext(() => opts.next()),
669
+ onSome: (value) =>
670
+ Effect.provideService(
671
+ withCurrentFiberContext(() => opts.next()),
672
+ tag,
673
+ value,
674
+ ),
675
+ }),
306
676
  );
307
677
  const exit = await runner.runPromiseExit(effect, {
308
678
  signal: opts.signal,
@@ -314,6 +684,80 @@ export function createEffectOptionalProviderMiddleware<
314
684
  };
315
685
  }
316
686
 
687
+ function callEffectCallback(
688
+ callback: (...args: Array<any>) => unknown,
689
+ ...args: Array<unknown>
690
+ ): Effect.Effect<any, any, any> {
691
+ if (isGeneratorFunction(callback)) {
692
+ return Effect.fnUntraced(callback as any)(...args);
693
+ }
694
+
695
+ return Effect.suspend(() => {
696
+ try {
697
+ return effectFromCallbackResult(callback(...args));
698
+ } catch (error) {
699
+ return Effect.fail(error);
700
+ }
701
+ });
702
+ }
703
+
704
+ function effectFromCallbackResult(
705
+ result: unknown,
706
+ ): Effect.Effect<any, any, any> {
707
+ if (Effect.isEffect(result)) {
708
+ return result;
709
+ }
710
+
711
+ if (isGeneratorIterator(result)) {
712
+ return Effect.fnUntraced(function* () {
713
+ return yield* result as any;
714
+ })();
715
+ }
716
+
717
+ if (isPromiseLike(result)) {
718
+ return Effect.promise(() => result);
719
+ }
720
+
721
+ return Effect.succeed(result);
722
+ }
723
+
724
+ function isGeneratorIterator(value: unknown): value is Generator<unknown> {
725
+ return (
726
+ typeof value === "object" &&
727
+ value !== null &&
728
+ "next" in value &&
729
+ typeof value.next === "function" &&
730
+ "throw" in value &&
731
+ typeof value.throw === "function"
732
+ );
733
+ }
734
+
735
+ function isHybridContinuation(value: unknown): boolean {
736
+ return (
737
+ (typeof value === "object" || typeof value === "function") &&
738
+ value !== null &&
739
+ HybridContinuationSymbol in value
740
+ );
741
+ }
742
+
743
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
744
+ return (
745
+ (typeof value === "object" || typeof value === "function") &&
746
+ value !== null &&
747
+ "then" in value &&
748
+ typeof value.then === "function"
749
+ );
750
+ }
751
+
752
+ function isGeneratorFunction(
753
+ value: unknown,
754
+ ): value is (...args: Array<any>) => Generator<unknown> {
755
+ return (
756
+ typeof value === "function" &&
757
+ value.constructor?.name === "GeneratorFunction"
758
+ );
759
+ }
760
+
317
761
  // todo(utopy): make this check more comprehensive, maybe add a Symbol to the EffectMiddleware type
318
762
  export function isEffectMiddleware(
319
763
  value: unknown,
@@ -326,9 +770,12 @@ export function isEffectMiddleware(
326
770
  unknown,
327
771
  Meta
328
772
  > {
773
+ return isGeneratorFunction(value);
774
+ }
775
+
776
+ export function isDecoratedMiddleware(value: unknown): boolean {
329
777
  return (
330
- typeof value === "function" &&
331
- value.constructor?.name === "GeneratorFunction"
778
+ typeof value === "function" && "mapInput" in value && "concat" in value
332
779
  );
333
780
  }
334
781
 
@@ -383,6 +830,7 @@ function runEffectPipeline<
383
830
  TRequirementsProvided
384
831
  >;
385
832
  input: TInput;
833
+ runner: EffectRuntimeRunner<TRequirementsProvided, unknown>;
386
834
  steps: readonly EffectPipelineStep[];
387
835
  }): Effect.Effect<
388
836
  EffectMiddlewareResult<Context, TOutput>,
@@ -403,18 +851,22 @@ function runEffectPipeline<
403
851
  const stepOptions = { ...options.baseOptions, context };
404
852
 
405
853
  if (step._tag === "provide") {
406
- return Effect.flatMap(step.provider(stepOptions as any), (service) =>
407
- Effect.provideService(run(index + 1, context), step.tag, service),
854
+ return Effect.flatMap(
855
+ callEffectCallback(step.provider, stepOptions),
856
+ (service) =>
857
+ Effect.provideService(run(index + 1, context), step.tag, service),
408
858
  );
409
859
  }
410
860
 
411
861
  if (step._tag === "provideOptional") {
412
- return Effect.flatMap(step.provider(stepOptions as any), (service) =>
413
- Option.match(service, {
414
- onNone: () => run(index + 1, context),
415
- onSome: (value) =>
416
- Effect.provideService(run(index + 1, context), step.tag, value),
417
- }),
862
+ return Effect.flatMap(
863
+ callEffectCallback(step.provider, stepOptions),
864
+ (service) =>
865
+ Option.match(service, {
866
+ onNone: () => run(index + 1, context),
867
+ onSome: (value) =>
868
+ Effect.provideService(run(index + 1, context), step.tag, value),
869
+ }),
418
870
  );
419
871
  }
420
872
 
@@ -422,59 +874,75 @@ function runEffectPipeline<
422
874
  return Effect.provide(run(index + 1, context), step.layer);
423
875
  }
424
876
 
425
- const nextTracker =
426
- createMiddlewareNextTracker<EffectMiddlewareResult<Context, TOutput>>();
427
- const effectOptions: EffectMiddlewareOptions<
428
- TCurrentContext,
429
- TOutput,
430
- TEffectErrorMap,
431
- TRequirementsProvided,
432
- TMeta
433
- > = {
434
- context,
435
- path: stepOptions.path,
436
- procedure: stepOptions.procedure,
437
- signal: stepOptions.signal,
438
- lastEventId: stepOptions.lastEventId,
439
- errors: createEffectErrorConstructorMap(options.effectErrorMap),
440
- next: nextTracker.wrapNext(
441
- (...rest: [MiddlewareNextFnOptions<Context>?]) => {
442
- const nextContext = rest[0]?.context ?? {};
443
- return Effect.map(
444
- run(index + 1, { ...context, ...nextContext }),
445
- (result) => ({
446
- output: result.output,
447
- context: nextContext,
448
- }),
449
- ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput>>;
450
- },
451
- ),
452
- };
453
- const effectOutput = makeEffectMiddlewareOutput<
454
- TOutput,
455
- TEffectErrorMap,
456
- TRequirementsProvided
457
- >((output) => ({ output, context: {} }));
458
- const middlewareEffect = Effect.fnUntraced(step.middleware)(
459
- effectOptions,
460
- options.input,
461
- effectOutput,
462
- ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput> | void>;
463
-
464
- return Effect.flatMap(middlewareEffect, (result) =>
465
- resolveEffectMiddlewareContinuation({
466
- autoNext: () => effectOptions.next(),
467
- nextInvoked: nextTracker.nextInvoked,
468
- nextResult: nextTracker.nextResult,
469
- result,
470
- }),
471
- );
877
+ return Effect.flatMap(Effect.getFiberRefs, (fiberRefs) => {
878
+ const makeThenable = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
879
+ makeThenableEffect(
880
+ effect,
881
+ options.runner as EffectRuntimeRunner<any, unknown>,
882
+ fiberRefs,
883
+ stepOptions.signal,
884
+ ) as Effect.Effect<A, E, R>;
885
+ const nextTracker =
886
+ createMiddlewareNextTracker<EffectMiddlewareResult<Context, TOutput>>(
887
+ makeThenable,
888
+ );
889
+ const effectOptions: EffectMiddlewareOptions<
890
+ TCurrentContext,
891
+ TOutput,
892
+ TEffectErrorMap,
893
+ TRequirementsProvided,
894
+ TMeta
895
+ > = {
896
+ context,
897
+ path: stepOptions.path,
898
+ procedure: stepOptions.procedure,
899
+ signal: stepOptions.signal,
900
+ lastEventId: stepOptions.lastEventId,
901
+ errors: createEffectErrorConstructorMap(options.effectErrorMap),
902
+ next: nextTracker.wrapNext(
903
+ (...rest: [MiddlewareNextFnOptions<Context>?]) => {
904
+ const nextContext = rest[0]?.context ?? {};
905
+ return Effect.map(
906
+ run(index + 1, { ...context, ...nextContext }),
907
+ (result) => ({
908
+ output: result.output,
909
+ context: nextContext,
910
+ }),
911
+ ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput>>;
912
+ },
913
+ ),
914
+ };
915
+ const effectOutput = makeEffectMiddlewareOutput<
916
+ TOutput,
917
+ TEffectErrorMap,
918
+ TRequirementsProvided
919
+ >((output) => ({ output, context: {} }), makeThenable);
920
+ const middlewareEffect = callEffectCallback(
921
+ step.middleware,
922
+ effectOptions,
923
+ options.input,
924
+ effectOutput,
925
+ ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput> | void>;
926
+
927
+ return Effect.flatMap(middlewareEffect, (result) =>
928
+ resolveEffectMiddlewareContinuation({
929
+ autoNext: () => effectOptions.next(),
930
+ nextInvoked: nextTracker.nextInvoked,
931
+ nextResult: nextTracker.nextResult,
932
+ result,
933
+ }),
934
+ );
935
+ });
472
936
  };
473
937
 
474
938
  return run(0, options.baseOptions.context);
475
939
  }
476
940
 
477
- function createMiddlewareNextTracker<T>() {
941
+ function createMiddlewareNextTracker<T>(
942
+ makeThenable?: <A, E, R>(
943
+ effect: Effect.Effect<A, E, R>,
944
+ ) => Effect.Effect<A, E, R>,
945
+ ) {
478
946
  let nextInvoked = false;
479
947
  let nextResult: T | undefined;
480
948
 
@@ -485,15 +953,28 @@ function createMiddlewareNextTracker<T>() {
485
953
  get nextResult() {
486
954
  return nextResult;
487
955
  },
956
+ setNextResult(result: T) {
957
+ nextResult = result;
958
+ },
488
959
  wrapNext<Fn extends (...args: any) => Effect.Effect<T, any, any>>(
489
960
  nextFn: Fn,
490
961
  ): Fn {
491
962
  return ((...args: Parameters<Fn>) => {
492
963
  nextInvoked = true;
493
- return Effect.map(nextFn(...args), (result) => {
964
+ const inner = nextFn(...args);
965
+ const tracked = Effect.map(inner, (result) => {
494
966
  nextResult = result;
495
967
  return result;
496
968
  });
969
+ if (isHybridContinuation(inner)) {
970
+ markHybridContinuation(tracked);
971
+ attachPromiseLikeContinuation(
972
+ tracked,
973
+ (inner as any).then.bind(inner),
974
+ );
975
+ return tracked;
976
+ }
977
+ return makeThenable ? makeThenable(tracked) : tracked;
497
978
  }) as Fn;
498
979
  },
499
980
  };
@@ -543,8 +1024,69 @@ function makeEffectMiddlewareOutput<
543
1024
  TRequirementsProvided,
544
1025
  >(
545
1026
  output: MiddlewareOutputFn<TOutput>,
1027
+ makeThenable?: <A, E, R>(
1028
+ effect: Effect.Effect<A, E, R>,
1029
+ ) => Effect.Effect<A, E, R>,
546
1030
  ): EffectMiddlewareOutput<TOutput, TEffectErrorMap, TRequirementsProvided> {
547
- return (value: TOutput) => withCurrentFiberContext(() => output(value));
1031
+ return (value: TOutput) => {
1032
+ const effect = withCurrentFiberContext(() => output(value));
1033
+ return makeThenable ? makeThenable(effect) : effect;
1034
+ };
1035
+ }
1036
+
1037
+ function makeThenableEffect<A, E, R>(
1038
+ effect: Effect.Effect<A, E, R>,
1039
+ runner: EffectRuntimeRunner<any, unknown>,
1040
+ fiberRefs: FiberRefs.FiberRefs,
1041
+ signal: AbortSignal | undefined,
1042
+ ): Effect.Effect<A, E, R> {
1043
+ if ("then" in effect) {
1044
+ return effect;
1045
+ }
1046
+
1047
+ attachPromiseLikeContinuation(
1048
+ effect,
1049
+ (
1050
+ onFulfilled: ((value: A) => unknown) | undefined,
1051
+ onRejected: ((error: unknown) => unknown) | undefined,
1052
+ ) =>
1053
+ runNestedEffect(effect, runner, fiberRefs, signal).then(
1054
+ onFulfilled,
1055
+ onRejected,
1056
+ ),
1057
+ );
1058
+
1059
+ return effect;
1060
+ }
1061
+
1062
+ async function runNestedEffect<A, E, R>(
1063
+ effect: Effect.Effect<A, E, R>,
1064
+ runner: EffectRuntimeRunner<any, unknown>,
1065
+ fiberRefs: FiberRefs.FiberRefs,
1066
+ signal: AbortSignal | undefined,
1067
+ ): Promise<A> {
1068
+ const exit = await runner.runPromiseExit(withFiberRefs(effect, fiberRefs), {
1069
+ signal,
1070
+ });
1071
+
1072
+ if (Exit.isFailure(exit)) {
1073
+ throw toORPCErrorFromCause(exit.cause);
1074
+ }
1075
+
1076
+ return exit.value;
1077
+ }
1078
+
1079
+ function withFiberRefs<A, E, R>(
1080
+ effect: Effect.Effect<A, E, R>,
1081
+ parentFiberRefs: FiberRefs.FiberRefs,
1082
+ ): Effect.Effect<A, E, R> {
1083
+ return Effect.fiberIdWith((fiberId) =>
1084
+ Effect.flatMap(Effect.getFiberRefs, (fiberRefs) =>
1085
+ Effect.setFiberRefs(
1086
+ FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs),
1087
+ ).pipe(Effect.andThen(effect)),
1088
+ ),
1089
+ );
548
1090
  }
549
1091
 
550
1092
  function withCurrentFiberContext<T>(fn: () => Promisable<T>): Effect.Effect<T> {