effect-cf 0.40.3 → 0.41.1

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.
@@ -11,6 +11,7 @@ import * as RpcDefinition from "./RpcDefinition";
11
11
  import * as ErrorMessage from "./internal/ErrorMessage";
12
12
  import * as RpcInvocation from "./internal/RpcInvocation";
13
13
  import * as RpcTracing from "./RpcTracing";
14
+ import * as RpcTargets from "./RpcTargets";
14
15
 
15
16
  const expectedDurableObjectNamespace =
16
17
  "Durable Object namespace binding with getByName(), get(), idFromName(), idFromString(), newUniqueId(), and jurisdiction()";
@@ -348,12 +349,18 @@ export const makeClient = <
348
349
  const get = (
349
350
  id: globalThis.DurableObjectId,
350
351
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
351
- ) => Effect.sync(() => namespace.get(id, options));
352
+ ) =>
353
+ RpcTargets.get(namespace, JSON.stringify(["id", id.toString(), options]), () =>
354
+ namespace.get(id, options),
355
+ ).pipe(Effect.orDie);
352
356
 
353
357
  const getByName = (
354
358
  name: string,
355
359
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
356
- ) => Effect.sync(() => namespace.getByName(name, options));
360
+ ) =>
361
+ RpcTargets.get(namespace, JSON.stringify(["name", name, options]), () =>
362
+ namespace.getByName(name, options),
363
+ ).pipe(Effect.orDie);
357
364
 
358
365
  const jurisdiction = (value: globalThis.DurableObjectJurisdiction) =>
359
366
  Effect.sync(() => namespace.jurisdiction(value));
@@ -377,7 +384,7 @@ export const makeClient = <
377
384
  });
378
385
  },
379
386
  catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
380
- });
387
+ }).pipe(Effect.tapCause(() => RpcTargets.invalidate(stub)));
381
388
 
382
389
  const rpc = Effect.fnUntraced(function* <Method extends StubMethodKey<Api>>(
383
390
  stub: StubClient,
@@ -419,7 +426,7 @@ export const makeClient = <
419
426
  method: methodName,
420
427
  cause,
421
428
  }),
422
- );
429
+ ).pipe(Effect.tapCause(() => RpcTargets.invalidate(stub)));
423
430
  });
424
431
 
425
432
  const decodeSuccess = Effect.fnUntraced(function* <Method extends StubMethodKey<Api>>(
@@ -459,6 +466,7 @@ export const makeClient = <
459
466
  ): Effect.fn.Return<StubMethodSuccess<Api, Method>, DurableObjectRpcError> {
460
467
  const methodName = String(method);
461
468
  const value = yield* CloudflareRpc.resolve(yield* rpc(stub, method, ...args)).pipe(
469
+ Effect.tapCause(() => RpcTargets.invalidate(stub)),
462
470
  Effect.mapError(
463
471
  (cause) =>
464
472
  new DurableObjectRpcError({
@@ -484,6 +492,7 @@ export const makeClient = <
484
492
  const methodName = String(method);
485
493
  const result = yield* rpc(stub, method, ...args);
486
494
  const value = yield* CloudflareRpc.scoped(result).pipe(
495
+ Effect.tapCause(() => RpcTargets.invalidate(stub)),
487
496
  Effect.mapError(
488
497
  (cause) =>
489
498
  new DurableObjectRpcError({
@@ -0,0 +1,94 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Data from "effect/Data";
3
+ import type * as Scope from "effect/Scope";
4
+ import * as Option from "effect/Option";
5
+
6
+ import { CurrentTargets, type Targets } from "./internal/RpcTargets";
7
+
8
+ /** A native RPC target could not be constructed. */
9
+ export class RpcTargetError extends Data.TaggedError("RpcTargetError")<{
10
+ readonly cause: unknown;
11
+ }> {}
12
+
13
+ /**
14
+ * Reuse a native RPC target within the current invocation. Native Durable Object
15
+ * stubs capture the calling request's channel when created. Recreating a stub
16
+ * after a callback can extend that callback's call chain until Cloudflare's
17
+ * subrequest depth limit is reached, even when an alarm owns the work.
18
+ *
19
+ * The owner and address must identify one target and its construction options.
20
+ * Outside an effect-cf invocation (or withScope), each lookup constructs a target.
21
+ */
22
+ export const get = <A extends object, Owner extends object>(
23
+ owner: Owner,
24
+ address: string,
25
+ create: () => A,
26
+ ) =>
27
+ Effect.flatMap(Effect.serviceOption(CurrentTargets), (targets) =>
28
+ Effect.try({
29
+ try: () => (Option.isSome(targets) ? targets.value.get(owner, address, create) : create()),
30
+ catch: (cause) => new RpcTargetError({ cause }),
31
+ }),
32
+ );
33
+
34
+ /** Discard a failed native target so a subsequent call can acquire a fresh channel. */
35
+ export const invalidate = <Target extends object>(target: Target): Effect.Effect<void> =>
36
+ Effect.map(Effect.serviceOption(CurrentTargets), (targets) => {
37
+ if (Option.isSome(targets)) targets.value.invalidate(target);
38
+ });
39
+
40
+ /**
41
+ * Own RPC targets in the current Scope, including a transferred response stream,
42
+ * never across incoming requests or durable retries. effect-cf entrypoints install this boundary automatically.
43
+ */
44
+ export const withScope = <A, E, R>(
45
+ effect: Effect.Effect<A, E, R>,
46
+ ): Effect.Effect<A, E, R | Scope.Scope> =>
47
+ Effect.gen(function* () {
48
+ let active = true;
49
+ let owners = new WeakMap<object, Map<string, object>>();
50
+ let addresses = new WeakMap<object, { entries: Map<string, object>; address: string }>();
51
+ const targets: Targets = {
52
+ get: <T extends object, Owner extends object>(
53
+ owner: Owner,
54
+ address: string,
55
+ create: () => T,
56
+ ): T => {
57
+ if (!active) return create();
58
+ let entries = owners.get(owner);
59
+
60
+ if (entries === undefined) {
61
+ entries = new Map();
62
+ owners.set(owner, entries);
63
+ }
64
+ const existing = entries.get(address);
65
+
66
+ if (existing !== undefined) {
67
+ // SAFETY: the caller's owner/address identifies the same native target type.
68
+ return existing as T;
69
+ }
70
+ const target = create();
71
+
72
+ entries.set(address, target);
73
+ addresses.set(target, { entries, address });
74
+
75
+ return target;
76
+ },
77
+ invalidate: (target) => {
78
+ const entry = addresses.get(target);
79
+
80
+ if (entry?.entries.get(entry.address) === target) entry.entries.delete(entry.address);
81
+ addresses.delete(target);
82
+ },
83
+ };
84
+
85
+ yield* Effect.addFinalizer(() =>
86
+ Effect.sync(() => {
87
+ active = false;
88
+ owners = new WeakMap();
89
+ addresses = new WeakMap();
90
+ }),
91
+ );
92
+
93
+ return yield* effect.pipe(Effect.provideService(CurrentTargets, targets));
94
+ });