effect-cf 0.34.0 → 0.36.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 +16 -0
- package/dist/ComputerWorkspace.d.mts +11 -3
- package/dist/ComputerWorkspace.mjs +31 -9
- package/dist/DurableObjectState-CpgAmFqo.d.mts +244 -0
- package/dist/{DurableObjectState-De4ZojhY.mjs → DurableObjectState-DCh4PZMN.mjs} +50 -76
- package/dist/Mcp.d.mts +122 -0
- package/dist/Mcp.mjs +270 -0
- package/dist/Vitest.d.mts +2 -2
- package/dist/Vitest.mjs +8 -10
- package/dist/{DurableObjectState-CrEunfQ-.d.mts → Worker-Dl6_Pijl.d.mts} +35 -269
- package/dist/Worker-XsGJsCJX.mjs +885 -0
- package/dist/WorkerContext-Yobnfw-K.mjs +60 -0
- package/dist/{WorkflowBinding-BumVQTuW.d.mts → WorkflowBinding-CvJT9hB5.d.mts} +1 -1
- package/dist/index.d.mts +13 -4
- package/dist/index.mjs +151 -1002
- package/package.json +12 -1
- package/src/AnalyticsEngine.ts +8 -1
- package/src/CloudflareOtlp.ts +6 -3
- package/src/ComputerWorkspace.ts +53 -26
- package/src/DurableObject.ts +8 -9
- package/src/DurableObjectNamespace.ts +33 -5
- package/src/DurableObjectRpcWebSocket.ts +2 -27
- package/src/DurableObjectState.ts +18 -20
- package/src/DurableObjectStorage.ts +53 -35
- package/src/DurableObjectWebSocket.ts +23 -41
- package/src/Mcp.ts +531 -0
- package/src/RpcDefinition.ts +5 -15
- package/src/ServiceBinding.ts +37 -5
- package/src/Vitest.ts +18 -15
- package/src/Worker.ts +107 -54
- package/src/WorkersAi.ts +21 -14
- package/src/Workflow.ts +45 -49
- package/src/internal/NativeCallback.ts +55 -0
- package/src/internal/WorkerContext.ts +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-cf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Effect-native primitives for Cloudflare Workers and bindings.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"import": "./dist/HyperdrivePg.mjs",
|
|
28
28
|
"default": "./dist/HyperdrivePg.mjs"
|
|
29
29
|
},
|
|
30
|
+
"./mcp": {
|
|
31
|
+
"types": "./dist/Mcp.d.mts",
|
|
32
|
+
"import": "./dist/Mcp.mjs",
|
|
33
|
+
"default": "./dist/Mcp.mjs"
|
|
34
|
+
},
|
|
30
35
|
"./computer-artifacts": {
|
|
31
36
|
"types": "./dist/ComputerArtifacts.d.mts",
|
|
32
37
|
"import": "./dist/ComputerArtifacts.mjs",
|
|
@@ -75,6 +80,8 @@
|
|
|
75
80
|
"@effect/sql-pg": "catalog:",
|
|
76
81
|
"@effect/sql-sqlite-do": "catalog:",
|
|
77
82
|
"@effect/vitest": "catalog:",
|
|
83
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
84
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
78
85
|
"@platformatic/vfs": "^0.4.0",
|
|
79
86
|
"effect": "catalog:",
|
|
80
87
|
"esbuild": "^0.28.1",
|
|
@@ -89,6 +96,7 @@
|
|
|
89
96
|
"@effect/sql-d1": "^4.0.0-rc.110",
|
|
90
97
|
"@effect/sql-pg": "^4.0.0-rc.110",
|
|
91
98
|
"@effect/sql-sqlite-do": "^4.0.0-rc.110",
|
|
99
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
92
100
|
"effect": "^4.0.0-rc.110"
|
|
93
101
|
},
|
|
94
102
|
"peerDependenciesMeta": {
|
|
@@ -106,6 +114,9 @@
|
|
|
106
114
|
},
|
|
107
115
|
"@effect/sql-pg": {
|
|
108
116
|
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"@modelcontextprotocol/server": {
|
|
119
|
+
"optional": true
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
}
|
package/src/AnalyticsEngine.ts
CHANGED
|
@@ -572,7 +572,14 @@ const executeQueryRequest = (
|
|
|
572
572
|
options?: AnalyticsEngineQueryOptions,
|
|
573
573
|
) =>
|
|
574
574
|
Effect.gen(function* () {
|
|
575
|
-
const
|
|
575
|
+
const request = yield* Effect.try({
|
|
576
|
+
try: () => queryRequest(definition, sql, options),
|
|
577
|
+
catch: (cause) =>
|
|
578
|
+
analyticsEngineQueryError(definition, "query", "Invalid Analytics Engine SQL API URL", {
|
|
579
|
+
cause,
|
|
580
|
+
}),
|
|
581
|
+
});
|
|
582
|
+
const response = yield* httpClient.execute(request).pipe(
|
|
576
583
|
Effect.mapError((cause) =>
|
|
577
584
|
analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", {
|
|
578
585
|
cause,
|
package/src/CloudflareOtlp.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Worker fetch/RPC and Durable Object alarm/RPC handlers schedule a
|
|
3
|
-
* flusher. It settles within two seconds;
|
|
4
|
-
* flush explicitly.
|
|
2
|
+
* Worker fetch/queue/RPC and Durable Object alarm/RPC handlers schedule a
|
|
3
|
+
* best-effort flusher. It settles within two seconds; other lifecycles must
|
|
4
|
+
* flush explicitly. Scheduled flush failures cannot fail user events. Exporter
|
|
5
|
+
* layer finalizers run separately and follow their configured shutdown timeout.
|
|
5
6
|
*/
|
|
6
7
|
import { ConfigProvider, Effect, Layer, Option } from "effect";
|
|
7
8
|
import type * as Tracer from "effect/Tracer";
|
|
@@ -207,6 +208,8 @@ export const layerProtobuf = (
|
|
|
207
208
|
*
|
|
208
209
|
* Provide this layer at runtime scope for long-lived metrics aggregation, or at
|
|
209
210
|
* handler scope when traces/logs should flush as the Cloudflare event finishes.
|
|
211
|
+
* A handler-scoped exporter can delay scope closure while its own finalizer
|
|
212
|
+
* flushes; the two-second background flush limit does not bound layer shutdown.
|
|
210
213
|
*/
|
|
211
214
|
export const layerWorker = (options: WorkerLayerOptions = {}): Layer.Layer<OtlpExporter.Flusher> =>
|
|
212
215
|
makeLayer(options, {
|
package/src/ComputerWorkspace.ts
CHANGED
|
@@ -100,6 +100,17 @@ export class WorkspaceAssetsError extends Schema.TaggedError<WorkspaceAssetsErro
|
|
|
100
100
|
},
|
|
101
101
|
) {}
|
|
102
102
|
|
|
103
|
+
/** Schema-serializable error while acquiring the Computer workspace client. */
|
|
104
|
+
export class WorkspaceAcquireError extends Schema.TaggedError<WorkspaceAcquireError>()(
|
|
105
|
+
"WorkspaceAcquireError",
|
|
106
|
+
{
|
|
107
|
+
operation: Schema.String,
|
|
108
|
+
message: Schema.String,
|
|
109
|
+
code: Schema.optionalKey(Schema.String),
|
|
110
|
+
cause: Schema.optionalKey(Schema.Defect({ includeStack: true })),
|
|
111
|
+
},
|
|
112
|
+
) {}
|
|
113
|
+
|
|
103
114
|
export type WorkspaceReadFileOptions = Omit<CloudflareReadFileOptions, "encoding">;
|
|
104
115
|
export type ReadTextFileOptions = WorkspaceReadFileOptions;
|
|
105
116
|
export type ListDirectoryOptions = CloudflareReaddirOptions;
|
|
@@ -469,6 +480,21 @@ const execError = (operation: string, cause: unknown) => {
|
|
|
469
480
|
return WorkspaceExecError.make(properties);
|
|
470
481
|
};
|
|
471
482
|
|
|
483
|
+
const acquireError = (operation: string, cause: unknown) => {
|
|
484
|
+
const properties: WorkspaceOperationErrorProperties = {
|
|
485
|
+
operation,
|
|
486
|
+
message: errorMessage(cause),
|
|
487
|
+
cause,
|
|
488
|
+
};
|
|
489
|
+
const code = errorCode(cause);
|
|
490
|
+
|
|
491
|
+
if (code !== undefined) {
|
|
492
|
+
properties.code = code;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return WorkspaceAcquireError.make(properties);
|
|
496
|
+
};
|
|
497
|
+
|
|
472
498
|
const assetsError = (operation: string, path: string, cause: unknown) => {
|
|
473
499
|
const properties: WorkspaceFsErrorProperties = {
|
|
474
500
|
operation,
|
|
@@ -887,9 +913,6 @@ const wrapRun = <Encoding extends WorkspaceExecEncoding>(
|
|
|
887
913
|
return { id: handle.id, backend: handle.backend, events, result, kill };
|
|
888
914
|
};
|
|
889
915
|
|
|
890
|
-
const releaseRun = (handle: CloudflareWorkspaceRuntimeExecHandle<"utf8" | undefined>) =>
|
|
891
|
-
Effect.sync(() => handle[Symbol.dispose]());
|
|
892
|
-
|
|
893
916
|
const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntime => {
|
|
894
917
|
const execImplementation = Effect.fn("ComputerWorkspace.runtime.exec", {
|
|
895
918
|
attributes: { family: "runtime", operation: "exec" },
|
|
@@ -905,7 +928,7 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
|
|
|
905
928
|
: undefined;
|
|
906
929
|
|
|
907
930
|
yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: options?.id });
|
|
908
|
-
const handle = yield* Effect.
|
|
931
|
+
const handle = yield* Effect.acquireDisposable(
|
|
909
932
|
Effect.tryPromise({
|
|
910
933
|
try: () => {
|
|
911
934
|
if (Predicate.isString(source)) {
|
|
@@ -926,7 +949,6 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
|
|
|
926
949
|
},
|
|
927
950
|
catch: (cause) => execError("exec", cause),
|
|
928
951
|
}),
|
|
929
|
-
releaseRun,
|
|
930
952
|
);
|
|
931
953
|
|
|
932
954
|
// SAFETY: runtimeExecOptions carries Encoding to Cloudflare's corresponding handle overload.
|
|
@@ -945,12 +967,11 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
|
|
|
945
967
|
options?: WorkspaceGetExecOptions<Encoding>,
|
|
946
968
|
) {
|
|
947
969
|
yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: id });
|
|
948
|
-
const handle = yield* Effect.
|
|
970
|
+
const handle = yield* Effect.acquireDisposable(
|
|
949
971
|
Effect.tryPromise({
|
|
950
972
|
try: () => client.runtime.getExec(id, runtimeGetOptions(options)),
|
|
951
973
|
catch: (cause) => execError("getExec", cause),
|
|
952
974
|
}),
|
|
953
|
-
releaseRun,
|
|
954
975
|
);
|
|
955
976
|
|
|
956
977
|
// SAFETY: runtimeGetOptions carries Encoding to Cloudflare's corresponding handle overload.
|
|
@@ -1169,25 +1190,31 @@ export class ComputerWorkspace extends Context.Service<
|
|
|
1169
1190
|
* Acquires the single workspace registered by `withComputerWorkspace` and
|
|
1170
1191
|
* releases its RPC client when the layer scope closes.
|
|
1171
1192
|
*/
|
|
1172
|
-
static readonly layer: Layer.Layer<ComputerWorkspace,
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1193
|
+
static readonly layer: Layer.Layer<ComputerWorkspace, WorkspaceAcquireError, DurableObjectState> =
|
|
1194
|
+
Layer.effect(
|
|
1195
|
+
ComputerWorkspace,
|
|
1196
|
+
Effect.gen(function* () {
|
|
1197
|
+
const state = yield* DurableObjectState;
|
|
1198
|
+
const host = HostRegistry.lookup(state.raw);
|
|
1199
|
+
|
|
1200
|
+
if (host === undefined) {
|
|
1201
|
+
return yield* Effect.die(
|
|
1202
|
+
"ComputerWorkspace.layer requires a Durable Object class built with withComputerWorkspace",
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const computer = yield* Effect.tryPromise({
|
|
1207
|
+
try: () => import("@cloudflare/computer"),
|
|
1208
|
+
catch: (cause) => acquireError("import", cause),
|
|
1209
|
+
});
|
|
1210
|
+
const client = yield* Effect.acquireDisposable(
|
|
1211
|
+
Effect.tryPromise({
|
|
1212
|
+
try: () => computer.getWorkspace(host),
|
|
1213
|
+
catch: (cause) => acquireError("getWorkspace", cause),
|
|
1214
|
+
}),
|
|
1181
1215
|
);
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
const computer = yield* Effect.promise(() => import("@cloudflare/computer"));
|
|
1185
|
-
const client = yield* Effect.acquireRelease(
|
|
1186
|
-
Effect.promise(() => computer.getWorkspace(host)),
|
|
1187
|
-
(workspace) => Effect.sync(() => workspace[Symbol.dispose]()),
|
|
1188
|
-
);
|
|
1189
1216
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1217
|
+
return ComputerWorkspace.of(fromWorkspaceClient(client));
|
|
1218
|
+
}),
|
|
1219
|
+
);
|
|
1193
1220
|
}
|
package/src/DurableObject.ts
CHANGED
|
@@ -5,20 +5,13 @@ import { NativeRequest } from "./Worker";
|
|
|
5
5
|
import { WorkerEnvironment, type WorkerEnv } from "./Environment";
|
|
6
6
|
import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
|
|
7
7
|
import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
|
|
8
|
+
import * as RpcDefinition from "./RpcDefinition";
|
|
8
9
|
import * as Entrypoint from "./internal/Entrypoint";
|
|
9
10
|
import * as Runtime from "./internal/Runtime";
|
|
10
11
|
import * as Telemetry from "./internal/Telemetry";
|
|
11
12
|
import type { ReceiverOptions, RpcInvocationInfo } from "./RpcTracing";
|
|
12
13
|
|
|
13
|
-
const reservedMethodNames
|
|
14
|
-
"constructor",
|
|
15
|
-
"dup",
|
|
16
|
-
"fetch",
|
|
17
|
-
"alarm",
|
|
18
|
-
"webSocketMessage",
|
|
19
|
-
"webSocketClose",
|
|
20
|
-
"webSocketError",
|
|
21
|
-
]);
|
|
14
|
+
const reservedMethodNames: ReadonlySet<string> = RpcDefinition.reservedMethodNames;
|
|
22
15
|
|
|
23
16
|
type RuntimeContext<ROut> = DurableObjectState | WorkerEnvironment | ROut;
|
|
24
17
|
|
|
@@ -167,6 +160,12 @@ export type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (
|
|
|
167
160
|
): Promise<A>;
|
|
168
161
|
};
|
|
169
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Builds a Durable Object whose base `layer` lives for the in-memory instance.
|
|
165
|
+
* Cloudflare does not expose an eviction or shutdown hook, so finalizers in
|
|
166
|
+
* that layer are not guaranteed to run. Put resources that require timely
|
|
167
|
+
* release in `eventLayer`, whose scope closes after each handled event.
|
|
168
|
+
*/
|
|
170
169
|
export const make = <
|
|
171
170
|
ROut,
|
|
172
171
|
LayerError,
|
|
@@ -250,7 +250,7 @@ export type DurableObjectNamespaceEffectClient<
|
|
|
250
250
|
stub: DurableObjectStubClient<Api>,
|
|
251
251
|
method: Method,
|
|
252
252
|
...args: StubMethodArgs<Api, Method>
|
|
253
|
-
) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>,
|
|
253
|
+
) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, DurableObjectRpcError, Scope.Scope>;
|
|
254
254
|
/**
|
|
255
255
|
* Exposes the underlying native Durable Object namespace binding.
|
|
256
256
|
*
|
|
@@ -300,7 +300,11 @@ export type DurableObjectNamespaceStaticClient<
|
|
|
300
300
|
stub: DurableObjectStubClient<Api>,
|
|
301
301
|
method: Method,
|
|
302
302
|
...args: StubMethodArgs<Api, Method>
|
|
303
|
-
) => Effect.Effect<
|
|
303
|
+
) => Effect.Effect<
|
|
304
|
+
Awaited<StubMethodSuccess<Api, Method>>,
|
|
305
|
+
DurableObjectRpcError,
|
|
306
|
+
Scope.Scope | R
|
|
307
|
+
>;
|
|
304
308
|
readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
|
|
305
309
|
};
|
|
306
310
|
|
|
@@ -351,7 +355,22 @@ export const makeClient = <
|
|
|
351
355
|
|
|
352
356
|
const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
|
|
353
357
|
Effect.tryPromise({
|
|
354
|
-
try: () =>
|
|
358
|
+
try: (signal) => {
|
|
359
|
+
const callerSignal =
|
|
360
|
+
init?.signal !== undefined
|
|
361
|
+
? init.signal
|
|
362
|
+
: input instanceof Request
|
|
363
|
+
? input.signal
|
|
364
|
+
: undefined;
|
|
365
|
+
|
|
366
|
+
return stub.fetch(input, {
|
|
367
|
+
...init,
|
|
368
|
+
signal:
|
|
369
|
+
callerSignal === null || callerSignal === undefined
|
|
370
|
+
? signal
|
|
371
|
+
: AbortSignal.any([callerSignal, signal]),
|
|
372
|
+
});
|
|
373
|
+
},
|
|
355
374
|
catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
|
|
356
375
|
});
|
|
357
376
|
|
|
@@ -456,10 +475,19 @@ export const makeClient = <
|
|
|
456
475
|
stub: StubClient,
|
|
457
476
|
method: Method,
|
|
458
477
|
...args: StubMethodArgs<Api, Method>
|
|
459
|
-
): Effect.fn.Return<StubMethodSuccess<Api, Method>,
|
|
478
|
+
): Effect.fn.Return<StubMethodSuccess<Api, Method>, DurableObjectRpcError, Scope.Scope> {
|
|
460
479
|
const methodName = String(method);
|
|
461
480
|
const result = yield* rpc(stub, method, ...args);
|
|
462
|
-
const value = yield* CloudflareRpc.scoped(result)
|
|
481
|
+
const value = yield* CloudflareRpc.scoped(result).pipe(
|
|
482
|
+
Effect.mapError(
|
|
483
|
+
(cause) =>
|
|
484
|
+
new DurableObjectRpcError({
|
|
485
|
+
binding: definition.binding,
|
|
486
|
+
method: methodName,
|
|
487
|
+
cause,
|
|
488
|
+
}),
|
|
489
|
+
),
|
|
490
|
+
);
|
|
463
491
|
|
|
464
492
|
return yield* decodeSuccess<Method>(methodName, value);
|
|
465
493
|
},
|
|
@@ -230,7 +230,6 @@ export const layer = (
|
|
|
230
230
|
const disconnects = yield* Queue.make<number>();
|
|
231
231
|
const connectionsBySocket = new Map<WebSocket, RpcConnection>();
|
|
232
232
|
const connectionsById = new Map<number, RpcConnection>();
|
|
233
|
-
const clientIds = new Set<number>();
|
|
234
233
|
const resetSockets = new WeakSet<WebSocket>();
|
|
235
234
|
const declarationsById = new Map<string, AnyResumableStreamDeclaration>();
|
|
236
235
|
const declarationsByTag = new Map<string, AnyResumableStreamDeclaration>();
|
|
@@ -268,21 +267,6 @@ export const layer = (
|
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
const reserveClientId = (socket: DurableWebSocket): AttachmentMetadata => {
|
|
271
|
-
const attachment = readAttachment(socket.raw, attachmentKey);
|
|
272
|
-
|
|
273
|
-
if (attachment._tag === "Invalid") {
|
|
274
|
-
throw new Error("Invalid Durable Object RPC websocket attachment metadata");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (attachment._tag === "Valid") {
|
|
278
|
-
const metadata = attachment.metadata;
|
|
279
|
-
|
|
280
|
-
nextClientId = Math.max(nextClientId, metadata.clientId + 1);
|
|
281
|
-
writeAttachment(socket.raw, attachmentKey, metadata);
|
|
282
|
-
|
|
283
|
-
return metadata;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
270
|
const id = nextClientId++;
|
|
287
271
|
const created: LegacyAttachmentMetadata = {
|
|
288
272
|
version: legacyAttachmentVersion,
|
|
@@ -377,7 +361,6 @@ export const layer = (
|
|
|
377
361
|
|
|
378
362
|
connectionsBySocket.set(socket.raw, connection);
|
|
379
363
|
connectionsById.set(connection.id, connection);
|
|
380
|
-
clientIds.add(connection.id);
|
|
381
364
|
nextClientId = Math.max(nextClientId, connection.id + 1);
|
|
382
365
|
|
|
383
366
|
return connection;
|
|
@@ -434,7 +417,6 @@ export const layer = (
|
|
|
434
417
|
|
|
435
418
|
connectionsBySocket.delete(socket.raw);
|
|
436
419
|
connectionsById.delete(connection.id);
|
|
437
|
-
clientIds.delete(connection.id);
|
|
438
420
|
activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
|
|
439
421
|
connection.requestIds.clear();
|
|
440
422
|
connection.nonResumableRequestIds.clear();
|
|
@@ -847,7 +829,7 @@ export const layer = (
|
|
|
847
829
|
|
|
848
830
|
connection?.socket.raw.close();
|
|
849
831
|
}),
|
|
850
|
-
clientIds: Effect.sync(() =>
|
|
832
|
+
clientIds: Effect.sync(() => new Set(connectionsById.keys())),
|
|
851
833
|
initialMessage: Effect.succeedNone,
|
|
852
834
|
supportsAck: true,
|
|
853
835
|
supportsTransferables: false,
|
|
@@ -1141,17 +1123,10 @@ const writeAttachment = (socket: WebSocket, key: string, metadata: AttachmentMet
|
|
|
1141
1123
|
}
|
|
1142
1124
|
|
|
1143
1125
|
const attachment = Predicate.isObject(current) ? current : {};
|
|
1144
|
-
const currentMetadata =
|
|
1145
|
-
Predicate.hasProperty(attachment, key) && Predicate.isObject(attachment[key])
|
|
1146
|
-
? attachment[key]
|
|
1147
|
-
: {};
|
|
1148
1126
|
|
|
1149
1127
|
socket.serializeAttachment({
|
|
1150
1128
|
...attachment,
|
|
1151
|
-
[key]:
|
|
1152
|
-
...currentMetadata,
|
|
1153
|
-
...metadata,
|
|
1154
|
-
},
|
|
1129
|
+
[key]: metadata,
|
|
1155
1130
|
});
|
|
1156
1131
|
};
|
|
1157
1132
|
|
|
@@ -3,6 +3,7 @@ import { Cause, Context, Effect, Exit } from "effect";
|
|
|
3
3
|
import { fromDurableObjectStorage, type DurableObjectStorage } from "./DurableObjectStorage";
|
|
4
4
|
import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
|
|
5
5
|
import type { WorkerContextWaitUntilOptions } from "./Worker";
|
|
6
|
+
import { runNativeCallback } from "./internal/NativeCallback";
|
|
6
7
|
import { makeWaitUntilScheduler } from "./internal/WorkerContext";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -87,24 +88,21 @@ export const fromDurableObjectState = (
|
|
|
87
88
|
storage: fromDurableObjectStorage(state.storage),
|
|
88
89
|
waitUntil,
|
|
89
90
|
blockConcurrencyWhile: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
state.blockConcurrencyWhile(() =>
|
|
94
|
-
runPromiseExitPreservingTypedFailures(context, effect),
|
|
95
|
-
),
|
|
96
|
-
),
|
|
97
|
-
),
|
|
98
|
-
Effect.flatten,
|
|
99
|
-
),
|
|
91
|
+
runNativeCallback<A, E, R, Exit.Exit<A, E>>((run) =>
|
|
92
|
+
state.blockConcurrencyWhile(() => runExitPreservingTypedFailures(run, effect)),
|
|
93
|
+
).pipe(Effect.orDie, Effect.flatten),
|
|
100
94
|
blockConcurrencyWhileOrReset: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
runNativeCallback<A, E, R, A>((run) =>
|
|
96
|
+
state.blockConcurrencyWhile(async () => {
|
|
97
|
+
const exit = await run(effect);
|
|
98
|
+
|
|
99
|
+
if (Exit.isFailure(exit)) {
|
|
100
|
+
throw Cause.squash(exit.cause);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return exit.value;
|
|
104
|
+
}),
|
|
105
|
+
).pipe(Effect.orDie),
|
|
108
106
|
acceptWebSocket: (ws: DurableWebSocket, tags?: Array<string>) =>
|
|
109
107
|
Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
|
|
110
108
|
getWebSockets: (tag?: string) =>
|
|
@@ -125,11 +123,11 @@ export const fromDurableObjectState = (
|
|
|
125
123
|
};
|
|
126
124
|
};
|
|
127
125
|
|
|
128
|
-
const
|
|
129
|
-
|
|
126
|
+
const runExitPreservingTypedFailures = async <A, E, R>(
|
|
127
|
+
run: (effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>,
|
|
130
128
|
effect: Effect.Effect<A, E, R>,
|
|
131
129
|
): Promise<Exit.Exit<A, E>> => {
|
|
132
|
-
const exit = await
|
|
130
|
+
const exit = await run(effect);
|
|
133
131
|
|
|
134
132
|
if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) {
|
|
135
133
|
throw Cause.squash(exit.cause);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
Clock,
|
|
4
|
+
Data,
|
|
5
|
+
Duration,
|
|
6
|
+
Effect,
|
|
7
|
+
Exit,
|
|
8
|
+
Fiber,
|
|
9
|
+
Option,
|
|
10
|
+
Predicate,
|
|
11
|
+
Result,
|
|
12
|
+
Schema as S,
|
|
13
|
+
} from "effect";
|
|
2
14
|
|
|
3
15
|
import * as ErrorMessage from "./internal/ErrorMessage";
|
|
16
|
+
import { runNativeCallback } from "./internal/NativeCallback";
|
|
4
17
|
|
|
5
18
|
export type SqlStorageValue = globalThis.SqlStorageValue;
|
|
6
19
|
|
|
@@ -331,6 +344,7 @@ export const fromDurableObjectStorage = (
|
|
|
331
344
|
// failed Exit is thrown to abort it. Keep a typed reference to that
|
|
332
345
|
// Exit rather than recovering its type from the untyped catch binding.
|
|
333
346
|
let aborted: Exit.Exit<A, E> | undefined;
|
|
347
|
+
let escapedFiber: Fiber.Fiber<unknown, unknown> | undefined;
|
|
334
348
|
|
|
335
349
|
try {
|
|
336
350
|
return Effect.succeed(
|
|
@@ -340,55 +354,59 @@ export const fromDurableObjectStorage = (
|
|
|
340
354
|
if (Exit.isSuccess(exit)) {
|
|
341
355
|
return exit.value;
|
|
342
356
|
}
|
|
357
|
+
const defect = Cause.findDefect(exit.cause);
|
|
358
|
+
|
|
359
|
+
if (Result.isSuccess(defect) && Cause.isAsyncFiberError(defect.success)) {
|
|
360
|
+
escapedFiber = defect.success.fiber;
|
|
361
|
+
escapedFiber.interruptUnsafe();
|
|
362
|
+
}
|
|
343
363
|
aborted = exit;
|
|
344
364
|
|
|
345
365
|
throw exit;
|
|
346
366
|
}),
|
|
347
367
|
);
|
|
348
368
|
} catch (cause) {
|
|
349
|
-
|
|
350
|
-
|
|
369
|
+
const failure: Effect.Effect<never, E | StorageOperationError> =
|
|
370
|
+
aborted !== undefined && cause === aborted && Exit.isFailure(aborted)
|
|
371
|
+
? Effect.failCause(aborted.cause)
|
|
372
|
+
: Effect.fail(storageError("transactionSync", cause));
|
|
373
|
+
const fiber = escapedFiber;
|
|
374
|
+
|
|
375
|
+
if (fiber !== undefined) {
|
|
376
|
+
return Fiber.interrupt(fiber).pipe(Effect.andThen(failure));
|
|
351
377
|
}
|
|
352
378
|
|
|
353
|
-
return
|
|
379
|
+
return failure;
|
|
354
380
|
}
|
|
355
381
|
}),
|
|
356
382
|
),
|
|
357
383
|
),
|
|
358
384
|
transaction: <A, E, R>(closure: (txn: DurableObjectTransaction) => Effect.Effect<A, E, R>) =>
|
|
359
|
-
Effect.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// transaction, and holding it here keeps its error type.
|
|
364
|
-
let aborted: Exit.Exit<A, E> | undefined;
|
|
385
|
+
Effect.suspend(() => {
|
|
386
|
+
// See `transactionSync`: the thrown Exit aborts Cloudflare's
|
|
387
|
+
// transaction, and holding it here keeps its error type.
|
|
388
|
+
let aborted: Exit.Exit<A, E> | undefined;
|
|
365
389
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
throw exit;
|
|
378
|
-
})
|
|
379
|
-
.then(
|
|
380
|
-
(value) => resume(Effect.succeed(value)),
|
|
381
|
-
(cause) => {
|
|
382
|
-
if (aborted !== undefined && cause === aborted && Exit.isFailure(aborted)) {
|
|
383
|
-
resume(Effect.failCause(aborted.cause));
|
|
384
|
-
} else {
|
|
385
|
-
resume(Effect.fail(storageError("transaction", cause)));
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
);
|
|
390
|
+
return runNativeCallback<A, E, R, A>((run) =>
|
|
391
|
+
storage.transaction(async (txn) => {
|
|
392
|
+
const exit = await run(Effect.suspend(() => closure(fromDurableObjectTransaction(txn))));
|
|
393
|
+
|
|
394
|
+
if (Exit.isSuccess(exit)) {
|
|
395
|
+
return exit.value;
|
|
396
|
+
}
|
|
397
|
+
aborted = exit;
|
|
398
|
+
|
|
399
|
+
throw exit;
|
|
389
400
|
}),
|
|
390
|
-
)
|
|
391
|
-
|
|
401
|
+
).pipe(
|
|
402
|
+
Effect.catch(
|
|
403
|
+
(cause): Effect.Effect<never, E | StorageOperationError> =>
|
|
404
|
+
aborted !== undefined && cause === aborted && Exit.isFailure(aborted)
|
|
405
|
+
? Effect.failCause(aborted.cause)
|
|
406
|
+
: Effect.fail(storageError("transaction", cause)),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}),
|
|
392
410
|
sync: () => tryStoragePromise("sync", () => storage.sync()),
|
|
393
411
|
getCurrentBookmark: () =>
|
|
394
412
|
tryStoragePromise("getCurrentBookmark", () => storage.getCurrentBookmark()),
|
|
@@ -38,53 +38,37 @@ export interface DurableWebSocket<Attachment = unknown> {
|
|
|
38
38
|
send(data: DurableWebSocketSendData): Effect.Effect<void, DurableWebSocketSendError>;
|
|
39
39
|
close(code?: number, reason?: string): Effect.Effect<void, DurableWebSocketCloseError>;
|
|
40
40
|
/** Serializes hibernation attachment metadata onto the socket. */
|
|
41
|
-
serializeAttachment<A = Attachment>(
|
|
41
|
+
serializeAttachment<A extends Attachment = Attachment>(
|
|
42
42
|
value: A,
|
|
43
43
|
): Effect.Effect<void, DurableWebSocketAttachmentError>;
|
|
44
44
|
/** Deserializes hibernation attachment metadata from the socket. */
|
|
45
45
|
readonly deserializeAttachment: Effect.Effect<unknown, DurableWebSocketAttachmentError>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const wrappers = new WeakMap<WebSocket, DurableWebSocket<unknown>>();
|
|
49
|
-
|
|
50
48
|
export const fromWebSocket = <Attachment = unknown>(
|
|
51
49
|
raw: WebSocket,
|
|
52
|
-
): DurableWebSocket<Attachment> => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const socket: DurableWebSocket<unknown> = {
|
|
61
|
-
raw,
|
|
62
|
-
send: (data) =>
|
|
63
|
-
Effect.try({
|
|
64
|
-
try: () => raw.send(data),
|
|
65
|
-
catch: (cause) => new DurableWebSocketSendError({ cause }),
|
|
66
|
-
}),
|
|
67
|
-
close: (code, reason) =>
|
|
68
|
-
Effect.try({
|
|
69
|
-
try: () => raw.close(code, reason),
|
|
70
|
-
catch: (cause) => new DurableWebSocketCloseError({ cause }),
|
|
71
|
-
}),
|
|
72
|
-
serializeAttachment: (value) =>
|
|
73
|
-
Effect.try({
|
|
74
|
-
try: () => raw.serializeAttachment(value),
|
|
75
|
-
catch: (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
|
|
76
|
-
}),
|
|
77
|
-
deserializeAttachment: Effect.try({
|
|
78
|
-
try: () => raw.deserializeAttachment(),
|
|
79
|
-
catch: (cause) => new DurableWebSocketAttachmentError({ operation: "deserialize", cause }),
|
|
50
|
+
): DurableWebSocket<Attachment> => ({
|
|
51
|
+
raw,
|
|
52
|
+
send: (data) =>
|
|
53
|
+
Effect.try({
|
|
54
|
+
try: () => raw.send(data),
|
|
55
|
+
catch: (cause) => new DurableWebSocketSendError({ cause }),
|
|
80
56
|
}),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
57
|
+
close: (code, reason) =>
|
|
58
|
+
Effect.try({
|
|
59
|
+
try: () => raw.close(code, reason),
|
|
60
|
+
catch: (cause) => new DurableWebSocketCloseError({ cause }),
|
|
61
|
+
}),
|
|
62
|
+
serializeAttachment: <A extends Attachment = Attachment>(value: A) =>
|
|
63
|
+
Effect.try({
|
|
64
|
+
try: () => raw.serializeAttachment(value),
|
|
65
|
+
catch: (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
|
|
66
|
+
}),
|
|
67
|
+
deserializeAttachment: Effect.try({
|
|
68
|
+
try: () => raw.deserializeAttachment(),
|
|
69
|
+
catch: (cause) => new DurableWebSocketAttachmentError({ operation: "deserialize", cause }),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
88
72
|
|
|
89
73
|
export interface AcceptUpgradeOptions<Attachment = unknown> {
|
|
90
74
|
readonly tags?: ReadonlyArray<string> | undefined;
|
|
@@ -165,15 +149,13 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
|
|
|
165
149
|
S.Codec.Encoded<AttachmentSchema>
|
|
166
150
|
> => {
|
|
167
151
|
type Attachment = S.Schema.Type<AttachmentSchema>;
|
|
168
|
-
type Encoded = S.Codec.Encoded<AttachmentSchema>;
|
|
169
152
|
|
|
170
153
|
const serialize = (socket: DurableWebSocket<unknown>, value: Attachment) =>
|
|
171
154
|
S.encodeEffect(schema)(value).pipe(
|
|
172
155
|
Effect.mapError(
|
|
173
156
|
(cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
|
|
174
157
|
),
|
|
175
|
-
|
|
176
|
-
Effect.flatMap((encoded) => socket.serializeAttachment(encoded as Encoded)),
|
|
158
|
+
Effect.flatMap((encoded) => socket.serializeAttachment(encoded)),
|
|
177
159
|
);
|
|
178
160
|
|
|
179
161
|
const deserialize = (socket: DurableWebSocket<unknown>) =>
|