1import type * as cf from "@cloudflare/workers-types";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
5import * as HttpBody from "effect/http/HttpBody";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import { DurableObjectState } from "./DurableObjectState.ts";
8
9export type RawWebSocket = cf.WebSocket;
10
11/**
12 * An application attachment could not be encoded, decoded, read, or written.
13 * Catch this tag to choose whether to fail, ignore the attachment, or close the
14 * connection. Codec failures and native exceptions are preserved in `cause`.
15 */
16export class WebSocketAttachmentError extends Data.TaggedError(
17 "WebSocketAttachmentError",
18)<{
19 /** The failed attachment operation, or `missing` for a nullish attachment. */
20 readonly reason: "encode" | "decode" | "missing" | "read" | "write";
21 /** A description of the failed operation. */
22 readonly message: string;
23 /** The original schema error or native exception, when present. */
24 readonly cause?: unknown;
25}> {}
26
27export interface WebSocket {
28 readonly ws: RawWebSocket;
29 send(data: string | Uint8Array): Effect.Effect<void>;
30 close(code: number, reason: string): Effect.Effect<void>;
31 /**
32 * Encode an application value and persist the encoded representation alongside
33 * this socket. The native structured-clone and serialized-size limits apply;
34 * schema validation does not guarantee that the encoded value is cloneable.
35 * Requires only the codec's encoding services. Does not close the socket.
36 *
37 * @example
38 * ```ts
39 * yield* socket.setAttachment(Schema.NumberFromString, 42);
40 * // The unchecked deserializeAttachment() now returns "42".
41 * ```
42 */
43 setAttachment<S extends Schema.Constraint>(
44 schema: S,
45 value: NoInfer<S["Type"]>,
46 ): Effect.Effect<void, WebSocketAttachmentError, S["EncodingServices"]>;
47 /**
48 * Read and decode an application attachment, including after hibernation.
49 * A nullish native value fails with reason `missing`; other values are decoded
50 * from the codec's encoded side. Requires only its decoding services.
51 * No envelope or schema version is added, so unchecked attachments interoperate
52 * and applications own migrations and invalid-attachment policy.
53 *
54 * @example
55 * ```ts
56 * const count = yield* socket.getAttachment(Schema.NumberFromString);
57 * // count is a number, not the stored string.
58 * ```
59 */
60 getAttachment<S extends Schema.Constraint>(
61 schema: S,
62 ): Effect.Effect<S["Type"], WebSocketAttachmentError, S["DecodingServices"]>;
63 serializeAttachment<T>(value: T): void;
64 deserializeAttachment<T>(): T | null;
65}
66
67export const fromWebSocket = (ws: RawWebSocket): WebSocket => ({
68 ws,
69 send: (data) => Effect.sync(() => ws.send(data as any)),
70 close: (code, reason) => Effect.sync(() => ws.close(code, reason)),
71 setAttachment: (schema, value) =>
72 Schema.encodeEffect(schema)(value).pipe(
73 Effect.mapError(
74 (cause) =>
75 new WebSocketAttachmentError({
76 reason: "encode",
77 message: "Could not encode WebSocket attachment",
78 cause,
79 }),
80 ),
81 Effect.flatMap((encoded) =>
82 Effect.try({
83 try: () => ws.serializeAttachment(encoded),
84 catch: (cause) =>
85 new WebSocketAttachmentError({
86 reason: "write",
87 message: "Could not write WebSocket attachment",
88 cause,
89 }),
90 }),
91 ),
92 ),
93 getAttachment: (schema) =>
94 Effect.try({
95 try: () => ws.deserializeAttachment(),
96 catch: (cause) =>
97 new WebSocketAttachmentError({
98 reason: "read",
99 message: "Could not read WebSocket attachment",
100 cause,
101 }),
102 }).pipe(
103 Effect.flatMap((encoded) =>
104 encoded === null || encoded === undefined
105 ? Effect.fail(
106 new WebSocketAttachmentError({
107 reason: "missing",
108 message: "WebSocket attachment is missing",
109 }),
110 )
111 : Schema.decodeUnknownEffect(schema)(encoded).pipe(
112 Effect.mapError(
113 (cause) =>
114 new WebSocketAttachmentError({
115 reason: "decode",
116 message: "Could not decode WebSocket attachment",
117 cause,
118 }),
119 ),
120 ),
121 ),
122 ),
123 serializeAttachment: (value) => ws.serializeAttachment(value),
124 deserializeAttachment: () => ws.deserializeAttachment() as any,
125});
126
127// declare global {
128// const WebSocketPair: new () => [cf.WebSocket, cf.WebSocket];
129// }
130
131export const upgrade = Effect.fn(function* () {
132 const _Response = Response as any as typeof cf.Response;
133 const ctx = yield* DurableObjectState;
134 // @ts-expect-error
135 const [client, server] = new WebSocketPair();
136 const serverSocket = fromWebSocket(server);
137 yield* ctx.acceptWebSocket(serverSocket);
138 const rawResponse = new _Response(null, {
139 status: 101,
140 webSocket: client,
141 });
142 const effectResponse = HttpServerResponse.setBody(
143 HttpServerResponse.empty({ status: 101 }),
144 HttpBody.raw(rawResponse),
145 );
146 return [effectResponse, serverSocket] as const;
147});