effect-cf 0.22.0 → 0.23.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.mjs CHANGED
@@ -995,6 +995,45 @@ const handlers = (options) => ({
995
995
  webSocketError: options.error
996
996
  });
997
997
  //#endregion
998
+ //#region src/internal/WorkerContext.ts
999
+ const causeError = (cause) => {
1000
+ const squashed = Cause.squash(cause);
1001
+ return squashed instanceof Error ? squashed : new Error(Cause.pretty(cause), { cause: squashed });
1002
+ };
1003
+ const failureHandler = (label, cause, options) => (options?.onFailure?.(cause) ?? Effect.logError(`${label} failed`, Cause.pretty(cause))).pipe(Effect.catchCause((handlerCause) => Effect.logError(`${label} failure handler failed`, Cause.pretty(cause), Cause.pretty(handlerCause))));
1004
+ /**
1005
+ * Builds a `waitUntil` scheduler for any Cloudflare lifecycle that accepts
1006
+ * background promises (`ExecutionContext`, `DurableObjectState`).
1007
+ *
1008
+ * Background effects capture the calling fiber's context via `Effect.context`
1009
+ * before being registered, so the default module-level `Effect.runPromiseExit`
1010
+ * runner is sufficient; entrypoints may still pass a runtime-bound runner.
1011
+ */
1012
+ const makeWaitUntilScheduler = (label, register, runPromiseExit = (effect) => Effect.runPromiseExit(effect)) => {
1013
+ return (effect, options, mode) => Effect.context().pipe(Effect.flatMap((context) => Effect.sync(() => {
1014
+ const runHandler = (cause) => runPromiseExit(Effect.scoped(Effect.provideContext(failureHandler(label, cause, options), context))).then((exit) => {
1015
+ if (Exit.isFailure(exit)) console.error(`${label} failure handler failed`, Cause.pretty(exit.cause));
1016
+ });
1017
+ register(runPromiseExit(Effect.scoped(Effect.provideContext(effect, context))).then(async (exit) => {
1018
+ if (Exit.isSuccess(exit)) return;
1019
+ await runHandler(exit.cause);
1020
+ if (mode === "propagate") throw causeError(exit.cause);
1021
+ }));
1022
+ })));
1023
+ };
1024
+ /**
1025
+ * Builds the `WorkerContext` service from a native `ExecutionContext`.
1026
+ */
1027
+ const fromExecutionContext = (ctx, runPromiseExit = (effect) => Effect.runPromiseExit(effect)) => {
1028
+ const schedule = makeWaitUntilScheduler("WorkerContext.waitUntil", (promise) => ctx.waitUntil(promise), runPromiseExit);
1029
+ return {
1030
+ raw: ctx,
1031
+ waitUntil: (effect, options) => schedule(effect, options, options?.mode ?? "observe"),
1032
+ waitUntilPropagating: (effect, options) => schedule(effect, options, "propagate"),
1033
+ passThroughOnException: Effect.sync(() => ctx.passThroughOnException())
1034
+ };
1035
+ };
1036
+ //#endregion
998
1037
  //#region src/DurableObjectState.ts
