effect-orpc 0.3.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,16 +3,17 @@ 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 type { ManagedRuntime } from "effect";
13
13
  import { Cause, Effect, Exit, FiberRefs, Option } from "effect";
14
14
 
15
- import { getCurrentFiberRefs, runWithFiberRefs } from "./fiber-context-bridge";
15
+ import { runWithFiberRefs } from "./fiber-context-bridge";
16
+ import type { EffectRuntimeRunner } from "./runtime-source";
16
17
  import type { EffectErrorConstructorMap, EffectErrorMap } from "./tagged-error";
17
18
  import {
18
19
  createEffectErrorConstructorMap,
@@ -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> {
@@ -78,7 +108,7 @@ export function createEffectProcedureHandler<
78
108
  TRuntimeError,
79
109
  TMeta extends Meta,
80
110
  >(options: {
81
- runtime: ManagedRuntime.ManagedRuntime<TRequirementsProvided, TRuntimeError>;
111
+ runner: EffectRuntimeRunner<TRequirementsProvided, TRuntimeError>;
82
112
  effectErrorMap: TEffectErrorMap;
83
113
  effectFn: EffectProcedureHandler<
84
114
  TCurrentContext,
@@ -99,7 +129,7 @@ export function createEffectProcedureHandler<
99
129
  TMeta & Record<never, never>
100
130
  > {
101
131
  const {
102
- runtime,
132
+ runner,
103
133
  effectErrorMap,
104
134
  effectFn,
105
135
  effectSteps = [],
@@ -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,19 +165,19 @@ 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,
146
176
  { captureStackTrace },
147
177
  );
148
- const exit = await runtime.runPromiseExit(
149
- withParentFiberRefs(tracedEffect),
150
- { signal: opts.signal },
151
- );
178
+ const exit = await runner.runPromiseExit(tracedEffect, {
179
+ signal: opts.signal,
180
+ });
152
181
 
153
182
  if (Exit.isFailure(exit)) {
154
183
  throw toORPCErrorFromCause(exit.cause);
@@ -166,7 +195,7 @@ export function createEffectPipelineMiddleware<
166
195
  TRuntimeError,
167
196
  TMeta extends Meta,
168
197
  >(options: {
169
- runtime: ManagedRuntime.ManagedRuntime<TRequirementsProvided, TRuntimeError>;
198
+ runner: EffectRuntimeRunner<TRequirementsProvided, TRuntimeError>;
170
199
  effectErrorMap: TEffectErrorMap;
171
200
  steps: readonly EffectPipelineStep[];
172
201
  }): Middleware<
@@ -177,7 +206,7 @@ export function createEffectPipelineMiddleware<
177
206
  any,
178
207
  TMeta
179
208
  > {
180
- const { runtime, effectErrorMap, steps } = options;
209
+ const { runner, effectErrorMap, steps } = options;
181
210
 
182
211
  return async (opts, input) => {
183
212
  const baseOptions = makeEffectOptions<
@@ -186,7 +215,14 @@ export function createEffectPipelineMiddleware<
186
215
  TEffectErrorMap,
187
216
  TMeta
188
217
  >(opts, input, effectErrorMap);
189
- const effect = runEffectPipeline({
218
+ const effect = runEffectPipeline<
219
+ TCurrentContext,
220
+ unknown,
221
+ TOutput,
222
+ TEffectErrorMap,
223
+ TRequirementsProvided,
224
+ TMeta
225
+ >({
190
226
  baseOptions,
191
227
  effectErrorMap,
192
228
  final: (context) =>
@@ -198,12 +234,12 @@ export function createEffectPipelineMiddleware<
198
234
  ),
199
235
  ) as any,
200
236
  input,
237
+ runner,
201
238
  steps,
202
239
  });
203
- const exit = await runtime.runPromiseExit(
204
- withParentFiberRefs(effect as any),
205
- { signal: opts.signal },
206
- );
240
+ const exit = await runner.runPromiseExit(effect, {
241
+ signal: opts.signal,
242
+ });
207
243
 
208
244
  if (Exit.isFailure(exit)) throw toORPCErrorFromCause(exit.cause);
209
245
 
@@ -211,6 +247,341 @@ export function createEffectPipelineMiddleware<
211
247
  };
212
248
  }
213
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
+
214
585
  export function createEffectProviderMiddleware<
215
586
  TCurrentContext extends Context,
216
587
  TInput,
@@ -220,7 +591,7 @@ export function createEffectProviderMiddleware<
220
591
  TMeta extends Meta,
221
592
  TTag extends EffectTag,
222
593
  >(options: {
223
- runtime: ManagedRuntime.ManagedRuntime<TRequirementsProvided, TRuntimeError>;
594
+ runner: EffectRuntimeRunner<TRequirementsProvided, TRuntimeError>;
224
595
  effectErrorMap: TEffectErrorMap;
225
596
  tag: TTag;
226
597
  provider: EffectProvider<
@@ -232,7 +603,7 @@ export function createEffectProviderMiddleware<
232
603
  TTag
233
604
  >;
234
605
  }): Middleware<TCurrentContext, Record<never, never>, TInput, any, any, TMeta> {
235
- const { runtime, effectErrorMap, tag, provider } = options;
606
+ const { runner, effectErrorMap, tag, provider } = options;
236
607
 
237
608
  return async (opts, input) => {
238
609
  const effectOpts = makeEffectOptions<
@@ -241,14 +612,16 @@ export function createEffectProviderMiddleware<
241
612
  TEffectErrorMap,
242
613
  TMeta
243
614
  >(opts, input, effectErrorMap);
244
- const effect = Effect.flatMap(provider(effectOpts), (service) =>
245
- Effect.provideService(
246
- withCurrentFiberContext(() => opts.next()),
247
- tag,
248
- service,
249
- ),
615
+ const effect = Effect.flatMap(
616
+ callEffectCallback(provider, effectOpts),
617
+ (service) =>
618
+ Effect.provideService(
619
+ withCurrentFiberContext(() => opts.next()),
620
+ tag,
621
+ service,
622
+ ),
250
623
  );
251
- const exit = await runtime.runPromiseExit(withParentFiberRefs(effect), {
624
+ const exit = await runner.runPromiseExit(effect, {
252
625
  signal: opts.signal,
253
626
  });
254
627
 
@@ -267,7 +640,7 @@ export function createEffectOptionalProviderMiddleware<
267
640
  TMeta extends Meta,
268
641
  TTag extends EffectTag,
269
642
  >(options: {
270
- runtime: ManagedRuntime.ManagedRuntime<TRequirementsProvided, TRuntimeError>;
643
+ runner: EffectRuntimeRunner<TRequirementsProvided, TRuntimeError>;
271
644
  effectErrorMap: TEffectErrorMap;
272
645
  tag: TTag;
273
646
  provider: EffectOptionalProvider<
@@ -279,7 +652,7 @@ export function createEffectOptionalProviderMiddleware<
279
652
  TTag
280
653
  >;
281
654
  }): Middleware<TCurrentContext, Record<never, never>, TInput, any, any, TMeta> {
282
- const { runtime, effectErrorMap, tag, provider } = options;
655
+ const { runner, effectErrorMap, tag, provider } = options;
283
656
 
284
657
  return async (opts, input) => {
285
658
  const effectOpts = makeEffectOptions<
@@ -288,18 +661,20 @@ export function createEffectOptionalProviderMiddleware<
288
661
  TEffectErrorMap,
289
662
  TMeta
290
663
  >(opts, input, effectErrorMap);
291
- const effect = Effect.flatMap(provider(effectOpts), (service) =>
292
- Option.match(service, {
293
- onNone: () => withCurrentFiberContext(() => opts.next()),
294
- onSome: (value) =>
295
- Effect.provideService(
296
- withCurrentFiberContext(() => opts.next()),
297
- tag,
298
- value,
299
- ),
300
- }),
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
+ }),
301
676
  );
302
- const exit = await runtime.runPromiseExit(withParentFiberRefs(effect), {
677
+ const exit = await runner.runPromiseExit(effect, {
303
678
  signal: opts.signal,
304
679
  });
305
680
 
@@ -309,6 +684,80 @@ export function createEffectOptionalProviderMiddleware<
309
684
  };
310
685
  }
311
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
+
312
761
  // todo(utopy): make this check more comprehensive, maybe add a Symbol to the EffectMiddleware type
313
762
  export function isEffectMiddleware(
314
763
  value: unknown,
@@ -321,9 +770,12 @@ export function isEffectMiddleware(
321
770
  unknown,
322
771
  Meta
323
772
  > {
773
+ return isGeneratorFunction(value);
774
+ }
775
+
776
+ export function isDecoratedMiddleware(value: unknown): boolean {
324
777
  return (
325
- typeof value === "function" &&
326
- value.constructor?.name === "GeneratorFunction"
778
+ typeof value === "function" && "mapInput" in value && "concat" in value
327
779
  );
328
780
  }
329
781
 
@@ -378,6 +830,7 @@ function runEffectPipeline<
378
830
  TRequirementsProvided
379
831
  >;
380
832
  input: TInput;
833
+ runner: EffectRuntimeRunner<TRequirementsProvided, unknown>;
381
834
  steps: readonly EffectPipelineStep[];
382
835
  }): Effect.Effect<
383
836
  EffectMiddlewareResult<Context, TOutput>,
@@ -398,18 +851,22 @@ function runEffectPipeline<
398
851
  const stepOptions = { ...options.baseOptions, context };
399
852
 
400
853
  if (step._tag === "provide") {
401
- return Effect.flatMap(step.provider(stepOptions as any), (service) =>
402
- 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),
403
858
  );
404
859
  }
405
860
 
406
861
  if (step._tag === "provideOptional") {
407
- return Effect.flatMap(step.provider(stepOptions as any), (service) =>
408
- Option.match(service, {
409
- onNone: () => run(index + 1, context),
410
- onSome: (value) =>
411
- Effect.provideService(run(index + 1, context), step.tag, value),
412
- }),
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
+ }),
413
870
  );
414
871
  }
415
872
 
@@ -417,59 +874,75 @@ function runEffectPipeline<
417
874
  return Effect.provide(run(index + 1, context), step.layer);
418
875
  }
419
876
 
420
- const nextTracker =
421
- createMiddlewareNextTracker<EffectMiddlewareResult<Context, TOutput>>();
422
- const effectOptions: EffectMiddlewareOptions<
423
- TCurrentContext,
424
- TOutput,
425
- TEffectErrorMap,
426
- TRequirementsProvided,
427
- TMeta
428
- > = {
429
- context,
430
- path: stepOptions.path,
431
- procedure: stepOptions.procedure,
432
- signal: stepOptions.signal,
433
- lastEventId: stepOptions.lastEventId,
434
- errors: createEffectErrorConstructorMap(options.effectErrorMap),
435
- next: nextTracker.wrapNext(
436
- (...rest: [MiddlewareNextFnOptions<Context>?]) => {
437
- const nextContext = rest[0]?.context ?? {};
438
- return Effect.map(
439
- run(index + 1, { ...context, ...nextContext }),
440
- (result) => ({
441
- output: result.output,
442
- context: nextContext,
443
- }),
444
- ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput>>;
445
- },
446
- ),
447
- };
448
- const effectOutput = makeEffectMiddlewareOutput<
449
- TOutput,
450
- TEffectErrorMap,
451
- TRequirementsProvided
452
- >((output) => ({ output, context: {} }));
453
- const middlewareEffect = Effect.fnUntraced(step.middleware)(
454
- effectOptions,
455
- options.input,
456
- effectOutput,
457
- ) as Effect.Effect<EffectMiddlewareResult<Context, TOutput> | void>;
458
-
459
- return Effect.flatMap(middlewareEffect, (result) =>
460
- resolveEffectMiddlewareContinuation({
461
- autoNext: () => effectOptions.next(),
462
- nextInvoked: nextTracker.nextInvoked,
463
- nextResult: nextTracker.nextResult,
464
- result,
465
- }),
466
- );
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
+ });
467
936
  };
468
937
 
469
938
  return run(0, options.baseOptions.context);
470
939
  }
471
940
 
472
- 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
+ ) {
473
946
  let nextInvoked = false;
474
947
  let nextResult: T | undefined;
475
948
 
@@ -480,15 +953,28 @@ function createMiddlewareNextTracker<T>() {
480
953
  get nextResult() {
481
954
  return nextResult;
482
955
  },
956
+ setNextResult(result: T) {
957
+ nextResult = result;
958
+ },
483
959
  wrapNext<Fn extends (...args: any) => Effect.Effect<T, any, any>>(
484
960
  nextFn: Fn,
485
961
  ): Fn {
486
962
  return ((...args: Parameters<Fn>) => {
487
963
  nextInvoked = true;
488
- return Effect.map(nextFn(...args), (result) => {
964
+ const inner = nextFn(...args);
965
+ const tracked = Effect.map(inner, (result) => {
489
966
  nextResult = result;
490
967
  return result;
491
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;
492
978
  }) as Fn;
493
979
  },
494
980
  };
@@ -538,8 +1024,69 @@ function makeEffectMiddlewareOutput<
538
1024
  TRequirementsProvided,
539
1025
  >(
540
1026
  output: MiddlewareOutputFn<TOutput>,
1027
+ makeThenable?: <A, E, R>(
1028
+ effect: Effect.Effect<A, E, R>,
1029
+ ) => Effect.Effect<A, E, R>,
541
1030
  ): EffectMiddlewareOutput<TOutput, TEffectErrorMap, TRequirementsProvided> {
542
- 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
+ );
543
1090
  }
544
1091
 
545
1092
  function withCurrentFiberContext<T>(fn: () => Promisable<T>): Effect.Effect<T> {
@@ -549,18 +1096,3 @@ function withCurrentFiberContext<T>(fn: () => Promisable<T>): Effect.Effect<T> {
549
1096
  ),
550
1097
  );
551
1098
  }
552
-
553
- function withParentFiberRefs<A, E, R>(
554
- effect: Effect.Effect<A, E, R>,
555
- ): Effect.Effect<A, E, R> {
556
- const parentFiberRefs = getCurrentFiberRefs();
557
- return parentFiberRefs
558
- ? Effect.fiberIdWith((fiberId) =>
559
- Effect.flatMap(Effect.getFiberRefs, (fiberRefs) =>
560
- Effect.setFiberRefs(
561
- FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs),
562
- ).pipe(Effect.andThen(effect)),
563
- ),
564
- )
565
- : effect;
566
- }