effect-cf 0.0.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.
@@ -0,0 +1,483 @@
1
+ import { Data, Effect } from "effect";
2
+
3
+ import * as Binding from "./Binding";
4
+ import * as CloudflareRpc from "./Rpc";
5
+ import type * as DurableObjectDefinition from "./DurableObjectDefinition";
6
+ import * as RpcDefinition from "./RpcDefinition";
7
+ import * as RpcInvocation from "./internal/RpcInvocation";
8
+
9
+ const TypeId = "effect-cf/DurableObjectNamespace" as const;
10
+
11
+ interface DurableObjectFetcher {
12
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
13
+ }
14
+
15
+ type RpcClient<Api> = {
16
+ readonly [Key in keyof Api as Key extends string
17
+ ? Api[Key] extends (...args: Array<any>) => unknown
18
+ ? Key
19
+ : never
20
+ : never]: Api[Key];
21
+ };
22
+
23
+ type ReservedMethodName = DurableObjectDefinition.ReservedMethodName | "fetch";
24
+
25
+ /**
26
+ * Cloudflare Durable Object stub, optionally enriched with RPC methods.
27
+ */
28
+ export type DurableObjectStubClient<Api extends object> = DurableObjectFetcher &
29
+ RpcClient<CloudflareRpc.Provider<Api, ReservedMethodName>> & {
30
+ readonly id: globalThis.DurableObjectId;
31
+ readonly name?: string;
32
+ };
33
+
34
+ /**
35
+ * Native Durable Object namespace binding shape.
36
+ */
37
+ export interface DurableObjectNamespaceClient<Api extends object> {
38
+ /** Creates a globally unique Durable Object id. */
39
+ newUniqueId(
40
+ options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions,
41
+ ): globalThis.DurableObjectId;
42
+ /** Deterministically maps a name to a Durable Object id. */
43
+ idFromName(name: string): globalThis.DurableObjectId;
44
+ /** Rehydrates a Durable Object id from its string form. */
45
+ idFromString(id: string): globalThis.DurableObjectId;
46
+ /** Returns a stub for an existing Durable Object id. */
47
+ get(
48
+ id: globalThis.DurableObjectId,
49
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
50
+ ): DurableObjectStubClient<Api>;
51
+ /** Returns a stub by deterministic name. */
52
+ getByName(
53
+ name: string,
54
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
55
+ ): DurableObjectStubClient<Api>;
56
+ /** Selects a namespace pinned to a specific jurisdiction. */
57
+ jurisdiction(
58
+ jurisdiction: globalThis.DurableObjectJurisdiction,
59
+ ): DurableObjectNamespaceClient<Api>;
60
+ }
61
+
62
+ /**
63
+ * Minimal namespace binding metadata.
64
+ */
65
+ export interface DurableObjectNamespaceDefinition {
66
+ readonly binding: string;
67
+ }
68
+
69
+ type ApiFromDefinition<Definition> = Definition extends DurableObjectDefinition.Definition.Any
70
+ ? DurableObjectDefinition.ServerApi<Definition>
71
+ : never;
72
+
73
+ type ApiOrDefinition<Api extends object, Definition> = [Api] extends [never]
74
+ ? ApiFromDefinition<Definition>
75
+ : Api;
76
+
77
+ /**
78
+ * Namespace binding metadata with optional RPC schema.
79
+ */
80
+ export interface DurableObjectNamespaceBindingDefinition<
81
+ Definition extends DurableObjectDefinition.Definition.Any | undefined = undefined,
82
+ > {
83
+ /** Binding name as configured in `wrangler.jsonc`. */
84
+ readonly binding: string;
85
+ /** Optional RPC schema used for argument/result encoding. */
86
+ readonly definition?: Definition;
87
+ }
88
+
89
+ /**
90
+ * Failure raised when invoking an RPC method on a Durable Object stub.
91
+ */
92
+ export class DurableObjectRpcError extends Data.TaggedError("DurableObjectRpcError")<{
93
+ readonly binding: string;
94
+ readonly method: string;
95
+ readonly cause: unknown;
96
+ }> {}
97
+
98
+ /**
99
+ * Failure raised when forwarding a request to a Durable Object stub.
100
+ */
101
+ export class DurableObjectFetchError extends Data.TaggedError("DurableObjectFetchError")<{
102
+ readonly binding: string;
103
+ readonly cause: unknown;
104
+ }> {}
105
+
106
+ type StubMethodKey<Api> = RpcInvocation.AsyncMethodKey<Api>;
107
+ type StubMethodArgs<Api, Method extends keyof Api> = RpcInvocation.AsyncMethodArgs<Api, Method>;
108
+ type StubMethodSuccess<Api, Method extends keyof Api> = RpcInvocation.AsyncMethodSuccess<
109
+ Api,
110
+ Method
111
+ >;
112
+
113
+ type StubCall<Api extends object> = <Method extends StubMethodKey<Api>>(
114
+ stub: DurableObjectStubClient<Api>,
115
+ method: Method,
116
+ ...args: StubMethodArgs<Api, Method>
117
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError>;
118
+
119
+ type DurableObjectDirectClient<Self, Api extends object> = {
120
+ readonly fetch: (
121
+ input: RequestInfo | URL,
122
+ init?: RequestInit,
123
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, Self>;
124
+ } & {
125
+ readonly [Method in StubMethodKey<Api>]: (
126
+ ...args: StubMethodArgs<Api, Method>
127
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError, Self>;
128
+ };
129
+
130
+ type DefinitionNamespaceDirectMethods<
131
+ Api extends object,
132
+ Definition extends DurableObjectDefinition.Definition.Any,
133
+ > = {
134
+ readonly [Method in RpcDefinition.Definition.MethodNames<Definition>]: (
135
+ stub: DurableObjectStubClient<Api>,
136
+ ...args: RpcDefinition.Method.Args<Definition["methods"][Method]>
137
+ ) => Effect.Effect<
138
+ RpcDefinition.Method.Success<Definition["methods"][Method]>,
139
+ DurableObjectRpcError
140
+ >;
141
+ };
142
+
143
+ type DefinitionDurableObjectDirectClient<
144
+ Self,
145
+ Definition extends DurableObjectDefinition.Definition.Any,
146
+ > = {
147
+ readonly fetch: (
148
+ input: RequestInfo | URL,
149
+ init?: RequestInit,
150
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, Self>;
151
+ } & {
152
+ readonly [Method in RpcDefinition.Definition.MethodNames<Definition>]: (
153
+ ...args: RpcDefinition.Method.Args<Definition["methods"][Method]>
154
+ ) => Effect.Effect<
155
+ RpcDefinition.Method.Success<Definition["methods"][Method]>,
156
+ DurableObjectRpcError,
157
+ Self
158
+ >;
159
+ };
160
+
161
+ type DefinitionNamespaceDirectClientMethods<
162
+ Self,
163
+ Definition extends DurableObjectDefinition.Definition.Any,
164
+ > = {
165
+ readonly byName: (
166
+ name: string,
167
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
168
+ ) => DefinitionDurableObjectDirectClient<Self, Definition>;
169
+ readonly byId: (
170
+ id: globalThis.DurableObjectId,
171
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
172
+ ) => DefinitionDurableObjectDirectClient<Self, Definition>;
173
+ };
174
+
175
+ type DirectMethods<
176
+ Self,
177
+ Api extends object,
178
+ Definition,
179
+ > = Definition extends DurableObjectDefinition.Definition.Any
180
+ ? DefinitionNamespaceDirectMethods<Api, Definition> &
181
+ DefinitionNamespaceDirectClientMethods<Self, Definition>
182
+ : {};
183
+
184
+ /**
185
+ * Creates a typed Effect service for a Durable Object namespace binding.
186
+ *
187
+ * Returned value includes namespace helpers (`get`, `getByName`, `newUniqueId`),
188
+ * raw `fetch` / `call` helpers, and direct RPC helpers when `definition` is provided.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * const ChatRoom = DurableObjectDefinition.make("ChatRoom", {
193
+ * postMessage: DurableObjectDefinition.method({
194
+ * args: [Schema.String],
195
+ * success: Schema.Void,
196
+ * }),
197
+ * });
198
+ *
199
+ * const ChatRooms = ChatRoom.namespace("CHAT_ROOMS", { binding: "CHAT_ROOMS" });
200
+ *
201
+ * const program = Effect.gen(function* () {
202
+ * const room = ChatRooms.byName("general");
203
+ * yield* room.postMessage("hello");
204
+ * });
205
+ * ```
206
+ */
207
+ export const Service =
208
+ <Self, Api extends object = never>() =>
209
+ <
210
+ Id extends string,
211
+ const Definition extends DurableObjectDefinition.Definition.Any | undefined = undefined,
212
+ >(
213
+ id: Id,
214
+ definition: DurableObjectNamespaceBindingDefinition<Definition>,
215
+ ) => {
216
+ type NamespaceApi = ApiOrDefinition<Api, Definition>;
217
+ type NamespaceClient = DurableObjectNamespaceClient<NamespaceApi>;
218
+ type StubClient = DurableObjectStubClient<NamespaceApi>;
219
+
220
+ const hasFunction = (value: object, key: string): boolean =>
221
+ typeof Reflect.get(value, key) === "function";
222
+
223
+ const isDurableObjectNamespaceClient = (value: unknown): value is NamespaceClient =>
224
+ typeof value === "object" &&
225
+ value !== null &&
226
+ hasFunction(value, "getByName") &&
227
+ hasFunction(value, "get") &&
228
+ hasFunction(value, "idFromName") &&
229
+ hasFunction(value, "idFromString") &&
230
+ hasFunction(value, "newUniqueId") &&
231
+ hasFunction(value, "jurisdiction");
232
+
233
+ const tag = Binding.Service<Self>()(id, definition.binding, (value): value is NamespaceClient =>
234
+ isDurableObjectNamespaceClient(value),
235
+ );
236
+
237
+ const newUniqueId = Effect.fnUntraced(function* (
238
+ options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions,
239
+ ) {
240
+ const namespace = yield* tag;
241
+ return namespace.newUniqueId(options);
242
+ });
243
+
244
+ const idFromName = Effect.fnUntraced(function* (name: string) {
245
+ const namespace = yield* tag;
246
+ return namespace.idFromName(name);
247
+ });
248
+
249
+ const idFromString = Effect.fnUntraced(function* (id: string) {
250
+ const namespace = yield* tag;
251
+ return namespace.idFromString(id);
252
+ });
253
+
254
+ const get = Effect.fnUntraced(function* (
255
+ id: globalThis.DurableObjectId,
256
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
257
+ ) {
258
+ const namespace = yield* tag;
259
+ return namespace.get(id, options);
260
+ });
261
+
262
+ const getByName = Effect.fnUntraced(function* (
263
+ name: string,
264
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
265
+ ) {
266
+ const namespace = yield* tag;
267
+ return namespace.getByName(name, options);
268
+ });
269
+
270
+ const jurisdiction = Effect.fnUntraced(function* (value: globalThis.DurableObjectJurisdiction) {
271
+ const namespace = yield* tag;
272
+ return namespace.jurisdiction(value);
273
+ });
274
+
275
+ const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
276
+ Effect.tryPromise({
277
+ try: () => stub.fetch(input, init),
278
+ catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
279
+ });
280
+
281
+ const rpc = <Method extends StubMethodKey<NamespaceApi>>(
282
+ stub: StubClient,
283
+ method: Method,
284
+ ...args: StubMethodArgs<NamespaceApi, Method>
285
+ ) =>
286
+ Effect.gen(function* () {
287
+ const methodName = String(method);
288
+ const encodedArgs =
289
+ definition.definition === undefined
290
+ ? args
291
+ : yield* RpcDefinition.encodeArgs(
292
+ definition.definition,
293
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
294
+ args as never,
295
+ ).pipe(
296
+ Effect.mapError(
297
+ (cause) =>
298
+ new DurableObjectRpcError({
299
+ binding: definition.binding,
300
+ method: methodName,
301
+ cause,
302
+ }),
303
+ ),
304
+ );
305
+
306
+ return yield* RpcInvocation.invokeRpcMethod(
307
+ stub,
308
+ method,
309
+ encodedArgs as StubMethodArgs<NamespaceApi, Method>,
310
+ (cause) =>
311
+ new DurableObjectRpcError({
312
+ binding: definition.binding,
313
+ method: methodName,
314
+ cause,
315
+ }),
316
+ );
317
+ });
318
+
319
+ const call = <Method extends StubMethodKey<NamespaceApi>>(
320
+ stub: StubClient,
321
+ method: Method,
322
+ ...args: StubMethodArgs<NamespaceApi, Method>
323
+ ) =>
324
+ Effect.gen(function* () {
325
+ const methodName = String(method);
326
+ const value = yield* CloudflareRpc.resolve(yield* rpc(stub, method, ...args)).pipe(
327
+ Effect.mapError(
328
+ (cause) =>
329
+ new DurableObjectRpcError({
330
+ binding: definition.binding,
331
+ method: methodName,
332
+ cause,
333
+ }),
334
+ ),
335
+ );
336
+
337
+ if (definition.definition === undefined) {
338
+ return value as StubMethodSuccess<NamespaceApi, Method>;
339
+ }
340
+
341
+ const decoded = yield* RpcDefinition.decodeSuccess(
342
+ definition.definition,
343
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
344
+ value,
345
+ ).pipe(
346
+ Effect.mapError(
347
+ (cause) =>
348
+ new DurableObjectRpcError({
349
+ binding: definition.binding,
350
+ method: methodName,
351
+ cause,
352
+ }),
353
+ ),
354
+ );
355
+
356
+ return decoded as StubMethodSuccess<NamespaceApi, Method>;
357
+ });
358
+
359
+ const scopedCall = <Method extends StubMethodKey<NamespaceApi>>(
360
+ stub: StubClient,
361
+ method: Method,
362
+ ...args: StubMethodArgs<NamespaceApi, Method>
363
+ ) =>
364
+ Effect.gen(function* () {
365
+ const result = yield* rpc(stub, method, ...args);
366
+ return yield* CloudflareRpc.scoped(result);
367
+ });
368
+
369
+ const directMethods = makeDirectMethods<Self, NamespaceApi, Definition>(definition.definition, {
370
+ call,
371
+ fetch,
372
+ get,
373
+ getByName,
374
+ });
375
+
376
+ return Object.assign(tag, directMethods, {
377
+ [TypeId]: TypeId,
378
+ definition,
379
+ newUniqueId,
380
+ idFromName,
381
+ idFromString,
382
+ get,
383
+ getByName,
384
+ jurisdiction,
385
+ fetch,
386
+ rpc,
387
+ call,
388
+ scopedCall,
389
+ }) as typeof tag &
390
+ DirectMethods<Self, NamespaceApi, Definition> & {
391
+ readonly [TypeId]: typeof TypeId;
392
+ readonly definition: DurableObjectNamespaceBindingDefinition<Definition>;
393
+ readonly newUniqueId: typeof newUniqueId;
394
+ readonly idFromName: typeof idFromName;
395
+ readonly idFromString: typeof idFromString;
396
+ readonly get: typeof get;
397
+ readonly getByName: typeof getByName;
398
+ readonly jurisdiction: typeof jurisdiction;
399
+ readonly fetch: typeof fetch;
400
+ readonly rpc: typeof rpc;
401
+ readonly call: typeof call;
402
+ readonly scopedCall: typeof scopedCall;
403
+ };
404
+ };
405
+
406
+ const makeDirectMethods = <
407
+ Self,
408
+ Api extends object,
409
+ Definition extends DurableObjectDefinition.Definition.Any | undefined,
410
+ >(
411
+ rpcDefinition: Definition | undefined,
412
+ helpers: {
413
+ readonly call: StubCall<Api>;
414
+ readonly fetch: (
415
+ stub: DurableObjectStubClient<Api>,
416
+ input: RequestInfo | URL,
417
+ init?: RequestInit,
418
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError>;
419
+ readonly get: (
420
+ id: globalThis.DurableObjectId,
421
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
422
+ ) => Effect.Effect<DurableObjectStubClient<Api>, never, Self>;
423
+ readonly getByName: (
424
+ name: string,
425
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
426
+ ) => Effect.Effect<DurableObjectStubClient<Api>, never, Self>;
427
+ },
428
+ ): DirectMethods<Self, Api, Definition> => {
429
+ const methods = {} as Record<string, unknown>;
430
+
431
+ if (rpcDefinition !== undefined) {
432
+ for (const methodName of Object.keys(rpcDefinition.methods)) {
433
+ methods[methodName] = (stub: DurableObjectStubClient<Api>, ...args: Array<unknown>) =>
434
+ (
435
+ helpers.call as (
436
+ stub: DurableObjectStubClient<Api>,
437
+ method: string,
438
+ ...args: Array<unknown>
439
+ ) => unknown
440
+ )(stub, methodName, ...args);
441
+ }
442
+
443
+ const makeClient = (
444
+ getStub: () => Effect.Effect<DurableObjectStubClient<Api>, never, Self>,
445
+ ): DurableObjectDirectClient<Self, Api> => {
446
+ const client = {
447
+ fetch: (input: RequestInfo | URL, init?: RequestInit) =>
448
+ Effect.gen(function* () {
449
+ const stub = yield* getStub();
450
+ return yield* helpers.fetch(stub, input, init);
451
+ }),
452
+ } as Record<string, unknown>;
453
+
454
+ for (const methodName of Object.keys(rpcDefinition.methods)) {
455
+ client[methodName] = (...args: Array<unknown>) =>
456
+ Effect.gen(function* () {
457
+ const stub = yield* getStub();
458
+ return yield* (
459
+ helpers.call as (
460
+ stub: DurableObjectStubClient<Api>,
461
+ method: string,
462
+ ...args: Array<unknown>
463
+ ) => Effect.Effect<unknown, DurableObjectRpcError>
464
+ )(stub, methodName, ...args);
465
+ });
466
+ }
467
+
468
+ return client as DurableObjectDirectClient<Self, Api>;
469
+ };
470
+
471
+ methods.byName = (
472
+ name: string,
473
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
474
+ ) => makeClient(() => helpers.getByName(name, options));
475
+
476
+ methods.byId = (
477
+ id: globalThis.DurableObjectId,
478
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
479
+ ) => makeClient(() => helpers.get(id, options));
480
+ }
481
+
482
+ return methods as DirectMethods<Self, Api, Definition>;
483
+ };
@@ -0,0 +1,222 @@
1
+ import { Context, Effect, Layer, Option, Queue } from "effect";
2
+ import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
3
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
4
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
5
+
6
+ import { DurableObjectState } from "./DurableObjectState";
7
+
8
+ const defaultAttachmentKey = "effectCloudflareRpcClientId";
9
+ const defaultTag = "effect-cf-rpc";
10
+
11
+ /**
12
+ * Configuration for {@link layer}.
13
+ */
14
+ export interface LayerOptions {
15
+ /** Tag used to select hibernated sockets. */
16
+ readonly tag?: string | undefined;
17
+ /** Socket attachment key used to persist client ids across hibernation. */
18
+ readonly attachmentKey?: string | undefined;
19
+ }
20
+
21
+ /** Native websocket event payload accepted by Cloudflare Durable Objects. */
22
+ export type NativeWebSocketMessage = string | ArrayBuffer;
23
+
24
+ /**
25
+ * Service API used to wire websocket lifecycle events to Effect RPC server protocol.
26
+ */
27
+ export interface DurableObjectRpcWebSocketService {
28
+ readonly accept: (socket: WebSocket) => Effect.Effect<void>;
29
+ readonly message: (socket: WebSocket, message: NativeWebSocketMessage) => Effect.Effect<void>;
30
+ readonly close: (socket: WebSocket) => Effect.Effect<void>;
31
+ readonly error: (socket: WebSocket, error: unknown) => Effect.Effect<void>;
32
+ }
33
+
34
+ interface RpcConnection {
35
+ readonly id: number;
36
+ readonly socket: WebSocket;
37
+ readonly parser: RpcSerialization.Parser;
38
+ }
39
+
40
+ /**
41
+ * Context tag for the Durable Object RPC websocket service.
42
+ */
43
+ export class DurableObjectRpcWebSocket extends Context.Service<
44
+ DurableObjectRpcWebSocket,
45
+ DurableObjectRpcWebSocketService
46
+ >()("effect-cf/DurableObjectRpcWebSocket") {}
47
+
48
+ /**
49
+ * Builds a layer that bridges Durable Object websocket events to `RpcServer.Protocol`.
50
+ */
51
+ export const layer = (options: LayerOptions = {}) =>
52
+ Layer.effectContext(
53
+ Effect.gen(function* () {
54
+ const tag = options.tag ?? defaultTag;
55
+ const attachmentKey = options.attachmentKey ?? defaultAttachmentKey;
56
+ const durableObjectState = yield* DurableObjectState;
57
+ const serialization = yield* RpcSerialization.RpcSerialization;
58
+ const disconnects = yield* Queue.make<number>();
59
+ const connectionsBySocket = new Map<WebSocket, RpcConnection>();
60
+ const connectionsById = new Map<number, RpcConnection>();
61
+ const clientIds = new Set<number>();
62
+ let nextClientId = 0;
63
+ let writeRequest:
64
+ | ((clientId: number, data: RpcMessage.FromClientEncoded) => Effect.Effect<void>)
65
+ | undefined;
66
+
67
+ const reserveClientId = (socket: WebSocket) => {
68
+ const attachment = readAttachment(socket, attachmentKey);
69
+ if (attachment !== undefined) {
70
+ nextClientId = Math.max(nextClientId, attachment + 1);
71
+ return attachment;
72
+ }
73
+
74
+ const id = nextClientId++;
75
+ writeAttachment(socket, attachmentKey, id);
76
+ return id;
77
+ };
78
+
79
+ const register = (socket: WebSocket) => {
80
+ const existing = connectionsBySocket.get(socket);
81
+ if (existing !== undefined) {
82
+ return existing;
83
+ }
84
+
85
+ const connection = {
86
+ id: reserveClientId(socket),
87
+ socket,
88
+ parser: serialization.makeUnsafe(),
89
+ } satisfies RpcConnection;
90
+
91
+ connectionsBySocket.set(socket, connection);
92
+ connectionsById.set(connection.id, connection);
93
+ clientIds.add(connection.id);
94
+ return connection;
95
+ };
96
+
97
+ const unregister = (socket: WebSocket) =>
98
+ Effect.sync(() => {
99
+ const connection = connectionsBySocket.get(socket);
100
+ if (connection === undefined) {
101
+ return;
102
+ }
103
+
104
+ connectionsBySocket.delete(socket);
105
+ connectionsById.delete(connection.id);
106
+ clientIds.delete(connection.id);
107
+ Queue.offerUnsafe(disconnects, connection.id);
108
+ });
109
+
110
+ for (const socket of yield* durableObjectState.getWebSockets(tag)) {
111
+ register(socket);
112
+ }
113
+
114
+ const send = (connection: RpcConnection, response: RpcMessage.FromServerEncoded) =>
115
+ Effect.sync(() => {
116
+ try {
117
+ const encoded = connection.parser.encode(response);
118
+ if (encoded !== undefined) {
119
+ connection.socket.send(encoded);
120
+ }
121
+ } catch (cause) {
122
+ const encoded = connection.parser.encode(RpcMessage.ResponseDefectEncoded(cause));
123
+ if (encoded !== undefined) {
124
+ connection.socket.send(encoded);
125
+ }
126
+ }
127
+ });
128
+
129
+ const protocol = yield* RpcServer.Protocol.make((writeRequest_) => {
130
+ writeRequest = writeRequest_;
131
+
132
+ return Effect.succeed({
133
+ disconnects,
134
+ send: (clientId, response) => {
135
+ const connection = connectionsById.get(clientId);
136
+ return connection === undefined ? Effect.void : send(connection, response);
137
+ },
138
+ end: (clientId) =>
139
+ Effect.sync(() => {
140
+ const connection = connectionsById.get(clientId);
141
+ connection?.socket.close();
142
+ }),
143
+ clientIds: Effect.sync(() => clientIds),
144
+ initialMessage: Effect.succeed(Option.none()),
145
+ supportsAck: true,
146
+ supportsTransferables: false,
147
+ supportsSpanPropagation: true,
148
+ });
149
+ });
150
+
151
+ const service = DurableObjectRpcWebSocket.of({
152
+ accept: (socket) =>
153
+ Effect.gen(function* () {
154
+ register(socket);
155
+ yield* durableObjectState.acceptWebSocket(socket, [tag]);
156
+ }),
157
+ message: (socket, message) =>
158
+ Effect.gen(function* () {
159
+ const connection = register(socket);
160
+ const decoded = yield* Effect.try({
161
+ try: () => connection.parser.decode(normalizeMessage(message)),
162
+ catch: RpcMessage.ResponseDefectEncoded,
163
+ });
164
+
165
+ const run = writeRequest;
166
+ if (run === undefined) {
167
+ yield* send(connection, RpcMessage.ResponseDefectEncoded("RPC server is not ready"));
168
+ return;
169
+ }
170
+
171
+ for (const current of decoded) {
172
+ yield* run(connection.id, current as RpcMessage.FromClientEncoded);
173
+ }
174
+ }).pipe(
175
+ Effect.catch((error) => {
176
+ const connection = register(socket);
177
+ return send(connection, error);
178
+ }),
179
+ ),
180
+ close: unregister,
181
+ error: (socket, error) =>
182
+ Effect.gen(function* () {
183
+ yield* Effect.logDebug("Durable Object RPC websocket error", error);
184
+ yield* unregister(socket);
185
+ }),
186
+ });
187
+
188
+ return Context.mergeAll(
189
+ Context.make(RpcServer.Protocol, protocol),
190
+ Context.make(DurableObjectRpcWebSocket, service),
191
+ );
192
+ }),
193
+ );
194
+
195
+ const normalizeMessage = (message: NativeWebSocketMessage) =>
196
+ typeof message === "string" ? message : new Uint8Array(message);
197
+
198
+ const readAttachment = (socket: WebSocket, key: string): number | undefined => {
199
+ const value = socket.deserializeAttachment();
200
+
201
+ if (
202
+ value !== null &&
203
+ typeof value === "object" &&
204
+ key in value &&
205
+ typeof value[key] === "number"
206
+ ) {
207
+ return value[key];
208
+ }
209
+
210
+ return undefined;
211
+ };
212
+
213
+ const writeAttachment = (socket: WebSocket, key: string, clientId: number) => {
214
+ const current = socket.deserializeAttachment();
215
+ const attachment =
216
+ current !== null && typeof current === "object" && !Array.isArray(current) ? current : {};
217
+
218
+ socket.serializeAttachment({
219
+ ...attachment,
220
+ [key]: clientId,
221
+ });
222
+ };