999
1038
  var DurableObjectState_exports = /* @__PURE__ */ __exportAll({
1000
1039
  DurableObjectState: () => DurableObjectState,
@@ -1007,23 +1046,27 @@ var DurableObjectState = class extends Context.Service()("effect-cf/DurableObjec
1007
1046
  /**
1008
1047
  * Wraps a native Cloudflare `DurableObjectState` as a {@link DurableObjectStateService}.
1009
1048
  */
1010
- const fromDurableObjectState = (state) => ({
1011
- raw: state,
1012
- id: state.id,
1013
- storage: fromDurableObjectStorage(state.storage),
1014
- waitUntil: (promise) => Effect.sync(() => state.waitUntil(promise)),
1015
- blockConcurrencyWhile: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => runPromiseExitPreservingTypedFailures(context, effect)))), Effect.flatten),
1016
- blockConcurrencyWhileOrReset: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect))))),
1017
- acceptWebSocket: (ws, tags) => Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
1018
- getWebSockets: (tag) => Effect.sync(() => state.getWebSockets(tag).map((socket) => fromWebSocket(socket))),
1019
- setWebSocketAutoResponse: (pair) => Effect.sync(() => state.setWebSocketAutoResponse(pair)),
1020
- getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
1021
- getWebSocketAutoResponseTimestamp: (ws) => Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws.raw)),
1022
- setHibernatableWebSocketEventTimeout: (timeoutMs) => Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
1023
- getHibernatableWebSocketEventTimeout: Effect.sync(() => state.getHibernatableWebSocketEventTimeout()),
1024
- getTags: (ws) => Effect.sync(() => state.getTags(ws.raw)),
1025
- abort: (reason) => Effect.sync(() => state.abort(reason))
1026
- });
1049
+ const fromDurableObjectState = (state) => {
1050
+ const scheduleWaitUntil = makeWaitUntilScheduler("DurableObjectState.waitUntil", (promise) => state.waitUntil(promise));
1051
+ const waitUntil = ((input, options) => Effect.isEffect(input) ? scheduleWaitUntil(input, options, options?.mode ?? "observe") : Effect.sync(() => state.waitUntil(input)));
1052
+ return {
1053
+ raw: state,
1054
+ id: state.id,
1055
+ storage: fromDurableObjectStorage(state.storage),
1056
+ waitUntil,
1057
+ blockConcurrencyWhile: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => runPromiseExitPreservingTypedFailures(context, effect)))), Effect.flatten),
1058
+ blockConcurrencyWhileOrReset: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect))))),
1059
+ acceptWebSocket: (ws, tags) => Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
1060
+ getWebSockets: (tag) => Effect.sync(() => state.getWebSockets(tag).map((socket) => fromWebSocket(socket))),
1061
+ setWebSocketAutoResponse: (pair) => Effect.sync(() => state.setWebSocketAutoResponse(pair)),
1062
+ getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
1063
+ getWebSocketAutoResponseTimestamp: (ws) => Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws.raw)),
1064
+ setHibernatableWebSocketEventTimeout: (timeoutMs) => Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
1065
+ getHibernatableWebSocketEventTimeout: Effect.sync(() => state.getHibernatableWebSocketEventTimeout()),
1066
+ getTags: (ws) => Effect.sync(() => state.getTags(ws.raw)),
1067
+ abort: (reason) => Effect.sync(() => state.abort(reason))
1068
+ };
1069
+ };
1027
1070
  const runPromiseExitPreservingTypedFailures = async (context, effect) => {
1028
1071
  const exit = await Effect.runPromiseExitWith(context)(effect);
1029
1072
  if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) throw Cause.squash(exit.cause);
@@ -1194,6 +1237,12 @@ const makeInstanceClient = (definition, instance, stub) => {
1194
1237
  instance,
1195
1238
  operation
1196
1239
  } });
