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.
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import { Layer } from "effect";
39
39
 
40
40
  // src/effect-runtime.ts
41
41
  import { ORPCError as ORPCError2 } from "@orpc/contract";
42
- import { Cause as Cause2, Effect, Exit, Option } from "effect";
42
+ import { Cause as Cause2, Effect, Exit, FiberRefs, Option } from "effect";
43
43
 
44
44
  // src/tagged-error.ts
45
45
  import {
@@ -175,6 +175,7 @@ function effectErrorMapToErrorMap(errorMap) {
175
175
  }
176
176
 
177
177
  // src/effect-runtime.ts
178
+ var HybridContinuationSymbol = /* @__PURE__ */ Symbol("effect-orpc/HybridContinuation");
178
179
  function toORPCErrorFromCause(cause) {
179
180
  return Cause2.match(cause, {
180
181
  onDie(defect) {
@@ -230,17 +231,17 @@ function createEffectProcedureHandler(options) {
230
231
  };
231
232
  const spanName = spanConfig?.name ?? opts.path.join(".");
232
233
  const captureStackTrace = spanConfig?.captureStackTrace ?? defaultCaptureStackTrace;
233
- const resolver = Effect.fnUntraced(effectFn);
234
- const handlerEffect = resolver(effectOpts);
234
+ const handlerEffect = callEffectCallback(effectFn, effectOpts);
235
235
  const tracedEffect = Effect.withSpan(
236
236
  runEffectPipeline({
237
237
  baseOptions: effectOpts,
238
238
  effectErrorMap,
239
239
  final: (context) => Effect.map(
240
- context === effectOpts.context ? handlerEffect : resolver({ ...effectOpts, context }),
240
+ context === effectOpts.context ? handlerEffect : callEffectCallback(effectFn, { ...effectOpts, context }),
241
241
  (output) => ({ output, context: {} })
242
242
  ),
243
243
  input: opts.input,
244
+ runner,
244
245
  steps: effectSteps
245
246
  }),
246
247
  spanName,
@@ -268,6 +269,7 @@ function createEffectPipelineMiddleware(options) {
268
269
  )
269
270
  ),
270
271
  input,
272
+ runner,
271
273
  steps
272
274
  });
273
275
  const exit = await runner.runPromiseExit(effect, {
@@ -277,12 +279,152 @@ function createEffectPipelineMiddleware(options) {
277
279
  return exit.value;
278
280
  };
279
281
  }
282
+ function createEffectOrORPCMiddleware(options) {
283
+ const { runner, effectErrorMap, middleware } = options;
284
+ return async (opts, input, output) => {
285
+ const effectOptions = makeEffectMiddlewareOptions({
286
+ effectErrorMap,
287
+ final: opts.next,
288
+ options: opts,
289
+ output
290
+ });
291
+ return runEffectOrORPCMiddlewareCallback({
292
+ effectOptions,
293
+ input,
294
+ middleware,
295
+ nativeNext: opts.next,
296
+ runner,
297
+ signal: opts.signal
298
+ });
299
+ };
300
+ }
301
+ async function runEffectOrORPCMiddlewareCallback(options) {
302
+ const result = options.middleware(
303
+ options.effectOptions,
304
+ options.input,
305
+ options.effectOptions.output
306
+ );
307
+ const classified = classifyEffectOrORPCMiddlewareResult(result);
308
+ switch (classified._tag) {
309
+ case "nativeContinuation":
310
+ return classified.result;
311
+ case "nativeGuardOnly":
312
+ return options.nativeNext();
313
+ case "nativeResult":
314
+ return classified.result;
315
+ case "effect": {
316
+ const exit = await options.runner.runPromiseExit(
317
+ runEffectMiddlewareResult({
318
+ autoNext: () => options.effectOptions.next(),
319
+ effect: classified.effect,
320
+ nextInvoked: options.effectOptions.nextTracker.nextInvoked,
321
+ nextResult: options.effectOptions.nextTracker.nextResult
322
+ }),
323
+ { signal: options.signal }
324
+ );
325
+ if (Exit.isFailure(exit)) throw toORPCErrorFromCause(exit.cause);
326
+ return exit.value;
327
+ }
328
+ }
329
+ }
330
+ function classifyEffectOrORPCMiddlewareResult(result) {
331
+ if (isHybridContinuation(result)) {
332
+ return {
333
+ _tag: "nativeContinuation",
334
+ result
335
+ };
336
+ }
337
+ if (Effect.isEffect(result) || isGeneratorIterator(result)) {
338
+ return {
339
+ _tag: "effect",
340
+ effect: effectFromCallbackResult(result)
341
+ };
342
+ }
343
+ if (result === void 0) {
344
+ return { _tag: "nativeGuardOnly" };
345
+ }
346
+ return {
347
+ _tag: "nativeResult",
348
+ result
349
+ };
350
+ }
351
+ function runEffectMiddlewareResult(options) {
352
+ return Effect.flatMap(
353
+ options.effect,
354
+ (result) => resolveEffectMiddlewareContinuation({
355
+ autoNext: options.autoNext,
356
+ nextInvoked: options.nextInvoked,
357
+ nextResult: options.nextResult,
358
+ result
359
+ })
360
+ );
361
+ }
362
+ function makeEffectMiddlewareOptions(options) {
363
+ const nextTracker = createMiddlewareNextTracker();
364
+ const effectOptions = {
365
+ context: options.options.context,
366
+ path: options.options.path,
367
+ procedure: options.options.procedure,
368
+ signal: options.options.signal,
369
+ lastEventId: options.options.lastEventId,
370
+ errors: createEffectErrorConstructorMap(options.effectErrorMap),
371
+ next: nextTracker.wrapNext(
372
+ (...rest) => makeHybridMiddlewareContinuation({
373
+ final: options.final,
374
+ nextTracker,
375
+ rest
376
+ })
377
+ ),
378
+ nextTracker,
379
+ output: makeEffectMiddlewareOutput(options.output)
380
+ };
381
+ return effectOptions;
382
+ }
383
+ function makeHybridMiddlewareContinuation(options) {
384
+ const nextContext = options.rest[0]?.context ?? {};
385
+ const toEffectResult = (result) => ({
386
+ output: result.output,
387
+ context: nextContext
388
+ });
389
+ const effect = Effect.map(
390
+ withCurrentFiberContext(() => options.final(...options.rest)),
391
+ toEffectResult
392
+ );
393
+ return makeHybridContinuation(effect, {
394
+ onResult: options.nextTracker.setNextResult,
395
+ run: () => options.final(...options.rest),
396
+ toEffectResult
397
+ });
398
+ }
399
+ function makeHybridContinuation(effect, options) {
400
+ markHybridContinuation(effect);
401
+ attachPromiseLikeContinuation(
402
+ effect,
403
+ (onFulfilled, onRejected) => Promise.resolve(options.run()).then(options.toEffectResult).then((result) => {
404
+ options.onResult?.(result);
405
+ return result;
406
+ }).then(onFulfilled, onRejected)
407
+ );
408
+ return effect;
409
+ }
410
+ function markHybridContinuation(value) {
411
+ Object.defineProperty(value, HybridContinuationSymbol, {
412
+ configurable: true,
413
+ value: true
414
+ });
415
+ }
416
+ function attachPromiseLikeContinuation(value, then) {
417
+ Object.defineProperty(value, "then", {
418
+ configurable: true,
419
+ value: then
420
+ });
421
+ }
280
422
  function createEffectProviderMiddleware(options) {
281
423
  const { runner, effectErrorMap, tag, provider } = options;
282
424
  return async (opts, input) => {
283
425
  const effectOpts = makeEffectOptions(opts, input, effectErrorMap);
284
426
  const effect = Effect.flatMap(
285
- provider(effectOpts),
427
+ callEffectCallback(provider, effectOpts),
286
428
  (service) => Effect.provideService(
287
429
  withCurrentFiberContext(() => opts.next()),
288
430
  tag,
@@ -301,7 +443,7 @@ function createEffectOptionalProviderMiddleware(options) {
301
443
  return async (opts, input) => {
302
444
  const effectOpts = makeEffectOptions(opts, input, effectErrorMap);
303
445
  const effect = Effect.flatMap(
304
- provider(effectOpts),
446
+ callEffectCallback(provider, effectOpts),
305
447
  (service) => Option.match(service, {
306
448
  onNone: () => withCurrentFiberContext(() => opts.next()),
307
449
  onSome: (value) => Effect.provideService(
@@ -318,9 +460,50 @@ function createEffectOptionalProviderMiddleware(options) {
318
460
  return exit.value;
319
461
  };
320
462
  }
321
- function isEffectMiddleware(value) {
463
+ function callEffectCallback(callback, ...args) {
464
+ if (isGeneratorFunction(callback)) {
465
+ return Effect.fnUntraced(callback)(...args);
466
+ }
467
+ return Effect.suspend(() => {
468
+ try {
469
+ return effectFromCallbackResult(callback(...args));
470
+ } catch (error) {
471
+ return Effect.fail(error);
472
+ }
473
+ });
474
+ }
475
+ function effectFromCallbackResult(result) {
476
+ if (Effect.isEffect(result)) {
477
+ return result;
478
+ }
479
+ if (isGeneratorIterator(result)) {
480
+ return Effect.fnUntraced(function* () {
481
+ return yield* result;
482
+ })();
483
+ }
484
+ if (isPromiseLike(result)) {
485
+ return Effect.promise(() => result);
486
+ }
487
+ return Effect.succeed(result);
488
+ }
489
+ function isGeneratorIterator(value) {
490
+ return typeof value === "object" && value !== null && "next" in value && typeof value.next === "function" && "throw" in value && typeof value.throw === "function";
491
+ }
492
+ function isHybridContinuation(value) {
493
+ return (typeof value === "object" || typeof value === "function") && value !== null && HybridContinuationSymbol in value;
494
+ }
495
+ function isPromiseLike(value) {
496
+ return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
497
+ }
498
+ function isGeneratorFunction(value) {
322
499
  return typeof value === "function" && value.constructor?.name === "GeneratorFunction";
323
500
  }
501
+ function isEffectMiddleware(value) {
502
+ return isGeneratorFunction(value);
503
+ }
504
+ function isDecoratedMiddleware(value) {
505
+ return typeof value === "function" && "mapInput" in value && "concat" in value;
506
+ }
324
507
  function makeEffectOptions(opts, input, effectErrorMap) {
325
508
  return {
326
509
  context: opts.context,
@@ -339,13 +522,13 @@ function runEffectPipeline(options) {
339
522
  const stepOptions = { ...options.baseOptions, context };
340
523
  if (step._tag === "provide") {
341
524
  return Effect.flatMap(
342
- step.provider(stepOptions),
525
+ callEffectCallback(step.provider, stepOptions),
343
526
  (service) => Effect.provideService(run(index + 1, context), step.tag, service)
344
527
  );
345
528
  }
346
529
  if (step._tag === "provideOptional") {
347
530
  return Effect.flatMap(
348
- step.provider(stepOptions),
531
+ callEffectCallback(step.provider, stepOptions),
349
532
  (service) => Option.match(service, {
350
533
  onNone: () => run(index + 1, context),
351
534
  onSome: (value) => Effect.provideService(run(index + 1, context), step.tag, value)
@@ -355,46 +538,57 @@ function runEffectPipeline(options) {
355
538
  if (step._tag === "provideLayer") {
356
539
  return Effect.provide(run(index + 1, context), step.layer);
357
540
  }
358
- const nextTracker = createMiddlewareNextTracker();
359
- const effectOptions = {
360
- context,
361
- path: stepOptions.path,
362
- procedure: stepOptions.procedure,
363
- signal: stepOptions.signal,
364
- lastEventId: stepOptions.lastEventId,
365
- errors: createEffectErrorConstructorMap(options.effectErrorMap),
366
- next: nextTracker.wrapNext(
367
- (...rest) => {
368
- const nextContext = rest[0]?.context ?? {};
369
- return Effect.map(
370
- run(index + 1, { ...context, ...nextContext }),
371
- (result) => ({
372
- output: result.output,
373
- context: nextContext
374
- })
375
- );
376
- }
377
- )
378
- };
379
- const effectOutput = makeEffectMiddlewareOutput((output) => ({ output, context: {} }));
380
- const middlewareEffect = Effect.fnUntraced(step.middleware)(
381
- effectOptions,
382
- options.input,
383
- effectOutput
384
- );
385
- return Effect.flatMap(
386
- middlewareEffect,
387
- (result) => resolveEffectMiddlewareContinuation({
388
- autoNext: () => effectOptions.next(),
389
- nextInvoked: nextTracker.nextInvoked,
390
- nextResult: nextTracker.nextResult,
391
- result
392
- })
393
- );
541
+ return Effect.flatMap(Effect.getFiberRefs, (fiberRefs) => {
542
+ const makeThenable = (effect) => makeThenableEffect(
543
+ effect,
544
+ options.runner,
545
+ fiberRefs,
546
+ stepOptions.signal
547
+ );
548
+ const nextTracker = createMiddlewareNextTracker(
549
+ makeThenable
550
+ );
551
+ const effectOptions = {
552
+ context,
553
+ path: stepOptions.path,
554
+ procedure: stepOptions.procedure,
555
+ signal: stepOptions.signal,
556
+ lastEventId: stepOptions.lastEventId,
557
+ errors: createEffectErrorConstructorMap(options.effectErrorMap),
558
+ next: nextTracker.wrapNext(
559
+ (...rest) => {
560
+ const nextContext = rest[0]?.context ?? {};
561
+ return Effect.map(
562
+ run(index + 1, { ...context, ...nextContext }),
563
+ (result) => ({
564
+ output: result.output,
565
+ context: nextContext
566
+ })
567
+ );
568
+ }
569
+ )
570
+ };
571
+ const effectOutput = makeEffectMiddlewareOutput((output) => ({ output, context: {} }), makeThenable);
572
+ const middlewareEffect = callEffectCallback(
573
+ step.middleware,
574
+ effectOptions,
575
+ options.input,
576
+ effectOutput
577
+ );
578
+ return Effect.flatMap(
579
+ middlewareEffect,
580
+ (result) => resolveEffectMiddlewareContinuation({
581
+ autoNext: () => effectOptions.next(),
582
+ nextInvoked: nextTracker.nextInvoked,
583
+ nextResult: nextTracker.nextResult,
584
+ result
585
+ })
586
+ );
587
+ });
394
588
  };
395
589
  return run(0, options.baseOptions.context);
396
590
  }
397
- function createMiddlewareNextTracker() {
591
+ function createMiddlewareNextTracker(makeThenable) {
398
592
  let nextInvoked = false;
399
593
  let nextResult;
400
594
  return {
@@ -404,13 +598,26 @@ function createMiddlewareNextTracker() {
404
598
  get nextResult() {
405
599
  return nextResult;
406
600
  },
601
+ setNextResult(result) {
602
+ nextResult = result;
603
+ },
407
604
  wrapNext(nextFn) {
408
605
  return ((...args) => {
409
606
  nextInvoked = true;
410
- return Effect.map(nextFn(...args), (result) => {
607
+ const inner = nextFn(...args);
608
+ const tracked = Effect.map(inner, (result) => {
411
609
  nextResult = result;
412
610
  return result;
413
611
  });
612
+ if (isHybridContinuation(inner)) {
613
+ markHybridContinuation(tracked);
614
+ attachPromiseLikeContinuation(
615
+ tracked,
616
+ inner.then.bind(inner)
617
+ );
618
+ return tracked;
619
+ }
620
+ return makeThenable ? makeThenable(tracked) : tracked;
414
621
  });
415
622
  }
416
623
  };
@@ -432,8 +639,43 @@ function resolveEffectMiddlewareContinuation(options) {
432
639
  }
433
640
  return autoNext();
434
641
  }
435
- function makeEffectMiddlewareOutput(output) {
436
- return (value) => withCurrentFiberContext(() => output(value));
642
+ function makeEffectMiddlewareOutput(output, makeThenable) {
643
+ return (value) => {
644
+ const effect = withCurrentFiberContext(() => output(value));
645
+ return makeThenable ? makeThenable(effect) : effect;
646
+ };
647
+ }
648
+ function makeThenableEffect(effect, runner, fiberRefs, signal) {
649
+ if ("then" in effect) {
650
+ return effect;
651
+ }
652
+ attachPromiseLikeContinuation(
653
+ effect,
654
+ (onFulfilled, onRejected) => runNestedEffect(effect, runner, fiberRefs, signal).then(
655
+ onFulfilled,
656
+ onRejected
657
+ )
658
+ );
659
+ return effect;
660
+ }
661
+ async function runNestedEffect(effect, runner, fiberRefs, signal) {
662
+ const exit = await runner.runPromiseExit(withFiberRefs(effect, fiberRefs), {
663
+ signal
664
+ });
665
+ if (Exit.isFailure(exit)) {
666
+ throw toORPCErrorFromCause(exit.cause);
667
+ }
668
+ return exit.value;
669
+ }
670
+ function withFiberRefs(effect, parentFiberRefs) {
671
+ return Effect.fiberIdWith(
672
+ (fiberId) => Effect.flatMap(
673
+ Effect.getFiberRefs,
674
+ (fiberRefs) => Effect.setFiberRefs(
675
+ FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs)
676
+ ).pipe(Effect.andThen(effect))
677
+ )
678
+ );
437
679
  }
438
680
  function withCurrentFiberContext(fn) {
439
681
  return Effect.flatMap(
@@ -818,15 +1060,33 @@ function createEffectProcedureProxy(target, decorated) {
818
1060
  return (middleware, mapInput) => {
819
1061
  const def = getEffectProcedureDef(context);
820
1062
  if (!mapInput && isEffectMiddleware(middleware)) {
821
- return new EffectDecoratedProcedure(
822
- appendEffectStep(def, {
823
- _tag: "middleware",
824
- middleware
825
- })
826
- );
1063
+ const step = {
1064
+ _tag: "middleware",
1065
+ middleware
1066
+ };
1067
+ if (def.effectHandler) {
1068
+ return new EffectDecoratedProcedure(
1069
+ appendEffectStep(def, step)
1070
+ );
1071
+ }
1072
+ return new EffectDecoratedProcedure({
1073
+ ...def,
1074
+ middlewares: addMiddleware(
1075
+ def.middlewares,
1076
+ createEffectPipelineMiddleware({
1077
+ effectErrorMap: state.effectErrorMap,
1078
+ runner: state.runner,
1079
+ steps: [step]
1080
+ })
1081
+ )
1082
+ });
827
1083
  }
828
1084
  const flushedDef = flushEffectSteps(def);
829
- const mapped = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) : middleware;
1085
+ const mapped = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) : isDecoratedMiddleware(middleware) ? middleware : createEffectOrORPCMiddleware({
1086
+ effectErrorMap: state.effectErrorMap,
1087
+ middleware,
1088
+ runner: state.runner
1089
+ });
830
1090
  return new EffectDecoratedProcedure({
831
1091
  ...flushedDef,
832
1092
  middlewares: addMiddleware(flushedDef.middlewares, mapped)
@@ -938,7 +1198,7 @@ function enhanceEffectRouter(router, options) {
938
1198
  }
939
1199
 
940
1200
  // src/runtime-source.ts
941
- import { Effect as Effect2, FiberRefs, ManagedRuntime } from "effect";
1201
+ import { Effect as Effect2, FiberRefs as FiberRefs2, ManagedRuntime } from "effect";
942
1202
  function makeEffectRuntimeRunner(source) {
943
1203
  if (source === void 0) {
944
1204
  return {
@@ -970,7 +1230,7 @@ function withParentFiberRefs(effect) {
970
1230
  (fiberId) => Effect2.flatMap(
971
1231
  Effect2.getFiberRefs,
972
1232
  (fiberRefs) => Effect2.setFiberRefs(
973
- FiberRefs.joinAs(fiberRefs, fiberId, parentFiberRefs)
1233
+ FiberRefs2.joinAs(fiberRefs, fiberId, parentFiberRefs)
974
1234
  ).pipe(Effect2.andThen(effect))
975
1235
  )
976
1236
  ) : effect;
@@ -1116,25 +1376,22 @@ function createEffectBuilderProxy(target) {
1116
1376
  case "middleware":
1117
1377
  return getOrCreateVirtualMethod2(context, prop, () => {
1118
1378
  return (middleware) => {
1119
- if (isEffectMiddleware(middleware)) {
1120
- const effectMiddleware = createEffectPipelineMiddleware({
1121
- effectErrorMap: state.effectErrorMap,
1122
- runner: state.runner,
1123
- steps: [
1124
- ...state.effectSteps ?? [],
1125
- { _tag: "middleware", middleware }
1126
- ]
1127
- });
1128
- return Reflect.apply(
1129
- Reflect.get(source, "middleware", source),
1130
- source,
1131
- [effectMiddleware]
1132
- );
1133
- }
1379
+ const effectMiddleware = isEffectMiddleware(middleware) ? createEffectPipelineMiddleware({
1380
+ effectErrorMap: state.effectErrorMap,
1381
+ runner: state.runner,
1382
+ steps: [
1383
+ ...state.effectSteps ?? [],
1384
+ { _tag: "middleware", middleware }
1385
+ ]
1386
+ }) : createEffectOrORPCMiddleware({
1387
+ effectErrorMap: state.effectErrorMap,
1388
+ middleware,
1389
+ runner: state.runner
1390
+ });
1134
1391
  return Reflect.apply(
1135
1392
  Reflect.get(source, "middleware", source),
1136
1393
  source,
1137
- [middleware]
1394
+ [effectMiddleware]
1138
1395
  );
1139
1396
  };
1140
1397
  });
@@ -1166,7 +1423,7 @@ function createEffectBuilderProxy(target) {
1166
1423
  case "use":
1167
1424
  return getOrCreateVirtualMethod2(context, prop, () => {
1168
1425
  return (middleware, ...rest) => {
1169
- if (isEffectMiddleware(middleware) && rest.length === 0) {
1426
+ if (rest.length === 0 && isEffectMiddleware(middleware)) {
1170
1427
  return wrapBuilderLike(
1171
1428
  source,
1172
1429
  appendEffectStep2(state, {
@@ -1179,7 +1436,14 @@ function createEffectBuilderProxy(target) {
1179
1436
  const nextBuilder = Reflect.apply(
1180
1437
  Reflect.get(flushed.builder, "use", flushed.builder),
1181
1438
  flushed.builder,
1182
- [middleware, ...rest]
1439
+ [
1440
+ rest.length === 0 && !isDecoratedMiddleware(middleware) ? createEffectOrORPCMiddleware({
1441
+ effectErrorMap: flushed.state.effectErrorMap,
1442
+ middleware,
1443
+ runner: flushed.state.runner
1444
+ }) : middleware,
1445
+ ...rest
1446
+ ]
1183
1447
  );
1184
1448
  return wrapBuilderLike(nextBuilder, flushed.state);
1185
1449
  };
@@ -1196,10 +1460,16 @@ function createEffectBuilderProxy(target) {
1196
1460
  });
1197
1461
  case "handler":
1198
1462
  return getOrCreateVirtualMethod2(context, prop, () => {
1199
- return (handler) => new EffectDecoratedProcedure({
1200
- ...effectDef,
1201
- handler
1202
- });
1463
+ return (handler) => {
1464
+ const flushed = flushEffectSteps2(source, state);
1465
+ return new EffectDecoratedProcedure({
1466
+ ...flushed.builder["~orpc"],
1467
+ effectErrorMap: flushed.state.effectErrorMap,
1468
+ runner: flushed.state.runner,
1469
+ effectSteps: flushed.state.effectSteps,
1470
+ handler
1471
+ });
1472
+ };
1203
1473
  });
1204
1474
  case "router":
1205
1475
  return getOrCreateVirtualMethod2(context, prop, () => {