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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.41.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#161](https://github.com/danieljvdm/effect-cf/pull/161) [`a627f80`](https://github.com/danieljvdm/effect-cf/commit/a627f80449bf40227bddf917640aae9574faa563) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Retain Sandbox and Container RPC channels for each invocation so repeated lookups after callbacks do not exhaust Cloudflare's subrequest depth. Sandbox configuration continues to apply on each lookup, and failed channels are replaced on subsequent acquisition.
8
+
9
+ ## 0.41.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#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.
14
+
3
15
  ## 0.40.3
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  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
+ import { get, invalidate } from "./RpcTargets.mjs";
4
5
  import * as Context from "effect/Context";
5
6
  import * as Data from "effect/Data";
6
7
  import * as Effect from "effect/Effect";
@@ -37,7 +38,7 @@ var ContainerOperationError = class extends Data.TaggedError("ContainerOperation
37
38
  return `Container ${this.operation} failed for binding "${this.binding}" instance "${this.instance}": ${causeMessage(this.cause)}`;
38
39
  }
39
40
  };
40
- const tryContainerPromise = (definition, instance, operation, evaluate) => Effect.tryPromise({
41
+ const tryContainerPromise = (definition, instance, operation, evaluate, target) => Effect.tryPromise({
41
42
  try: evaluate,
42
43
  catch: (cause) => new ContainerOperationError({
43
44
  binding: definition.binding,
@@ -45,43 +46,44 @@ const tryContainerPromise = (definition, instance, operation, evaluate) => Effec
45
46
  operation,
46
47
  cause
47
48
  })
48
- });
49
+ }).pipe(Effect.tapCause(() => invalidate(target)));
49
50
  const makeInstanceClient = (definition, instance, stub) => {
51
+ const attempt = (operation, evaluate) => tryContainerPromise(definition, instance, operation, evaluate, stub);
50
52
  const spanOptions = (operation) => ({ attributes: {
51
53
  binding: definition.binding,
52
54
  instance,
53
55
  operation
54
56
  } });
55
57
  const hostListOperation = (operation) => Effect.fn(`ContainerNamespace.${operation}`, spanOptions(operation))(function* (hosts) {
56
- return yield* tryContainerPromise(definition, instance, operation, () => stub[operation]([...hosts]));
58
+ return yield* attempt(operation, () => stub[operation]([...hosts]));
57
59
  });
58
60
  const hostnameOperation = (operation) => Effect.fn(`ContainerNamespace.${operation}`, spanOptions(operation))(function* (hostname) {
59
- return yield* tryContainerPromise(definition, instance, operation, () => stub[operation](hostname));
61
+ return yield* attempt(operation, () => stub[operation](hostname));
60
62
  });
61
63
  return {
62
64
  rawUnsafe: Effect.succeed(stub),
63
- state: tryContainerPromise(definition, instance, "state", () => stub.getState()).pipe(Effect.flatMap(S.decodeUnknownEffect(ContainerState)), Effect.mapError((cause) => cause instanceof ContainerOperationError ? cause : new ContainerOperationError({
65
+ state: attempt("state", () => stub.getState()).pipe(Effect.flatMap(S.decodeUnknownEffect(ContainerState)), Effect.mapError((cause) => cause instanceof ContainerOperationError ? cause : new ContainerOperationError({
64
66
  binding: definition.binding,
65
67
  instance,
66
68
  operation: "state",
67
69
  cause
68
70
  })), Effect.withSpan("ContainerNamespace.state", spanOptions("state"))),
69
71
  fetch: Effect.fn("ContainerNamespace.fetch", spanOptions("fetch"))(function* (input, init) {
70
- return yield* tryContainerPromise(definition, instance, "fetch", () => stub.fetch(input, init));
72
+ return yield* attempt("fetch", () => stub.fetch(input, init));
71
73
  }),
72
74
  start: Effect.fn("ContainerNamespace.start", spanOptions("start"))(function* (options, waitOptions) {
73
- return yield* tryContainerPromise(definition, instance, "start", () => stub.start(options, waitOptions));
75
+ return yield* attempt("start", () => stub.start(options, waitOptions));
74
76
  }),
75
77
  startAndWaitForPorts: Effect.fn("ContainerNamespace.startAndWaitForPorts", spanOptions("startAndWaitForPorts"))(function* (options) {
76
- return yield* tryContainerPromise(definition, instance, "startAndWaitForPorts", () => stub.startAndWaitForPorts(options));
78
+ return yield* attempt("startAndWaitForPorts", () => stub.startAndWaitForPorts(options));
77
79
  }),
78
80
  waitForPort: Effect.fn("ContainerNamespace.waitForPort", spanOptions("waitForPort"))(function* (options) {
79
- return yield* tryContainerPromise(definition, instance, "waitForPort", () => stub.waitForPort(options));
81
+ return yield* attempt("waitForPort", () => stub.waitForPort(options));
80
82
  }),
81
83
  stop: Effect.fn("ContainerNamespace.stop", spanOptions("stop"))(function* (signal) {
82
- return yield* tryContainerPromise(definition, instance, "stop", () => stub.stop(signal));
84
+ return yield* attempt("stop", () => stub.stop(signal));
83
85
  }),
84
- destroy: tryContainerPromise(definition, instance, "destroy", () => stub.destroy()).pipe(Effect.withSpan("ContainerNamespace.destroy", spanOptions("destroy"))),
86
+ destroy: attempt("destroy", () => stub.destroy()).pipe(Effect.withSpan("ContainerNamespace.destroy", spanOptions("destroy"))),
85
87
  setAllowedHosts: hostListOperation("setAllowedHosts"),
86
88
  setDeniedHosts: hostListOperation("setDeniedHosts"),
87
89
  allowHost: hostnameOperation("allowHost"),
@@ -92,15 +94,16 @@ const makeInstanceClient = (definition, instance, stub) => {
92
94
  };
93
95
  const isContainerNamespaceResource = (value) => Predicate.hasProperty(value, "getByName") && Predicate.isFunction(value.getByName);
94
96
  const makeClient = (definition) => (namespace) => {
95
- const getByName = (name, options) => Effect.try({
96
- try: () => makeInstanceClient(definition, name, namespace.getByName(name, options)),
97
- catch: (cause) => new ContainerOperationError({
98
- binding: definition.binding,
99
- instance: name,
100
- operation: "getByName",
101
- cause
102
- })
103
- });
97
+ const getByName = (name, options) => get(namespace, JSON.stringify([
98
+ "container",
99
+ name,
100
+ options
101
+ ]), () => namespace.getByName(name, options)).pipe(Effect.map((stub) => makeInstanceClient(definition, name, stub)), Effect.mapError((error) => new ContainerOperationError({
102
+ binding: definition.binding,
103
+ instance: name,
104
+ operation: "getByName",
105
+ cause: error.cause
106
+ })));
104
107
  const byName = (name, options) => {
105
108
  const instance = getByName(name, options);
106
109
  return {
@@ -1,6 +1,7 @@
1
1
  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
+ import { get, invalidate } from "./RpcTargets.mjs";
4
5
  import { decodeSuccess, encodeArgs } from "./RpcDefinition.mjs";
5
6
  import { withRpcClientSpan, withRpcTraceContext } from "./RpcTracing.mjs";
6
7
  import { invokeRpcMethod } from "./internal/RpcInvocation.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,72 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { CurrentTargets } from "./internal/RpcTargets.mjs";
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
+ /**
16
+ * Reuse a native RPC target within the current invocation. Native Durable Object
17
+ * stubs capture the calling request's channel when created. Recreating a stub
18
+ * after a callback can extend that callback's call chain until Cloudflare's
19
+ * subrequest depth limit is reached, even when an alarm owns the work.
20
+ *
21
+ * The owner and address must identify one target and its construction options.
22
+ * Outside an effect-cf invocation (or withScope), each lookup constructs a target.
23
+ */
24
+ const get = (owner, address, create) => Effect.flatMap(Effect.serviceOption(CurrentTargets), (targets) => Effect.try({
25
+ try: () => Option.isSome(targets) ? targets.value.get(owner, address, create) : create(),
26
+ catch: (cause) => new RpcTargetError({ cause })
27
+ }));
28
+ /** Discard a failed native target so a subsequent call can acquire a fresh channel. */
29
+ const invalidate = (target) => Effect.map(Effect.serviceOption(CurrentTargets), (targets) => {
30
+ if (Option.isSome(targets)) targets.value.invalidate(target);
31
+ });
32
+ /**
33
+ * Own RPC targets in the current Scope, including a transferred response stream,
34
+ * never across incoming requests or durable retries. effect-cf entrypoints install this boundary automatically.
35
+ */
36
+ const withScope = (effect) => Effect.gen(function* () {
37
+ let active = true;
38
+ let owners = /* @__PURE__ */ new WeakMap();
39
+ let addresses = /* @__PURE__ */ new WeakMap();
40
+ const targets = {
41
+ get: (owner, address, create) => {
42
+ if (!active) return create();
43
+ let entries = owners.get(owner);
44
+ if (entries === void 0) {
45
+ entries = /* @__PURE__ */ new Map();
46
+ owners.set(owner, entries);
47
+ }
48
+ const existing = entries.get(address);
49
+ if (existing !== void 0) return existing;
50
+ const target = create();
51
+ entries.set(address, target);
52
+ addresses.set(target, {
53
+ entries,
54
+ address
55
+ });
56
+ return target;
57
+ },
58
+ invalidate: (target) => {
59
+ const entry = addresses.get(target);
60
+ if (entry?.entries.get(entry.address) === target) entry.entries.delete(entry.address);
61
+ addresses.delete(target);
62
+ }
63
+ };
64
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
65
+ active = false;
66
+ owners = /* @__PURE__ */ new WeakMap();
67
+ addresses = /* @__PURE__ */ new WeakMap();
68
+ }));
69
+ return yield* effect.pipe(Effect.provideService(CurrentTargets, targets));
70
+ });
71
+ //#endregion
72
+ export { RpcTargetError, RpcTargets_exports, get, invalidate, withScope };