1240
+ const hostListOperation = (operation) => Effect.fn(`ContainerNamespace.${operation}`, spanOptions(operation))(function* (hosts) {
1241
+ return yield* tryContainerPromise(definition, instance, operation, () => stub[operation]([...hosts]));
1242
+ });
1243
+ const hostnameOperation = (operation) => Effect.fn(`ContainerNamespace.${operation}`, spanOptions(operation))(function* (hostname) {
1244
+ return yield* tryContainerPromise(definition, instance, operation, () => stub[operation](hostname));
1245
+ });
1197
1246
  return {
1198
1247
  rawUnsafe: Effect.succeed(stub),
1199
1248
  state: tryContainerPromise(definition, instance, "state", () => stub.getState()).pipe(Effect.flatMap(Schema.decodeUnknownEffect(ContainerState)), Effect.mapError((cause) => cause instanceof ContainerOperationError ? cause : new ContainerOperationError({
@@ -1211,10 +1260,19 @@ const makeInstanceClient = (definition, instance, stub) => {
1211
1260
  startAndWaitForPorts: Effect.fn("ContainerNamespace.startAndWaitForPorts", spanOptions("startAndWaitForPorts"))(function* (options) {
1212
1261
  return yield* tryContainerPromise(definition, instance, "startAndWaitForPorts", () => stub.startAndWaitForPorts(options));
1213
1262
  }),
1263
+ waitForPort: Effect.fn("ContainerNamespace.waitForPort", spanOptions("waitForPort"))(function* (options) {
1264
+ return yield* tryContainerPromise(definition, instance, "waitForPort", () => stub.waitForPort(options));
1265
+ }),
1214
1266
  stop: Effect.fn("ContainerNamespace.stop", spanOptions("stop"))(function* (signal) {
1215
1267
  return yield* tryContainerPromise(definition, instance, "stop", () => stub.stop(signal));
1216
1268
  }),
1217
- destroy: tryContainerPromise(definition, instance, "destroy", () => stub.destroy()).pipe(Effect.withSpan("ContainerNamespace.destroy", spanOptions("destroy")))
1269
+ destroy: tryContainerPromise(definition, instance, "destroy", () => stub.destroy()).pipe(Effect.withSpan("ContainerNamespace.destroy", spanOptions("destroy"))),
1270
+ setAllowedHosts: hostListOperation("setAllowedHosts"),
1271
+ setDeniedHosts: hostListOperation("setDeniedHosts"),
1272
+ allowHost: hostnameOperation("allowHost"),
1273
+ denyHost: hostnameOperation("denyHost"),
1274
+ removeAllowedHost: hostnameOperation("removeAllowedHost"),
1275
+ removeDeniedHost: hostnameOperation("removeDeniedHost")
1218
1276
  };
1219
1277
  };
1220
1278
  const isContainerNamespaceResource = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "getByName") === "function";
@@ -1236,8 +1294,15 @@ const makeClient$10 = (definition) => (namespace) => {
1236
1294
  fetch: (input, init) => Effect.flatMap(instance, (client) => client.fetch(input, init)),
1237
1295
  start: (startOptions, waitOptions) => Effect.flatMap(instance, (client) => client.start(startOptions, waitOptions)),
1238
1296
  startAndWaitForPorts: (startOptions) => Effect.flatMap(instance, (client) => client.startAndWaitForPorts(startOptions)),
1297
+ waitForPort: (waitOptions) => Effect.flatMap(instance, (client) => client.waitForPort(waitOptions)),
1239
1298
  stop: (signal) => Effect.flatMap(instance, (client) => client.stop(signal)),
1240
- destroy: Effect.flatMap(instance, (client) => client.destroy)
1299
+ destroy: Effect.flatMap(instance, (client) => client.destroy),
1300
+ setAllowedHosts: (hosts) => Effect.flatMap(instance, (client) => client.setAllowedHosts(hosts)),
1301
+ setDeniedHosts: (hosts) => Effect.flatMap(instance, (client) => client.setDeniedHosts(hosts)),
1302
+ allowHost: (hostname) => Effect.flatMap(instance, (client) => client.allowHost(hostname)),
1303
+ denyHost: (hostname) => Effect.flatMap(instance, (client) => client.denyHost(hostname)),
1304
+ removeAllowedHost: (hostname) => Effect.flatMap(instance, (client) => client.removeAllowedHost(hostname)),
1305
+ removeDeniedHost: (hostname) => Effect.flatMap(instance, (client) => client.removeDeniedHost(hostname))
1241
1306
  };
1242
1307
  };
1243
1308
  return {
@@ -1261,8 +1326,15 @@ const Tag$12 = () => (id) => {
1261
1326
  fetch: (input, init) => Effect.flatMap(getByName(name, options), (instance) => instance.fetch(input, init)),
1262
1327
  start: (startOptions, waitOptions) => Effect.flatMap(getByName(name, options), (instance) => instance.start(startOptions, waitOptions)),
1263
1328
  startAndWaitForPorts: (startOptions) => Effect.flatMap(getByName(name, options), (instance) => instance.startAndWaitForPorts(startOptions)),
1329
+ waitForPort: (waitOptions) => Effect.flatMap(getByName(name, options), (instance) => instance.waitForPort(waitOptions)),
1264
1330
  stop: (signal) => Effect.flatMap(getByName(name, options), (instance) => instance.stop(signal)),
1265
1331
  destroy: Effect.flatMap(getByName(name, options), (instance) => instance.destroy),
1332
+ setAllowedHosts: (hosts) => Effect.flatMap(getByName(name, options), (instance) => instance.setAllowedHosts(hosts)),
1333
+ setDeniedHosts: (hosts) => Effect.flatMap(getByName(name, options), (instance) => instance.setDeniedHosts(hosts)),
1334
+ allowHost: (hostname) => Effect.flatMap(getByName(name, options), (instance) => instance.allowHost(hostname)),
1335
+ denyHost: (hostname) => Effect.flatMap(getByName(name, options), (instance) => instance.denyHost(hostname)),
1336
+ removeAllowedHost: (hostname) => Effect.flatMap(getByName(name, options), (instance) => instance.removeAllowedHost(hostname)),
1337
+ removeDeniedHost: (hostname) => Effect.flatMap(getByName(name, options), (instance) => instance.removeDeniedHost(hostname)),
1266
1338
  rawUnsafe: Effect.flatMap(getByName(name, options), (instance) => instance.rawUnsafe)
1267
1339
  });
1268
1340
  const rawUnsafe = Effect.flatMap(tag, (namespace) => namespace.rawUnsafe);
@@ -1710,39 +1782,6 @@ const runEventPromise = (runtime, effect, eventLayer) => {
1710
1782
  return runtime.runPromise(Effect.scoped(withEventLayer));
1711
1783
  };
1712
1784
  //#endregion
1713
- //#region src/internal/WorkerContext.ts
1714
- const causeError = (cause) => {
1715
- const squashed = Cause.squash(cause);
1716
- return squashed instanceof Error ? squashed : new Error(Cause.pretty(cause), { cause: squashed });
1717
- };
1718
- const failureHandler = (cause, options) => (options?.onFailure?.(cause) ?? Effect.logError("WorkerContext.waitUntil failed", Cause.pretty(cause))).pipe(Effect.catchCause((handlerCause) => Effect.logError("WorkerContext.waitUntil failure handler failed", Cause.pretty(cause), Cause.pretty(handlerCause))));
1719
- /**
1720
- * Builds the `WorkerContext` service from a native `ExecutionContext`.
1721
- *
1722
- * Background effects capture the calling fiber's context via `Effect.context`
1723
- * before being handed to `ctx.waitUntil`, so the default module-level
1724
- * `Effect.runPromiseExit` runner is sufficient; entrypoints may still pass a
1725
- * runtime-bound runner.
1726
- */
1727
- const fromExecutionContext = (ctx, runPromiseExit = (effect) => Effect.runPromiseExit(effect)) => {
1728
- const schedule = (effect, options, mode) => Effect.context().pipe(Effect.flatMap((context) => Effect.sync(() => {
1729
- const runHandler = (cause) => runPromiseExit(Effect.scoped(Effect.provideContext(failureHandler(cause, options), context))).then((exit) => {
1730
- if (Exit.isFailure(exit)) console.error("WorkerContext.waitUntil failure handler failed", Cause.pretty(exit.cause));
1731
- });
1732
- ctx.waitUntil(runPromiseExit(Effect.scoped(Effect.provideContext(effect, context))).then(async (exit) => {
1733
- if (Exit.isSuccess(exit)) return;
1734
- await runHandler(exit.cause);
1735
- if (mode === "propagate") throw causeError(exit.cause);
1736
- }));
1737
- })));
1738
- return {
1739
- raw: ctx,
1740
- waitUntil: (effect, options) => schedule(effect, options, options?.mode ?? "observe"),
1741
- waitUntilPropagating: (effect, options) => schedule(effect, options, "propagate"),
1742
- passThroughOnException: Effect.sync(() => ctx.passThroughOnException())
1743
- };
1744
- };
1745
- //#endregion
1746
1785
  //#region src/Rpc.ts
1747
1786
  var Rpc_exports = /* @__PURE__ */ __exportAll({
1748
1787
  RpcStub: () => RpcStub,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -6,9 +6,12 @@ import * as ErrorMessage from "./internal/ErrorMessage";
6
6
 
7
7
  const expectedContainerNamespace = "Container namespace binding with getByName()";
8
8
 
9
- /** Signal accepted by Cloudflare Container instances. */
9
+ /** Named signal accepted by Cloudflare Container instances. */
10
10
  export type ContainerSignal = "SIGKILL" | "SIGINT" | "SIGTERM";
11
11
 
12
+ /** Named or numeric signal accepted by Container `stop()`. */
13
+ export type ContainerStopSignal = ContainerSignal | number;
14
+
12
15
  /** Per-instance Container startup configuration. */
13
16
  export interface ContainerStartOptions {
14
17
  readonly envVars?: Record<string, string>;
@@ -63,8 +66,15 @@ export interface ContainerStub {
63
66
  getState(): Promise<ContainerState>;
64
67
  start(options?: ContainerStartOptions, waitOptions?: ContainerWaitOptions): Promise<void>;
65
68
  startAndWaitForPorts(options?: ContainerStartAndWaitForPortsOptions): Promise<void>;
66
- stop(signal?: ContainerSignal): Promise<void>;
69
+ waitForPort(options: ContainerWaitOptions): Promise<number>;
70
+ stop(signal?: ContainerStopSignal): Promise<void>;
67
71
  destroy(): Promise<void>;
72
+ setAllowedHosts(hosts: Array<string>): Promise<void>;
73
+ setDeniedHosts(hosts: Array<string>): Promise<void>;
74
+ allowHost(hostname: string): Promise<void>;
75
+ denyHost(hostname: string): Promise<void>;
76
+ removeAllowedHost(hostname: string): Promise<void>;
77
+ removeDeniedHost(hostname: string): Promise<void>;
68
78
  }
69
79
 
70
80
  /** Native Container namespace binding shape. */
@@ -75,6 +85,11 @@ export interface ContainerNamespaceResource {
75
85
  ): ContainerStub;
76
86
  }
77
87
 
88
+ /** Exact native stub type produced by a Container namespace binding. */
89
+ export type ContainerStubOf<Namespace extends ContainerNamespaceResource> = ReturnType<
90
+ Namespace["getByName"]
91
+ >;
92
+
78
93
  /** Container namespace binding metadata. */
79
94
  export interface ContainerNamespaceDefinition {
80
95
  /** Binding name as configured in `wrangler.jsonc`. */
@@ -94,8 +109,8 @@ export class ContainerOperationError extends Data.TaggedError("ContainerOperatio
94
109
  }
95
110
 
96
111
  /** Effect-wrapped client for one named Container instance. */
97
- export interface ContainerInstanceClient {
98
- readonly rawUnsafe: Effect.Effect<ContainerStub, ContainerOperationError>;
112
+ export interface ContainerInstanceClient<Stub extends ContainerStub = ContainerStub> {
113
+ readonly rawUnsafe: Effect.Effect<Stub, ContainerOperationError>;
99
114
  readonly state: Effect.Effect<ContainerState, ContainerOperationError>;
100
115
  readonly fetch: (
101
116
  input: RequestInfo | URL,
@@ -108,29 +123,51 @@ export interface ContainerInstanceClient {
108
123
  readonly startAndWaitForPorts: (
109
124
  options?: ContainerStartAndWaitForPortsOptions,
110
125
  ) => Effect.Effect<void, ContainerOperationError>;
111
- readonly stop: (signal?: ContainerSignal) => Effect.Effect<void, ContainerOperationError>;
126
+ readonly waitForPort: (
127
+ options: ContainerWaitOptions,
128
+ ) => Effect.Effect<number, ContainerOperationError>;
129
+ readonly stop: (signal?: ContainerStopSignal) => Effect.Effect<void, ContainerOperationError>;
112
130
  readonly destroy: Effect.Effect<void, ContainerOperationError>;
131
+ readonly setAllowedHosts: (
132
+ hosts: ReadonlyArray<string>,
133
+ ) => Effect.Effect<void, ContainerOperationError>;
134
+ readonly setDeniedHosts: (
135
+ hosts: ReadonlyArray<string>,
136
+ ) => Effect.Effect<void, ContainerOperationError>;
137
+ readonly allowHost: (hostname: string) => Effect.Effect<void, ContainerOperationError>;
138
+ readonly denyHost: (hostname: string) => Effect.Effect<void, ContainerOperationError>;
139
+ readonly removeAllowedHost: (hostname: string) => Effect.Effect<void, ContainerOperationError>;
140
+ readonly removeDeniedHost: (hostname: string) => Effect.Effect<void, ContainerOperationError>;
113
141
  }
114
142
 
115
143
  /** Effect client for a Container namespace binding. */
116
- export interface ContainerNamespaceClient {
144
+ export interface ContainerNamespaceClient<
145
+ Namespace extends ContainerNamespaceResource = ContainerNamespaceResource,
146
+ > {
117
147
  readonly definition: ContainerNamespaceDefinition;
118
148
  readonly getByName: (
119
149
  name: string,
120
150
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
121
- ) => Effect.Effect<ContainerInstanceClient, ContainerOperationError>;
151
+ ) => Effect.Effect<ContainerInstanceClient<ContainerStubOf<Namespace>>, ContainerOperationError>;
122
152
  readonly byName: (
123
153
  name: string,
124
154
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
125
- ) => ContainerInstanceClient;
126
- readonly rawUnsafe: Effect.Effect<ContainerNamespaceResource>;
155
+ ) => ContainerInstanceClient<ContainerStubOf<Namespace>>;
156
+ readonly rawUnsafe: Effect.Effect<Namespace>;
127
157
  }
128
158
 
129
- export type ContainerNamespaceStaticClient<R> = {
159
+ export type ContainerNamespaceStaticClient<
160
+ R,
161
+ Namespace extends ContainerNamespaceResource = ContainerNamespaceResource,
162
+ > = {
130
163
  readonly getByName: (
131
164
  name: string,
132
165
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
133
- ) => Effect.Effect<ContainerInstanceClient, ContainerOperationError, R>;
166
+ ) => Effect.Effect<
167
+ ContainerInstanceClient<ContainerStubOf<Namespace>>,
168
+ ContainerOperationError,
169
+ R
170
+ >;
134
171
  readonly byName: (
135
172
  name: string,
136
173
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
@@ -147,21 +184,44 @@ export type ContainerNamespaceStaticClient<R> = {
147
184
  readonly startAndWaitForPorts: (
148
185
  options?: ContainerStartAndWaitForPortsOptions,
149
186
  ) => Effect.Effect<void, ContainerOperationError, R>;
150
- readonly stop: (signal?: ContainerSignal) => Effect.Effect<void, ContainerOperationError, R>;
187
+ readonly waitForPort: (
188
+ options: ContainerWaitOptions,
189
+ ) => Effect.Effect<number, ContainerOperationError, R>;
190
+ readonly stop: (
191
+ signal?: ContainerStopSignal,
192
+ ) => Effect.Effect<void, ContainerOperationError, R>;
151
193
  readonly destroy: Effect.Effect<void, ContainerOperationError, R>;
152
- readonly rawUnsafe: Effect.Effect<ContainerStub, ContainerOperationError, R>;
194
+ readonly setAllowedHosts: (
195
+ hosts: ReadonlyArray<string>,
196
+ ) => Effect.Effect<void, ContainerOperationError, R>;
197
+ readonly setDeniedHosts: (
198
+ hosts: ReadonlyArray<string>,
199
+ ) => Effect.Effect<void, ContainerOperationError, R>;
200
+ readonly allowHost: (hostname: string) => Effect.Effect<void, ContainerOperationError, R>;
201
+ readonly denyHost: (hostname: string) => Effect.Effect<void, ContainerOperationError, R>;
202
+ readonly removeAllowedHost: (
203
+ hostname: string,
204
+ ) => Effect.Effect<void, ContainerOperationError, R>;
205
+ readonly removeDeniedHost: (
206
+ hostname: string,
207
+ ) => Effect.Effect<void, ContainerOperationError, R>;
208
+ readonly rawUnsafe: Effect.Effect<ContainerStubOf<Namespace>, ContainerOperationError, R>;
153
209
  };
154
- readonly rawUnsafe: Effect.Effect<ContainerNamespaceResource, never, R>;
210
+ readonly rawUnsafe: Effect.Effect<Namespace, never, R>;
155
211
  };
156
212
 
157
213
  export type LayerOptions = {
158
214
  readonly binding: string;
159
215
  };
160
216
 
161
- export interface TagClass<Self, Id extends string>
217
+ export interface TagClass<
218
+ Self,
219
+ Id extends string,
220
+ Namespace extends ContainerNamespaceResource = ContainerNamespaceResource,
221
+ >
162
222
  extends
163
- Context.ServiceClass<Self, `effect-cf/Container/${Id}`, ContainerNamespaceClient>,
164
- ContainerNamespaceStaticClient<Self> {
223
+ Context.ServiceClass<Self, `effect-cf/Container/${Id}`, ContainerNamespaceClient<Namespace>>,
224
+ ContainerNamespaceStaticClient<Self, Namespace> {
165
225
  readonly id: Id;
166
226
  readonly layer: (
167
227
  options: LayerOptions,
@@ -189,14 +249,34 @@ const tryContainerPromise = <A>(
189
249
  }),
190
250
  });
191
251
 
192
- const makeInstanceClient = (
252
+ const makeInstanceClient = <Stub extends ContainerStub>(
193
253
  definition: ContainerNamespaceDefinition,
194
254
  instance: string,
195
- stub: ContainerStub,
196
- ): ContainerInstanceClient => {
255
+ stub: Stub,
256
+ ): ContainerInstanceClient<Stub> => {
197
257
  const spanOptions = (operation: string) => ({
198
258
  attributes: { binding: definition.binding, instance, operation },
199
259
  });
260
+ const hostListOperation = (operation: "setAllowedHosts" | "setDeniedHosts") =>
261
+ Effect.fn(
262
+ `ContainerNamespace.${operation}`,
263
+ spanOptions(operation),
264
+ )(function* (hosts: ReadonlyArray<string>) {
265
+ return yield* tryContainerPromise(definition, instance, operation, () =>
266
+ stub[operation]([...hosts]),
267
+ );
268
+ });
269
+ const hostnameOperation = (
270
+ operation: "allowHost" | "denyHost" | "removeAllowedHost" | "removeDeniedHost",
271
+ ) =>
272
+ Effect.fn(
273
+ `ContainerNamespace.${operation}`,
274
+ spanOptions(operation),
275
+ )(function* (hostname: string) {
276
+ return yield* tryContainerPromise(definition, instance, operation, () =>
277
+ stub[operation](hostname),
278
+ );
279
+ });
200
280
 
201
281
  return {
202
282
  rawUnsafe: Effect.succeed(stub),
@@ -238,15 +318,29 @@ const makeInstanceClient = (
238
318
  stub.startAndWaitForPorts(options),
239
319
  );
240
320
  }),
321
+ waitForPort: Effect.fn(
322
+ "ContainerNamespace.waitForPort",
323
+ spanOptions("waitForPort"),
324
+ )(function* (options: ContainerWaitOptions) {
325
+ return yield* tryContainerPromise(definition, instance, "waitForPort", () =>
326
+ stub.waitForPort(options),
327
+ );
328
+ }),
241
329
  stop: Effect.fn(
242
330
  "ContainerNamespace.stop",
243
331
  spanOptions("stop"),
244
- )(function* (signal?: ContainerSignal) {
332
+ )(function* (signal?: ContainerStopSignal) {
245
333
  return yield* tryContainerPromise(definition, instance, "stop", () => stub.stop(signal));
246
334
  }),
247
335
  destroy: tryContainerPromise(definition, instance, "destroy", () => stub.destroy()).pipe(
248
336
  Effect.withSpan("ContainerNamespace.destroy", spanOptions("destroy")),
249
337
  ),
338
+ setAllowedHosts: hostListOperation("setAllowedHosts"),
339
+ setDeniedHosts: hostListOperation("setDeniedHosts"),
340
+ allowHost: hostnameOperation("allowHost"),
341
+ denyHost: hostnameOperation("denyHost"),
342
+ removeAllowedHost: hostnameOperation("removeAllowedHost"),
343
+ removeDeniedHost: hostnameOperation("removeDeniedHost"),
250
344
  };
251
345
  };
252
346
 
@@ -257,13 +351,22 @@ export const isContainerNamespaceResource = (value: unknown): value is Container
257
351
 
258
352
  export const makeClient =
259
353
  (definition: ContainerNamespaceDefinition) =>
260
- (namespace: ContainerNamespaceResource): ContainerNamespaceClient => {
354
+ <Namespace extends ContainerNamespaceResource>(
355
+ namespace: Namespace,
356
+ ): ContainerNamespaceClient<Namespace> => {
261
357
  const getByName = (
262
358
  name: string,
263
359
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
264
360
  ) =>
265
361
  Effect.try({
266
- try: () => makeInstanceClient(definition, name, namespace.getByName(name, options)),
362
+ try: () =>
363
+ makeInstanceClient(
364
+ definition,
365
+ name,
366
+ // getByName has a single call signature, so its return type is
367
+ // exactly the namespace's stub type.
368
+ namespace.getByName(name, options) as ContainerStubOf<Namespace>,
369
+ ),
267
370
  catch: (cause) =>
268
371
  new ContainerOperationError({
269
372
  binding: definition.binding,
@@ -276,7 +379,7 @@ export const makeClient =
276
379
  const byName = (
277
380
  name: string,
278
381
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
279
- ): ContainerInstanceClient => {
382
+ ): ContainerInstanceClient<ContainerStubOf<Namespace>> => {
280
383
  const instance = getByName(name, options);
281
384
 
282
385
  return {
@@ -287,8 +390,20 @@ export const makeClient =
287
390
  Effect.flatMap(instance, (client) => client.start(startOptions, waitOptions)),
288
391
  startAndWaitForPorts: (startOptions) =>
289
392
  Effect.flatMap(instance, (client) => client.startAndWaitForPorts(startOptions)),
393
+ waitForPort: (waitOptions) =>
394
+ Effect.flatMap(instance, (client) => client.waitForPort(waitOptions)),
290
395
  stop: (signal) => Effect.flatMap(instance, (client) => client.stop(signal)),
291
396
  destroy: Effect.flatMap(instance, (client) => client.destroy),
397
+ setAllowedHosts: (hosts) =>
398
+ Effect.flatMap(instance, (client) => client.setAllowedHosts(hosts)),
399
+ setDeniedHosts: (hosts) =>
400
+ Effect.flatMap(instance, (client) => client.setDeniedHosts(hosts)),
401
+ allowHost: (hostname) => Effect.flatMap(instance, (client) => client.allowHost(hostname)),
402
+ denyHost: (hostname) => Effect.flatMap(instance, (client) => client.denyHost(hostname)),
403
+ removeAllowedHost: (hostname) =>
404
+ Effect.flatMap(instance, (client) => client.removeAllowedHost(hostname)),
405
+ removeDeniedHost: (hostname) =>
406
+ Effect.flatMap(instance, (client) => client.removeDeniedHost(hostname)),
292
407
  };
293
408
  };
294
409
 
@@ -300,13 +415,21 @@ export const makeClient =
300
415
  };
301
416
  };
302
417
 
303
- export const layer = <Self>(
304
- tag: Context.Service<Self, ContainerNamespaceClient>,
418
+ export const layer = <Self, Namespace extends ContainerNamespaceResource>(
419
+ tag: Context.Service<Self, ContainerNamespaceClient<Namespace>>,
305
420
  definition: ContainerNamespaceDefinition,
306
421
  ) =>
307
- Binding.layer(tag, definition.binding, isContainerNamespaceResource, makeClient(definition), {
308
- expected: expectedContainerNamespace,
309
- });
422
+ Binding.layer(
423
+ tag,
424
+ definition.binding,
425
+ // Runtime validation stays structural; the exact namespace type is
426
+ // asserted by the consumer's binding declaration, like env typing.
427
+ isContainerNamespaceResource as (value: unknown) => value is Namespace,
428
+ makeClient(definition),
429
+ {
430
+ expected: expectedContainerNamespace,
431
+ },
432
+ );
310
433
 
311
434
  export const make = <Id extends string>(id: Id) => Tag<ContainerNamespaceService<Id>>()<Id>(id);
312
435
 
@@ -321,9 +444,9 @@ export interface ContainerNamespaceService<Id extends string> {
321
444
 
322
445
  /** Creates a typed Effect service for a Cloudflare Container namespace. */
323
446
  export const Tag =
324
- <Self>() =>
447
+ <Self, Namespace extends ContainerNamespaceResource = ContainerNamespaceResource>() =>
325
448
  <Id extends string>(id: Id) => {
326
- const tag = Context.Service<Self, ContainerNamespaceClient>()(
449
+ const tag = Context.Service<Self, ContainerNamespaceClient<Namespace>>()(
327
450
  `effect-cf/Container/${id}` as const,
328
451
  );
329
452
  const makeLayer = (definition: LayerOptions) => layer(tag, definition);
@@ -352,9 +475,25 @@ export const Tag =
352
475
  Effect.flatMap(getByName(name, options), (instance) =>
353
476
  instance.startAndWaitForPorts(startOptions),
354
477
  ),
355
- stop: (signal?: ContainerSignal) =>
478
+ waitForPort: (waitOptions: ContainerWaitOptions) =>
479
+ Effect.flatMap(getByName(name, options), (instance) => instance.waitForPort(waitOptions)),
480
+ stop: (signal?: ContainerStopSignal) =>
356
481
  Effect.flatMap(getByName(name, options), (instance) => instance.stop(signal)),
357
482
  destroy: Effect.flatMap(getByName(name, options), (instance) => instance.destroy),
483
+ setAllowedHosts: (hosts: ReadonlyArray<string>) =>
484
+ Effect.flatMap(getByName(name, options), (instance) => instance.setAllowedHosts(hosts)),
485
+ setDeniedHosts: (hosts: ReadonlyArray<string>) =>
486
+ Effect.flatMap(getByName(name, options), (instance) => instance.setDeniedHosts(hosts)),
487
+ allowHost: (hostname: string) =>
488
+ Effect.flatMap(getByName(name, options), (instance) => instance.allowHost(hostname)),
489
+ denyHost: (hostname: string) =>
490
+ Effect.flatMap(getByName(name, options), (instance) => instance.denyHost(hostname)),
491
+ removeAllowedHost: (hostname: string) =>
492
+ Effect.flatMap(getByName(name, options), (instance) =>
493
+ instance.removeAllowedHost(hostname),
494
+ ),
495
+ removeDeniedHost: (hostname: string) =>
496
+ Effect.flatMap(getByName(name, options), (instance) => instance.removeDeniedHost(hostname)),
358
497
  rawUnsafe: Effect.flatMap(getByName(name, options), (instance) => instance.rawUnsafe),
359
498
  });
360
499
 
@@ -366,7 +505,7 @@ export const Tag =
366
505
  getByName,
367
506
  byName,
368
507
  rawUnsafe,
369
- }) as TagClass<Self, Id>;
508
+ }) as TagClass<Self, Id, Namespace>;
370
509
  };
371
510
 
372
511
  export const ContainerNamespace = Tag;