effect-cf 0.40.3 → 0.41.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.41.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#159](https://github.com/danieljvdm/effect-cf/pull/159) [`8cbc534`](https://github.com/danieljvdm/effect-cf/commit/8cbc5344ffa2aa031b6d98a2845ca3b945cf7d5f) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Reuse Durable Object RPC targets within each invocation so callbacks cannot grow a new subrequest chain on every call. Incoming requests and durable retries retain separate target lifetimes. Expose RpcTargets for native RPC adapters sharing the same invocation boundary.
8
+
3
9
  ## 0.40.3
4
10
 
5
11
  ### Patch Changes
@@ -2,6 +2,7 @@ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { layer as layer$1 } from "./Binding.mjs";
3
3
  import { causeMessage } from "./internal/ErrorMessage.mjs";
4
4
  import { decodeSuccess, encodeArgs } from "./RpcDefinition.mjs";
5
+ import { get, invalidate } from "./RpcTargets.mjs";
5
6
  import { withRpcClientSpan, withRpcTraceContext } from "./RpcTracing.mjs";
6
7
  import { invokeRpcMethod } from "./internal/RpcInvocation.mjs";
7
8
  import { resolve, scoped } from "./Rpc.mjs";
@@ -41,8 +42,16 @@ const makeClient = (definition) => {
41
42
  const newUniqueId = (options) => Effect.sync(() => namespace.newUniqueId(options));
42
43
  const idFromName = (name) => Effect.sync(() => namespace.idFromName(name));
43
44
  const idFromString = (id) => Effect.sync(() => namespace.idFromString(id));
44
- const get = (id, options) => Effect.sync(() => namespace.get(id, options));
45
- const getByName = (name, options) => Effect.sync(() => namespace.getByName(name, options));
45
+ const get$1 = (id, options) => get(namespace, JSON.stringify([
46
+ "id",
47
+ id.toString(),
48
+ options
49
+ ]), () => namespace.get(id, options)).pipe(Effect.orDie);
50
+ const getByName = (name, options) => get(namespace, JSON.stringify([
51
+ "name",
52
+ name,
53
+ options
54
+ ]), () => namespace.getByName(name, options)).pipe(Effect.orDie);
46
55
  const jurisdiction = (value) => Effect.sync(() => namespace.jurisdiction(value));
47
56
  const fetch = (stub, input, init) => Effect.tryPromise({
48
57
  try: (signal) => {
@@ -56,7 +65,7 @@ const makeClient = (definition) => {
56
65
  binding: definition.binding,
57
66
  cause
58
67
  })
59
- });
68
+ }).pipe(Effect.tapCause(() => invalidate(stub)));
60
69
  const rpc = Effect.fnUntraced(function* (stub, method, ...args) {
61
70
  const methodName = String(method);
62
71
  const encodedArgs = definition.definition === void 0 ? args : yield* encodeArgs(definition.definition, methodName, args).pipe(Effect.mapError((cause) => new DurableObjectRpcError({
@@ -69,7 +78,7 @@ const makeClient = (definition) => {
69
78
  binding: definition.binding,
70
79
  method: methodName,
71
80
  cause
72
- }));
81
+ })).pipe(Effect.tapCause(() => invalidate(stub)));
73
82
  });
74
83
  const decodeSuccess$1 = Effect.fnUntraced(function* (methodName, value) {
75
84
  if (definition.definition === void 0) return value;
@@ -81,7 +90,7 @@ const makeClient = (definition) => {
81
90
  });
82
91
  const call = Effect.fnUntraced(function* (stub, method, ...args) {
83
92
  const methodName = String(method);
84
- const value = yield* resolve(yield* rpc(stub, method, ...args)).pipe(Effect.mapError((cause) => new DurableObjectRpcError({
93
+ const value = yield* resolve(yield* rpc(stub, method, ...args)).pipe(Effect.tapCause(() => invalidate(stub)), Effect.mapError((cause) => new DurableObjectRpcError({
85
94
  binding: definition.binding,
86
95
  method: methodName,
87
96
  cause
@@ -91,7 +100,7 @@ const makeClient = (definition) => {
91
100
  const scopedCall = Effect.fnUntraced(function* (stub, method, ...args) {
92
101
  const methodName = String(method);
93
102
  const result = yield* rpc(stub, method, ...args);
94
- const value = yield* scoped(result).pipe(Effect.mapError((cause) => new DurableObjectRpcError({
103
+ const value = yield* scoped(result).pipe(Effect.tapCause(() => invalidate(stub)), Effect.mapError((cause) => new DurableObjectRpcError({
95
104
  binding: definition.binding,
96
105
  method: methodName,
97
106
  cause
@@ -101,14 +110,14 @@ const makeClient = (definition) => {
101
110
  const directMethods = makeDirectMethods(definition.definition, {
102
111
  call,
103
112
  fetch,
104
- get,
113
+ get: get$1,
105
114
  getByName
106
115
  });
107
116
  return Object.assign(directMethods, {
108
117
  newUniqueId,
109
118
  idFromName,
110
119
  idFromString,
111
- get,
120
+ get: get$1,
112
121
  getByName,
113
122
  jurisdiction,
114
123
  fetch,
@@ -0,0 +1,31 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Scope from "effect/Scope";
3
+ declare namespace RpcTargets_d_exports {
4
+ export { RpcTargetError, get, invalidate, withScope };
5
+ }
6
+ declare const RpcTargetError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
7
+ readonly _tag: "RpcTargetError";
8
+ } & Readonly<A>;
9
+ /** A native RPC target could not be constructed. */
10
+ declare class RpcTargetError extends RpcTargetError_base<{
11
+ readonly cause: unknown;
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
+ declare const get: <A extends object, Owner extends object>(owner: Owner, address: string, create: () => A) => Effect.Effect<A, RpcTargetError, never>;
23
+ /** Discard a failed native target so a subsequent call can acquire a fresh channel. */
24
+ declare const invalidate: <Target extends object>(target: Target) => Effect.Effect<void>;
25
+ /**
26
+ * Own RPC targets in the current Scope, including a transferred response stream,
27
+ * never across incoming requests or durable retries. effect-cf entrypoints install this boundary automatically.
28
+ */
29
+ declare const withScope: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | Scope.Scope>;
30
+ //#endregion
31
+ export { RpcTargetError, RpcTargets_d_exports, get, invalidate, withScope };
@@ -0,0 +1,73 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import * as Context from "effect/Context";
3
+ import * as Data from "effect/Data";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ //#region src/RpcTargets.ts
7
+ var RpcTargets_exports = /* @__PURE__ */ __exportAll({
8
+ RpcTargetError: () => RpcTargetError,
9
+ get: () => get,
10
+ invalidate: () => invalidate,
11
+ withScope: () => withScope
12
+ });
13
+ /** A native RPC target could not be constructed. */
14
+ var RpcTargetError = class extends Data.TaggedError("RpcTargetError") {};
15
+ var CurrentTargets = class extends Context.Service()("effect-cf/RpcTargets") {};
16
+ /**
17
+ * Reuse a native RPC target within the current invocation. Native Durable Object
18
+ * stubs capture the calling request's channel when created. Recreating a stub
19
+ * after a callback can extend that callback's call chain until Cloudflare's
20
+ * subrequest depth limit is reached, even when an alarm owns the work.
21
+ *
22
+ * The owner and address must identify one target and its construction options.
23
+ * Outside an effect-cf invocation (or withScope), each lookup constructs a target.
24
+ */
25
+ const get = (owner, address, create) => Effect.flatMap(Effect.serviceOption(CurrentTargets), (targets) => Effect.try({
26
+ try: () => Option.isSome(targets) ? targets.value.get(owner, address, create) : create(),
27
+ catch: (cause) => new RpcTargetError({ cause })
28
+ }));
29
+ /** Discard a failed native target so a subsequent call can acquire a fresh channel. */
30
+ const invalidate = (target) => Effect.map(Effect.serviceOption(CurrentTargets), (targets) => {
31
+ if (Option.isSome(targets)) targets.value.invalidate(target);
32
+ });
33
+ /**
34
+ * Own RPC targets in the current Scope, including a transferred response stream,
35
+ * never across incoming requests or durable retries. effect-cf entrypoints install this boundary automatically.
36
+ */
37
+ const withScope = (effect) => Effect.gen(function* () {
38
+ let active = true;
39
+ let owners = /* @__PURE__ */ new WeakMap();
40
+ let addresses = /* @__PURE__ */ new WeakMap();
41
+ const targets = {
42
+ get: (owner, address, create) => {
43
+ if (!active) return create();
44
+ let entries = owners.get(owner);
45
+ if (entries === void 0) {
46
+ entries = /* @__PURE__ */ new Map();
47
+ owners.set(owner, entries);
48
+ }
49
+ const existing = entries.get(address);
50
+ if (existing !== void 0) return existing;
51
+ const target = create();
52
+ entries.set(address, target);
53
+ addresses.set(target, {
54
+ entries,
55
+ address
56
+ });
57
+ return target;
58
+ },
59
+ invalidate: (target) => {
60
+ const entry = addresses.get(target);
61
+ if (entry?.entries.get(entry.address) === target) entry.entries.delete(entry.address);
62
+ addresses.delete(target);
63
+ }
64
+ };
65
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
66
+ active = false;
67
+ owners = /* @__PURE__ */ new WeakMap();
68
+ addresses = /* @__PURE__ */ new WeakMap();
69
+ }));
70
+ return yield* effect.pipe(Effect.provideService(CurrentTargets, targets));
71
+ });
72
+ //#endregion
73
+ export { RpcTargetError, RpcTargets_exports, get, invalidate, withScope };
package/dist/Worker.mjs CHANGED
@@ -2,6 +2,7 @@ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { fromExecutionContext } from "./internal/WorkerContext.mjs";
3
3
  import { fromMessage, fromMessageBatch } from "./Queue.mjs";
4
4
  import { reservedMethodNames as reservedMethodNames$1 } from "./RpcDefinition.mjs";
5
+ import { withScope } from "./RpcTargets.mjs";
5
6
  import { assumeEntrypointClass, defineEntrypointRpcMethods } from "./internal/Entrypoint.mjs";
6
7
  import { makeEntrypointRuntime, runEventPromise } from "./internal/Runtime.mjs";
7
8
  import { scheduleTelemetryFlush as scheduleTelemetryFlush$1 } from "./internal/Telemetry.mjs";
@@ -61,15 +62,15 @@ const renderFetchSuccess = (effect, eventLayer) => Effect.suspend(() => {
61
62
  context
62
63
  });
63
64
  });
64
- const telemetryMiddleware = HttpMiddleware.make((self) => Effect.gen(function* () {
65
+ const telemetryMiddleware = HttpMiddleware.make((self) => withScope(Effect.gen(function* () {
65
66
  yield* Effect.addFinalizer(() => scheduleTelemetryFlush);
66
67
  return yield* self;
67
- }));
68
- const handled = eventLayer === void 0 ? HttpEffect.toHandled(httpApp, handleResponse, telemetryMiddleware) : Effect.flatMap(References.TracerEnabled, (tracerEnabled) => HttpEffect.toHandled(httpApp, handleResponse, HttpMiddleware.make((self) => Effect.provideService(Effect.gen(function* () {
68
+ })));
69
+ const handled = eventLayer === void 0 ? HttpEffect.toHandled(httpApp, handleResponse, telemetryMiddleware) : Effect.flatMap(References.TracerEnabled, (tracerEnabled) => HttpEffect.toHandled(httpApp, handleResponse, HttpMiddleware.make((self) => withScope(Effect.provideService(Effect.gen(function* () {
69
70
  const context = yield* Layer.build(Layer.fresh(eventLayer));
70
71
  yield* Effect.addFinalizer(() => Effect.provideContext(scheduleTelemetryFlush, context));
71
72
  return yield* HttpMiddleware.tracer(self).pipe(Effect.onExit(() => Effect.yieldNow), Effect.provideContext(context));
72
- }), References.TracerEnabled, tracerEnabled))).pipe(Effect.withTracerEnabled(false)));
73
+ }), References.TracerEnabled, tracerEnabled)))).pipe(Effect.withTracerEnabled(false)));
73
74
  const captured = () => nativeResponse ?? webResponse ?? new Response(null, { status: 500 });
74
75
  return Effect.matchCause(handled, {
75
76
  onFailure: captured,
package/dist/Workflow.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { causeMessage } from "./internal/ErrorMessage.mjs";
3
3
  import { fromExecutionContext } from "./internal/WorkerContext.mjs";
4
+ import { withScope } from "./RpcTargets.mjs";
4
5
  import { assumeEntrypointClass } from "./internal/Entrypoint.mjs";
5
6
  import { makeEntrypointRuntime, runEventPromise } from "./internal/Runtime.mjs";
6
7
  import { ExecutionContext, WorkerContext } from "./Worker.mjs";
@@ -74,7 +75,7 @@ const fromWorkflowStep = (step) => ({
74
75
  return yield* Effect.tryPromise({
75
76
  try: () => {
76
77
  const run = (stepContext) => {
77
- const stepEffect = Effect.scoped(Effect.provideService(effect, WorkflowStepContext$1, stepContext));
78
+ const stepEffect = Effect.scoped(withScope(Effect.provideService(effect, WorkflowStepContext$1, stepContext)));
78
79
  return runPromise(exposeCloudflareNonRetryableError(Effect.provideContext(stepEffect, context)));
79
80
  };
80
81
  const rawStep = step;
package/dist/index.d.mts CHANGED
@@ -34,8 +34,9 @@ import { Email_d_exports } from "./Email.mjs";
34
34
  import { Images_d_exports } from "./Images.mjs";
35
35
  import { Kv_d_exports } from "./Kv.mjs";
36
36
  import { R2_d_exports } from "./R2.mjs";
37
+ import { RpcTargets_d_exports } from "./RpcTargets.mjs";
37
38
  import { Vectorize_d_exports } from "./Vectorize.mjs";
38
39
  import { WebTransport_d_exports } from "./WebTransport.mjs";
39
40
  import { WorkflowDefinition_d_exports } from "./WorkflowDefinition.mjs";
40
41
  import { Workflow_d_exports } from "./Workflow.mjs";
41
- export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTracing_d_exports as RpcTracing, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
42
+ export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTargets_d_exports as RpcTargets, RpcTracing_d_exports as RpcTracing, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
package/dist/index.mjs CHANGED
@@ -17,6 +17,7 @@ import { QueueBinding_exports } from "./QueueBinding.mjs";
17
17
  import { QueueDefinition_exports } from "./QueueDefinition.mjs";
18
18
  import { Queue_exports } from "./Queue.mjs";
19
19
  import { RpcDefinition_exports } from "./RpcDefinition.mjs";
20
+ import { RpcTargets_exports } from "./RpcTargets.mjs";
20
21
  import { RpcTracing_exports } from "./RpcTracing.mjs";
21
22
  import { Rpc_exports } from "./Rpc.mjs";
22
23
  import { ServiceBinding_exports } from "./ServiceBinding.mjs";
@@ -38,4 +39,4 @@ import { WebTransport_exports } from "./WebTransport.mjs";
38
39
  import { WorkflowBinding_exports } from "./WorkflowBinding.mjs";
39
40
  import { WorkflowDefinition_exports } from "./WorkflowDefinition.mjs";
40
41
  import { Workflow_exports } from "./Workflow.mjs";
41
- export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTracing_exports as RpcTracing, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
42
+ export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTargets_exports as RpcTargets, RpcTracing_exports as RpcTracing, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
@@ -1,4 +1,5 @@
1
1
  import { WorkerConfig, WorkerEnvironment } from "../Environment.mjs";
2
+ import { withScope } from "../RpcTargets.mjs";
2
3
  import { provideEntrypointServices } from "./Entrypoint.mjs";
3
4
  import * as Effect from "effect/Effect";
4
5
  import * as Layer from "effect/Layer";
@@ -21,10 +22,10 @@ const makeEntrypointRuntime = (layer, env, services) => {
21
22
  function runEventPromise(...args) {
22
23
  const [runtime, effect, eventLayer, parent] = args;
23
24
  if (eventLayer === void 0) {
24
- const event = Effect.scoped(effect);
25
+ const event = Effect.scoped(withScope(effect));
25
26
  return runtime.runPromise(parent === void 0 ? event : Effect.withParentSpan(event, parent));
26
27
  }
27
- const event = Effect.scoped(effect.pipe(Effect.provide(eventLayer, { local: true })));
28
+ const event = Effect.scoped(withScope(effect.pipe(Effect.provide(eventLayer, { local: true }))));
28
29
  return runtime.runPromise(parent === void 0 ? event : Effect.withParentSpan(event, parent));
29
30
  }
30
31
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.40.3",
3
+ "version": "0.41.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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,100 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Data from "effect/Data";
4
+ import type * as Scope from "effect/Scope";
5
+ import * as Option from "effect/Option";
6
+
7
+ /** A native RPC target could not be constructed. */
8
+ export class RpcTargetError extends Data.TaggedError("RpcTargetError")<{
9
+ readonly cause: unknown;
10
+ }> {}
11
+
12
+ interface Targets {
13
+ get<A extends object, Owner extends object>(owner: Owner, address: string, create: () => A): A;
14
+ invalidate<Target extends object>(target: Target): void;
15
+ }
16
+
17
+ class CurrentTargets extends Context.Service<CurrentTargets, Targets>()("effect-cf/RpcTargets") {}
18
+
19
+ /**
20
+ * Reuse a native RPC target within the current invocation. Native Durable Object
21
+ * stubs capture the calling request's channel when created. Recreating a stub
22
+ * after a callback can extend that callback's call chain until Cloudflare's
23
+ * subrequest depth limit is reached, even when an alarm owns the work.
24
+ *
25
+ * The owner and address must identify one target and its construction options.
26
+ * Outside an effect-cf invocation (or withScope), each lookup constructs a target.
27
+ */
28
+ export const get = <A extends object, Owner extends object>(
29
+ owner: Owner,
30
+ address: string,
31
+ create: () => A,
32
+ ) =>
33
+ Effect.flatMap(Effect.serviceOption(CurrentTargets), (targets) =>
34
+ Effect.try({
35
+ try: () => (Option.isSome(targets) ? targets.value.get(owner, address, create) : create()),
36
+ catch: (cause) => new RpcTargetError({ cause }),
37
+ }),
38
+ );
39
+
40
+ /** Discard a failed native target so a subsequent call can acquire a fresh channel. */
41
+ export const invalidate = <Target extends object>(target: Target): Effect.Effect<void> =>
42
+ Effect.map(Effect.serviceOption(CurrentTargets), (targets) => {
43
+ if (Option.isSome(targets)) targets.value.invalidate(target);
44
+ });
45
+
46
+ /**
47
+ * Own RPC targets in the current Scope, including a transferred response stream,
48
+ * never across incoming requests or durable retries. effect-cf entrypoints install this boundary automatically.
49
+ */
50
+ export const withScope = <A, E, R>(
51
+ effect: Effect.Effect<A, E, R>,
52
+ ): Effect.Effect<A, E, R | Scope.Scope> =>
53
+ Effect.gen(function* () {
54
+ let active = true;
55
+ let owners = new WeakMap<object, Map<string, object>>();
56
+ let addresses = new WeakMap<object, { entries: Map<string, object>; address: string }>();
57
+ const targets: Targets = {
58
+ get: <T extends object, Owner extends object>(
59
+ owner: Owner,
60
+ address: string,
61
+ create: () => T,
62
+ ): T => {
63
+ if (!active) return create();
64
+ let entries = owners.get(owner);
65
+
66
+ if (entries === undefined) {
67
+ entries = new Map();
68
+ owners.set(owner, entries);
69
+ }
70
+ const existing = entries.get(address);
71
+
72
+ if (existing !== undefined) {
73
+ // SAFETY: the caller's owner/address identifies the same native target type.
74
+ return existing as T;
75
+ }
76
+ const target = create();
77
+
78
+ entries.set(address, target);
79
+ addresses.set(target, { entries, address });
80
+
81
+ return target;
82
+ },
83
+ invalidate: (target) => {
84
+ const entry = addresses.get(target);
85
+
86
+ if (entry?.entries.get(entry.address) === target) entry.entries.delete(entry.address);
87
+ addresses.delete(target);
88
+ },
89
+ };
90
+
91
+ yield* Effect.addFinalizer(() =>
92
+ Effect.sync(() => {
93
+ active = false;
94
+ owners = new WeakMap();
95
+ addresses = new WeakMap();
96
+ }),
97
+ );
98
+
99
+ return yield* effect.pipe(Effect.provideService(CurrentTargets, targets));
100
+ });
package/src/Worker.ts CHANGED
@@ -13,6 +13,7 @@ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
13
13
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
14
14
  import { fromMessage, fromMessageBatch, type QueueHandler } from "./Queue";
15
15
  import * as RpcDefinition from "./RpcDefinition";
16
+ import * as RpcTargets from "./RpcTargets";
16
17
  import type { ReceiverOptions, RpcInvocationInfo } from "./RpcTracing";
17
18
  import * as Entrypoint from "./internal/Entrypoint";
18
19
  import * as Runtime from "./internal/Runtime";
@@ -273,11 +274,13 @@ const renderFetchSuccess = <E, R, REvent, EventLayerError, EventLayerRequirement
273
274
  });
274
275
  });
275
276
  const telemetryMiddleware = HttpMiddleware.make((self) =>
276
- Effect.gen(function* () {
277
- yield* Effect.addFinalizer(() => scheduleTelemetryFlush);
277
+ RpcTargets.withScope(
278
+ Effect.gen(function* () {
279
+ yield* Effect.addFinalizer(() => scheduleTelemetryFlush);
278
280
 
279
- return yield* self;
280
- }),
281
+ return yield* self;
282
+ }),
283
+ ),
281
284
  );
282
285
  const handled =
283
286
  eventLayer === undefined
@@ -287,24 +290,26 @@ const renderFetchSuccess = <E, R, REvent, EventLayerError, EventLayerRequirement
287
290
  httpApp,
288
291
  handleResponse,
289
292
  HttpMiddleware.make((self) =>
290
- Effect.provideService(
291
- Effect.gen(function* () {
292
- const context = yield* Layer.build(Layer.fresh(eventLayer));
293
-
294
- yield* Effect.addFinalizer(() =>
295
- Effect.provideContext(scheduleTelemetryFlush, context),
296
- );
297
-
298
- return yield* HttpMiddleware.tracer(self).pipe(
299
- // The tracer schedules span completion. Let it publish on
300
- // every exit with event references still installed, before
301
- // the exporter scope finalizes.
302
- Effect.onExit(() => Effect.yieldNow),
303
- Effect.provideContext(context),
304
- );
305
- }),
306
- References.TracerEnabled,
307
- tracerEnabled,
293
+ RpcTargets.withScope(
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
+ ),
308
313
  ),
309
314
  ),
310
315
  ).pipe(Effect.withTracerEnabled(false)),
package/src/Workflow.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as RpcTargets from "./RpcTargets";
1
2
  import {
2
3
  WorkflowEntrypoint as CloudflareWorkflowEntrypoint,
3
4
  type WorkflowEvent as CloudflareWorkflowEvent,
@@ -151,7 +152,11 @@ const fromWorkflowStep = (step: CloudflareWorkflowStep): WorkflowStepService =>
151
152
  A,
152
153
  E,
153
154
  Exclude<R, WorkflowStepContext>
154
- > = Effect.scoped(Effect.provideService(effect, WorkflowStepContext, stepContext));
155
+ > = Effect.scoped(
156
+ RpcTargets.withScope(
157
+ Effect.provideService(effect, WorkflowStepContext, stepContext),
158
+ ),
159
+ );
155
160
 
156
161
  return runPromise(
157
162
  exposeCloudflareNonRetryableError(Effect.provideContext(stepEffect, context)),
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export * as R2 from "./R2";
29
29
  export * as Rpc from "./Rpc";
30
30
  export * as RpcDefinition from "./RpcDefinition";
31
31
  export * as RpcTracing from "./RpcTracing";
32
+ export * as RpcTargets from "./RpcTargets";
32
33
  export * as ServiceBinding from "./ServiceBinding";
33
34
  export * as Vectorize from "./Vectorize";
34
35
  export * as WebTransport from "./WebTransport";
@@ -5,6 +5,7 @@ import * as ManagedRuntime from "effect/ManagedRuntime";
5
5
  import type { Scope, Tracer } from "effect";
6
6
 
7
7
  import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "../Environment";
8
+ import * as RpcTargets from "../RpcTargets";
8
9
  import { provideEntrypointServices } from "./Entrypoint";
9
10
 
10
11
  /**
@@ -61,12 +62,14 @@ export function runEventPromise<A, E, R, REvent, EventLayerError, LayerError>(
61
62
  const [runtime, effect, eventLayer, parent] = args;
62
63
 
63
64
  if (eventLayer === undefined) {
64
- const event = Effect.scoped(effect);
65
+ const event = Effect.scoped(RpcTargets.withScope(effect));
65
66
 
66
67
  return runtime.runPromise(parent === undefined ? event : Effect.withParentSpan(event, parent));
67
68
  }
68
69
 
69
- const event = Effect.scoped(effect.pipe(Effect.provide(eventLayer, { local: true })));
70
+ const event = Effect.scoped(
71
+ RpcTargets.withScope(effect.pipe(Effect.provide(eventLayer, { local: true }))),
72
+ );
70
73
 
71
74
  return runtime.runPromise(parent === undefined ? event : Effect.withParentSpan(event, parent));
72
75
  }