@firtoz/socka 3.0.3 → 4.0.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/{SockaWebSocketSession-i5U9wvd-.d.ts → SockaWebSocketSession-BaGvSerM.d.ts} +19 -1
- package/dist/bun/index.d.ts +1 -1
- package/dist/bun/index.js +1 -1
- package/dist/{chunk-6HY22C5G.js → chunk-TTXY7O5P.js} +20 -4
- package/dist/chunk-TTXY7O5P.js.map +1 -0
- package/dist/do/index.d.ts +81 -23
- package/dist/do/index.js +62 -6
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +1 -1
- package/dist/hono/cloudflare-workers.js +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/hono/index.js +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +2 -2
- package/docs/durable-objects.md +71 -46
- package/docs/pushes.md +14 -1
- package/docs/react-durable-objects.md +8 -19
- package/docs/recipes.md +1 -1
- package/docs/reference.md +2 -2
- package/package.json +8 -8
- package/skills/socka/do-session/SKILL.md +1 -1
- package/dist/chunk-6HY22C5G.js.map +0 -1
|
@@ -141,10 +141,28 @@ interface SockaPushSession<TContract extends SockaContractBound> {
|
|
|
141
141
|
* Exclusion uses the **WebSocket** identity (`self.websocket`), not the session
|
|
142
142
|
* object reference, so the same `sessions` map can hold `SockaDoSession` while
|
|
143
143
|
* `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).
|
|
144
|
+
*
|
|
145
|
+
* When there is no caller session, use {@link broadcastSockaEventToAll} or
|
|
146
|
+
* {@link broadcastContractPushToAll} instead of picking an arbitrary anchor.
|
|
144
147
|
*/
|
|
145
148
|
declare function broadcastSockaEventToPeers(sessions: Map<WebSocket, SockaEmitCapable>, self: SockaEmitCapable & {
|
|
146
149
|
readonly websocket: WebSocket;
|
|
147
150
|
}, event: string, body: unknown, excludeSelf?: boolean): void;
|
|
151
|
+
/**
|
|
152
|
+
* Broadcast a socka server event to **every** session in the map. Payload must
|
|
153
|
+
* already be contract-validated.
|
|
154
|
+
*
|
|
155
|
+
* Use when there is no originating WebSocket session (HTTP admin routes, alarms,
|
|
156
|
+
* cron). Prefer {@link broadcastContractPushToAll} so validation stays centralized.
|
|
157
|
+
*/
|
|
158
|
+
declare function broadcastSockaEventToAll(sessions: Map<WebSocket, SockaEmitCapable>, event: string, body: unknown): void;
|
|
159
|
+
/**
|
|
160
|
+
* Validate a contract push payload and broadcast it to every session in the map.
|
|
161
|
+
*
|
|
162
|
+
* Works with any session type that implements {@link SockaEmitCapable} (including
|
|
163
|
+
* {@link SockaDoSession} on Durable Objects). No-op when `sessions` is empty.
|
|
164
|
+
*/
|
|
165
|
+
declare function broadcastContractPushToAll<TContract extends SockaContractBound, K extends keyof TContract["pushes"] & string>(sessions: Map<WebSocket, SockaEmitCapable>, contract: TContract, name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
|
|
148
166
|
/**
|
|
149
167
|
* Runtime-agnostic socka server session: standard {@link WebSocket} wire
|
|
150
168
|
* dispatch without Cloudflare Durable Object APIs.
|
|
@@ -206,4 +224,4 @@ declare class SockaWebSocketSession<TContract extends SockaContractBound, TData
|
|
|
206
224
|
*/
|
|
207
225
|
declare function runSockaSessionOnAttached<TContract extends SockaContractBound, TData>(config: SockaWebSocketSessionConfigUnion<TContract, TData>, session: SockaWebSocketSession<TContract, TData>): void;
|
|
208
226
|
|
|
209
|
-
export { type SockaStrictWebSocketInit as S, SockaWebSocketSession as a, type SockaWebSocketSessionConfigUnion as b, type SockaWebSocketInit as c, type
|
|
227
|
+
export { type SockaStrictWebSocketInit as S, SockaWebSocketSession as a, type SockaWebSocketSessionConfigUnion as b, type SockaWebSocketInit as c, type SockaWebSocketSessionConfig as d, broadcastContractPushToAll as e, broadcastSockaEventToAll as f, broadcastSockaEventToPeers as g, type SockaEmitCapable as h, type SockaPushSession as i, type SockaWebSocketSessionConfigLoose as j, runSockaSessionOnAttached as r };
|
package/dist/bun/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ServerWebSocket } from 'bun';
|
|
2
2
|
import { c as SockaContractBound, y as SockaWireFormat } from '../socka-report-error-nTXJIzNb.js';
|
|
3
|
-
import { S as SockaStrictWebSocketInit, a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion } from '../SockaWebSocketSession-
|
|
3
|
+
import { S as SockaStrictWebSocketInit, a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/bun/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dispatchSockaInboundMessage } from '../chunk-JR2GENNT.js';
|
|
2
|
-
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-
|
|
2
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-TTXY7O5P.js';
|
|
3
3
|
import { reportSockaError } from '../chunk-IFIGKR3W.js';
|
|
4
4
|
|
|
5
5
|
// src/bun/index.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseWirePayload, decodeSockaWire, SockaWireError, encodeServerError,
|
|
1
|
+
import { parseStandardSchema, parseWirePayload, decodeSockaWire, SockaWireError, encodeServerError, SockaError, encodeServerResponse, encodeSockaWire, encodeServerEvent, reportSockaError } from './chunk-IFIGKR3W.js';
|
|
2
2
|
import { exhaustiveGuard } from '@firtoz/maybe-error';
|
|
3
3
|
|
|
4
4
|
function isLooseUpgradeConfig(config) {
|
|
@@ -10,6 +10,22 @@ function broadcastSockaEventToPeers(sessions, self, event, body, excludeSelf = f
|
|
|
10
10
|
session.emitWireEvent(event, body);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function broadcastSockaEventToAll(sessions, event, body) {
|
|
14
|
+
for (const session of sessions.values()) {
|
|
15
|
+
session.emitWireEvent(event, body);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function broadcastContractPushToAll(sessions, contract, name, body) {
|
|
19
|
+
const schema = contract.pushes[name];
|
|
20
|
+
if (!schema) {
|
|
21
|
+
throw new Error(`socka: unknown push ${String(name)}`);
|
|
22
|
+
}
|
|
23
|
+
const validated = await parseStandardSchema(
|
|
24
|
+
schema,
|
|
25
|
+
body
|
|
26
|
+
);
|
|
27
|
+
broadcastSockaEventToAll(sessions, name, validated);
|
|
28
|
+
}
|
|
13
29
|
var SockaWebSocketSession = class {
|
|
14
30
|
constructor(websocket, sessions, config, init) {
|
|
15
31
|
this.websocket = websocket;
|
|
@@ -296,6 +312,6 @@ function runSockaSessionOnAttached(config, session) {
|
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
314
|
|
|
299
|
-
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached };
|
|
300
|
-
//# sourceMappingURL=chunk-
|
|
301
|
-
//# sourceMappingURL=chunk-
|
|
315
|
+
export { SockaWebSocketSession, broadcastContractPushToAll, broadcastSockaEventToAll, broadcastSockaEventToPeers, runSockaSessionOnAttached };
|
|
316
|
+
//# sourceMappingURL=chunk-TTXY7O5P.js.map
|
|
317
|
+
//# sourceMappingURL=chunk-TTXY7O5P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/SockaWebSocketSession.ts"],"names":[],"mappings":"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","file":"chunk-TTXY7O5P.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { exhaustiveGuard } from \"@firtoz/maybe-error\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContractBound,\n} from \"../core/contract\";\nimport {\n\tSockaWireError,\n\tdecodeSockaWire,\n\tencodeServerResponse,\n\tencodeServerError,\n\tencodeServerEvent,\n\ttype SockaClientRequestFrame,\n\ttype SockaWireFrame,\n} from \"../core/envelope\";\nimport {\n\tencodeSockaWire,\n\tparseWirePayload,\n\ttype SockaWireFormat,\n} from \"../core/wire-codec\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport { parseStandardSchema } from \"../core/validate\";\nimport { SockaError } from \"../core/socka-error\";\nimport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n} from \"./SockaWebSocketSessionConfig\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\nexport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n};\n\nfunction isLooseUpgradeConfig<TContract extends SockaContractBound, TData>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n): config is SockaWebSocketSessionConfigLoose<TContract, TData> {\n\treturn (\n\t\t\"strictUpgradeRequest\" in config && config.strictUpgradeRequest === false\n\t);\n}\n\n/** Session that can send a wire-level server event (already validated). */\nexport type SockaEmitCapable = {\n\temitWireEvent(event: string, body: unknown): void;\n};\n\n/**\n * Contract-typed session surface for handlers that push to clients.\n */\nexport interface SockaPushSession<TContract extends SockaContractBound> {\n\temitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void>;\n\tbroadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf?: boolean,\n\t): Promise<void>;\n}\n\n/**\n * Broadcast a socka server event to every session in the map (optionally\n * excluding the caller). Payload must already be contract-validated.\n *\n * Exclusion uses the **WebSocket** identity (`self.websocket`), not the session\n * object reference, so the same `sessions` map can hold `SockaDoSession` while\n * `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).\n *\n * When there is no caller session, use {@link broadcastSockaEventToAll} or\n * {@link broadcastContractPushToAll} instead of picking an arbitrary anchor.\n */\nexport function broadcastSockaEventToPeers(\n\tsessions: Map<WebSocket, SockaEmitCapable>,\n\tself: SockaEmitCapable & { readonly websocket: WebSocket },\n\tevent: string,\n\tbody: unknown,\n\texcludeSelf = false,\n): void {\n\tfor (const [ws, session] of sessions) {\n\t\tif (excludeSelf && ws === self.websocket) continue;\n\t\tsession.emitWireEvent(event, body);\n\t}\n}\n\n/**\n * Broadcast a socka server event to **every** session in the map. Payload must\n * already be contract-validated.\n *\n * Use when there is no originating WebSocket session (HTTP admin routes, alarms,\n * cron). Prefer {@link broadcastContractPushToAll} so validation stays centralized.\n */\nexport function broadcastSockaEventToAll(\n\tsessions: Map<WebSocket, SockaEmitCapable>,\n\tevent: string,\n\tbody: unknown,\n): void {\n\tfor (const session of sessions.values()) {\n\t\tsession.emitWireEvent(event, body);\n\t}\n}\n\n/**\n * Validate a contract push payload and broadcast it to every session in the map.\n *\n * Works with any session type that implements {@link SockaEmitCapable} (including\n * {@link SockaDoSession} on Durable Objects). No-op when `sessions` is empty.\n */\nexport async function broadcastContractPushToAll<\n\tTContract extends SockaContractBound,\n\tK extends keyof TContract[\"pushes\"] & string,\n>(\n\tsessions: Map<WebSocket, SockaEmitCapable>,\n\tcontract: TContract,\n\tname: K,\n\tbody: InferSockaPushPayload<TContract, K>,\n): Promise<void> {\n\tconst schema = contract.pushes[name];\n\tif (!schema) {\n\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t}\n\tconst validated = await parseStandardSchema(\n\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\tbody,\n\t);\n\tbroadcastSockaEventToAll(sessions, name, validated);\n}\n\n/**\n * Runtime-agnostic socka server session: standard {@link WebSocket} wire\n * dispatch without Cloudflare Durable Object APIs.\n */\nexport class SockaWebSocketSession<\n\tTContract extends SockaContractBound,\n\tTData = EmptySockaSessionData,\n> implements SockaPushSession<TContract>\n{\n\tprivate readonly config: SockaWebSocketSessionConfigUnion<TContract, TData>;\n\tprivate readonly wireFormat: SockaWireFormat;\n\tprivate _data!: TData;\n\n\tpublic constructor(\n\t\tpublic readonly websocket: WebSocket,\n\t\tprotected readonly sessions: Map<\n\t\t\tWebSocket,\n\t\t\tSockaWebSocketSession<TContract, TData>\n\t\t>,\n\t\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\t\tinit?: SockaWebSocketInit,\n\t) {\n\t\tthis.config = config;\n\t\tthis.wireFormat = config.wireFormat ?? \"json\";\n\t\tif (isLooseUpgradeConfig(config)) {\n\t\t\tconst createData = config.createData as\n\t\t\t\t| ((init: SockaWebSocketInit) => TData)\n\t\t\t\t| undefined;\n\t\t\tconst create = createData ?? ((_i: SockaWebSocketInit) => ({}) as TData);\n\t\t\tthis._data = create(init ?? {});\n\t\t} else {\n\t\t\tif (!init?.request) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"socka: strict upgrade (default) requires a Request on the upgrade init (e.g. Bun upgrade with `data: { …, request: req }`, or Hono default sockaInit), or use SockaWebSocketSessionConfigLoose with strictUpgradeRequest: false\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst strictInit: SockaStrictWebSocketInit = { request: init.request };\n\t\t\tif (config.createData) {\n\t\t\t\tthis._data = config.createData(strictInit);\n\t\t\t} else {\n\t\t\t\tthis._data = {} as TData;\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic get data(): TData {\n\t\treturn this._data;\n\t}\n\n\t/**\n\t * Session data for every connection in the same {@link sessions} map (same room),\n\t * optionally excluding this socket.\n\t */\n\tpublic listPeers(options?: { excludeSelf?: boolean }): TData[] {\n\t\tconst out: TData[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(s.data);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Like {@link listPeers} but maps each peer {@link SockaWebSocketSession}\n\t * (e.g. when you need more than {@link #data}).\n\t */\n\tpublic listPeersWith<R>(\n\t\tmap: (session: SockaWebSocketSession<TContract, TData>) => R,\n\t\toptions?: { excludeSelf?: boolean },\n\t): R[] {\n\t\tconst out: R[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(map(s));\n\t\t}\n\t\treturn out;\n\t}\n\n\t/** Count of sessions in this room (same {@link sessions} map), optionally excluding self. */\n\tpublic peerCount(options?: { excludeSelf?: boolean }): number {\n\t\tlet n = 0;\n\t\tfor (const [ws] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tn += 1;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/** Whether any peer sessions exist (optionally excluding self). */\n\tpublic hasPeers(options?: { excludeSelf?: boolean }): boolean {\n\t\treturn this.peerCount(options) > 0;\n\t}\n\n\t/**\n\t * Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.\n\t * Server adapters should call this when the WebSocket closes, **before** deleting\n\t * this session from the shared `sessions` map.\n\t */\n\tpublic async invokeHandleClose(): Promise<void> {\n\t\tawait this.config.handleClose(this);\n\t}\n\n\tpublic async handleRawMessage(rawMessage: string): Promise<void> {\n\t\tif (this.wireFormat !== \"json\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected JSON frame in msgpack mode\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst deserialize = this.config.deserializeJson ?? JSON.parse;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = deserialize(rawMessage);\n\t\t} catch {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: invalid JSON\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, rawMessage);\n\t}\n\n\tpublic async handleBinaryMessage(buffer: ArrayBuffer): Promise<void> {\n\t\tif (this.wireFormat !== \"msgpack\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected binary frame in JSON mode\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = parseWirePayload(buffer, \"msgpack\");\n\t\t} catch (err) {\n\t\t\tawait this.reportValidationError(\n\t\t\t\terr instanceof Error ? err : new Error(\"socka: msgpack decode failed\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, buffer);\n\t}\n\n\tprivate async dispatchAfterParsed(\n\t\tparsed: unknown,\n\t\toriginalWire: unknown,\n\t): Promise<void> {\n\t\tlet decoded: ReturnType<typeof decodeSockaWire>;\n\t\ttry {\n\t\t\tdecoded = decodeSockaWire(parsed);\n\t\t} catch (err) {\n\t\t\tif (err instanceof SockaWireError) {\n\t\t\t\tawait this.reportValidationError(err, originalWire);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\n\t\tswitch (decoded.kind) {\n\t\t\tcase \"clientRequest\":\n\t\t\t\tawait this.dispatchClientRequest(decoded.frame, originalWire);\n\t\t\t\treturn;\n\t\t\tcase \"serverResponse\":\n\t\t\tcase \"serverError\":\n\t\t\tcase \"serverEvent\":\n\t\t\t\tawait this.reportValidationError(\n\t\t\t\t\tnew Error(\"socka: unexpected server-originated frame from client\"),\n\t\t\t\t\tparsed,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\texhaustiveGuard(decoded);\n\t\t}\n\t}\n\n\tprivate async dispatchClientRequest(\n\t\tframe: SockaClientRequestFrame,\n\t\t_originalWire: unknown,\n\t): Promise<void> {\n\t\tconst rpcName = frame.rpc;\n\t\tconst procedure = this.config.contract.calls[rpcName];\n\n\t\tif (!procedure) {\n\t\t\tconst errorFrame = encodeServerError(\n\t\t\t\tframe.id,\n\t\t\t\t`Unknown call: ${rpcName}`,\n\t\t\t\t{ rpc: rpcName },\n\t\t\t);\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedInput: unknown;\n\t\tif (procedure.input) {\n\t\t\ttry {\n\t\t\t\tvalidatedInput = await parseStandardSchema(procedure.input, frame.body);\n\t\t\t} catch (err) {\n\t\t\t\tconst msg =\n\t\t\t\t\terr instanceof Error ? err.message : \"Input validation failed\";\n\t\t\t\tconst errorFrame = encodeServerError(frame.id, msg, {\n\t\t\t\t\trpc: rpcName,\n\t\t\t\t});\n\t\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tif (procedure.input) {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\tinput: unknown,\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(validatedInput, this);\n\t\t\t} else {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(this);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.config.onHandlerError?.(err, rpcName, validatedInput, this);\n\t\t\tconst sockaErr =\n\t\t\t\terr instanceof SockaError\n\t\t\t\t\t? err\n\t\t\t\t\t: new SockaError(\n\t\t\t\t\t\t\terr instanceof Error ? err.message : \"Handler failed\",\n\t\t\t\t\t\t);\n\t\t\tconst errorFrame = encodeServerError(frame.id, sockaErr.message, {\n\t\t\t\trpc: rpcName,\n\t\t\t\tcode: sockaErr.code,\n\t\t\t\tdata: sockaErr.data,\n\t\t\t});\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tif (procedure.output === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedOutput: unknown;\n\t\ttry {\n\t\t\tvalidatedOutput = await parseStandardSchema(procedure.output, result);\n\t\t} catch (err) {\n\t\t\tconst msg =\n\t\t\t\terr instanceof Error ? err.message : \"Output validation failed\";\n\t\t\tconst errorFrame = encodeServerError(frame.id, msg, { rpc: rpcName });\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tconst responseFrame = encodeServerResponse(\n\t\t\tframe.id,\n\t\t\trpcName,\n\t\t\tvalidatedOutput,\n\t\t);\n\t\tthis.sendWireFrame(responseFrame);\n\t}\n\n\tprivate encodeOutgoing(frame: SockaWireFrame): string | Uint8Array {\n\t\treturn encodeSockaWire(\n\t\t\tframe,\n\t\t\tthis.wireFormat,\n\t\t\tthis.config.serializeJson ?? JSON.stringify,\n\t\t);\n\t}\n\n\tprivate sendWireFrame(frame: SockaWireFrame): void {\n\t\tif (this.websocket.readyState !== WebSocket.OPEN) {\n\t\t\treturn;\n\t\t}\n\t\tconst encoded = this.encodeOutgoing(frame);\n\t\tif (typeof encoded === \"string\") {\n\t\t\tthis.websocket.send(encoded);\n\t\t\treturn;\n\t\t}\n\t\tconst copy = new Uint8Array(encoded.byteLength);\n\t\tcopy.set(encoded);\n\t\tthis.websocket.send(copy.buffer);\n\t}\n\n\t/**\n\t * Send a server event frame (wire). Prefer {@link emitPush} so\n\t * payloads are validated against the contract.\n\t */\n\tpublic emitWireEvent(event: string, body: unknown): void {\n\t\tconst frame = encodeServerEvent(event, body);\n\t\tthis.sendWireFrame(frame);\n\t}\n\n\tpublic async emitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tthis.emitWireEvent(name, validated);\n\t}\n\n\tpublic async broadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf = false,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tbroadcastSockaEventToPeers(\n\t\t\tthis.sessions,\n\t\t\tthis,\n\t\t\tname,\n\t\t\tvalidated,\n\t\t\texcludeSelf,\n\t\t);\n\t}\n\n\tprivate async reportValidationError(\n\t\terror: unknown,\n\t\toriginalMessage: unknown,\n\t): Promise<void> {\n\t\tif (this.config.onValidationError) {\n\t\t\tawait this.config.onValidationError(error, originalMessage);\n\t\t} else {\n\t\t\tconsole.error(\"socka: validation error:\", error, originalMessage);\n\t\t}\n\t}\n}\n\n/**\n * Invoke {@link SockaWebSocketSessionConfig.onAttached} after the session is\n * registered in the shared map.\n */\nexport function runSockaSessionOnAttached<\n\tTContract extends SockaContractBound,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\tsession: SockaWebSocketSession<TContract, TData>,\n): void {\n\tconst cb = config.onAttached;\n\tif (!cb) return;\n\ttry {\n\t\tconst result = cb(session);\n\t\tvoid Promise.resolve(result).catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverOnAttached\",\n\t\t\t\terror,\n\t\t\t});\n\t\t});\n\t} catch (error) {\n\t\treportSockaError(config.reportError, { kind: \"serverOnAttached\", error });\n\t}\n}\n"]}
|
package/dist/do/index.d.ts
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
2
|
import { BaseSession, SessionEnv, BaseWebSocketDO } from '@firtoz/websocket-do';
|
|
3
3
|
import { c as SockaContractBound, y as SockaWireFormat, f as InferSockaHandlers, B as SockaReportError, h as InferSockaPushPayload } from '../socka-report-error-nTXJIzNb.js';
|
|
4
|
-
import {
|
|
4
|
+
import { i as SockaPushSession } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
5
5
|
import '@standard-schema/spec';
|
|
6
6
|
|
|
7
7
|
/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
|
|
8
|
-
type EmptySockaSessionData = Record<string, never>;
|
|
8
|
+
type EmptySockaSessionData$2 = Record<string, never>;
|
|
9
|
+
/**
|
|
10
|
+
* Durable Object (or test double) that owns the socka contract, shared
|
|
11
|
+
* {@link SockaDoSession} map, and per-connection config for
|
|
12
|
+
* {@link SockaDoSession}.
|
|
13
|
+
*/
|
|
14
|
+
interface SockaDoHost<TContract extends SockaContractBound, TData = EmptySockaSessionData$2, TEnv extends object = Cloudflare.Env, TSession extends SockaDoSession<TContract, TData, TEnv> = SockaDoSession<TContract, TData, TEnv>> {
|
|
15
|
+
readonly contract: TContract;
|
|
16
|
+
readonly sessions: Map<WebSocket, TSession>;
|
|
17
|
+
buildSockaSessionConfig(ctx: Context<{
|
|
18
|
+
Bindings: TEnv;
|
|
19
|
+
}> | undefined): SockaDoSessionConfigInput<TContract, TData, TEnv>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
|
|
23
|
+
type EmptySockaSessionData$1 = Record<string, never>;
|
|
9
24
|
type SockaDoOuterSession<TContract extends SockaContractBound, TData, TEnv extends object> = SockaDoSession<TContract, TData, TEnv>;
|
|
10
25
|
type SockaDoSessionCreateData<TData, TEnv extends object> = [TData] extends [
|
|
11
|
-
EmptySockaSessionData
|
|
26
|
+
EmptySockaSessionData$1
|
|
12
27
|
] ? {
|
|
13
28
|
createData?: (ctx: Context<{
|
|
14
29
|
Bindings: TEnv;
|
|
@@ -18,8 +33,7 @@ type SockaDoSessionCreateData<TData, TEnv extends object> = [TData] extends [
|
|
|
18
33
|
Bindings: TEnv;
|
|
19
34
|
}>) => TData;
|
|
20
35
|
};
|
|
21
|
-
type
|
|
22
|
-
contract: TContract;
|
|
36
|
+
type SockaDoSessionConfigFields<TContract extends SockaContractBound, TData, TEnv extends object> = {
|
|
23
37
|
/** Default `"json"`. Use `"msgpack"` for binary frames (must match client). */
|
|
24
38
|
wireFormat?: SockaWireFormat;
|
|
25
39
|
handlers: InferSockaHandlers<TContract, SockaDoOuterSession<TContract, TData, TEnv>>;
|
|
@@ -39,14 +53,22 @@ type SockaDoSessionConfig<TContract extends SockaContractBound, TData, TEnv exte
|
|
|
39
53
|
onAttached?: (session: SockaDoOuterSession<TContract, TData, TEnv>) => void | Promise<void>;
|
|
40
54
|
serializeJson?: (value: unknown) => string;
|
|
41
55
|
deserializeJson?: (raw: string) => unknown;
|
|
42
|
-
}
|
|
56
|
+
};
|
|
57
|
+
/** {@link SockaDoSessionConfig} without `contract` — supplied by the DO host. */
|
|
58
|
+
type SockaDoSessionConfigInput<TContract extends SockaContractBound, TData, TEnv extends object> = SockaDoSessionConfigFields<TContract, TData, TEnv> & SockaDoSessionCreateData<TData, TEnv>;
|
|
59
|
+
type SockaDoSessionConfig<TContract extends SockaContractBound, TData, TEnv extends object> = {
|
|
60
|
+
contract: TContract;
|
|
61
|
+
} & SockaDoSessionConfigInput<TContract, TData, TEnv>;
|
|
43
62
|
/**
|
|
44
63
|
* Durable Object WebSocket session driven by a socka contract.
|
|
45
64
|
* Dispatches client requests to typed handler functions, validates
|
|
46
65
|
* input/output via Standard Schema, and auto-sends response/error frames.
|
|
47
66
|
*/
|
|
48
|
-
declare class SockaDoSession<TContract extends SockaContractBound, TData = EmptySockaSessionData, TEnv extends object = Cloudflare.Env> extends BaseSession<TData, unknown, unknown, TEnv> implements SockaPushSession<TContract> {
|
|
67
|
+
declare class SockaDoSession<TContract extends SockaContractBound, TData = EmptySockaSessionData$1, TEnv extends object = Cloudflare.Env> extends BaseSession<TData, unknown, unknown, TEnv> implements SockaPushSession<TContract> {
|
|
49
68
|
private socka;
|
|
69
|
+
constructor(websocket: WebSocket, host: SockaDoHost<TContract, TData, TEnv, SockaDoSession<TContract, TData, TEnv>>, attachCtx?: Context<{
|
|
70
|
+
Bindings: TEnv;
|
|
71
|
+
}> | undefined);
|
|
50
72
|
constructor(websocket: WebSocket, sessions: Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>, config: SockaDoSessionConfig<TContract, TData, TEnv>);
|
|
51
73
|
handleRawMessage(rawMessage: string): Promise<void>;
|
|
52
74
|
emitWireEvent(event: string, body: unknown): void;
|
|
@@ -72,26 +94,62 @@ declare class SockaDoSession<TContract extends SockaContractBound, TData = Empty
|
|
|
72
94
|
}): boolean;
|
|
73
95
|
}
|
|
74
96
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* `SockaDoSession<typeof myContract, …>` is not a subtype of
|
|
79
|
-
* `SockaDoSession<SockaContractBound, …>` for `extends` even when `myContract` is
|
|
80
|
-
* a valid `SockaContractBound`. Use a concrete `SockaDoSession` subclass as `TSession`.
|
|
81
|
-
*/
|
|
82
|
-
TSession extends SockaDoSession<any, any, TEnv>> = {
|
|
83
|
-
createSockaSession: (ctx: Context<{
|
|
84
|
-
Bindings: TEnv;
|
|
85
|
-
}> | undefined, websocket: WebSocket) => TSession | Promise<TSession>;
|
|
97
|
+
/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
|
|
98
|
+
type EmptySockaSessionData = Record<string, never>;
|
|
99
|
+
type SockaWebSocketDOOptions = {
|
|
86
100
|
/** Same as `pairServerWebSocketAcceptOptions` on `BaseWebSocketDOOptions` from `@firtoz/websocket-do`. */
|
|
87
101
|
pairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;
|
|
88
102
|
};
|
|
89
103
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
104
|
+
* Low-level DO base when {@link createSockaSession} returns a custom
|
|
105
|
+
* {@link SockaDoSession} subclass. Most apps extend {@link SockaWebSocketDO}
|
|
106
|
+
* instead (default session type, three type parameters).
|
|
107
|
+
*/
|
|
108
|
+
declare abstract class SockaWebSocketDOBase<TContract extends SockaContractBound, TData = EmptySockaSessionData, TSession extends SockaDoSession<TContract, TData, any> = SockaDoSession<TContract, TData, Cloudflare.Env>, TEnv extends SessionEnv<TSession> = SessionEnv<TSession>> extends BaseWebSocketDO<TSession, TEnv> {
|
|
109
|
+
protected abstract readonly contract: TContract;
|
|
110
|
+
constructor(ctx: DurableObjectState, env: TEnv, options?: SockaWebSocketDOOptions);
|
|
111
|
+
/**
|
|
112
|
+
* Per-connection socka config (handlers, `createData`, lifecycle hooks).
|
|
113
|
+
* `contract` comes from {@link contract} on this host — do not repeat it here.
|
|
114
|
+
*
|
|
115
|
+
* Called on each new attach and on hibernation resume (`ctx` is `undefined`
|
|
116
|
+
* on resume). **`createData`** runs only on fresh upgrade via
|
|
117
|
+
* {@link BaseSession.startFresh}, not on {@link BaseSession.resume}.
|
|
118
|
+
*/
|
|
119
|
+
protected abstract buildSockaSessionConfig(ctx: Context<{
|
|
120
|
+
Bindings: TEnv;
|
|
121
|
+
}> | undefined): SockaDoSessionConfigInput<TContract, TData, TEnv>;
|
|
122
|
+
/**
|
|
123
|
+
* Factory for each connected WebSocket. Default: {@link SockaDoSession} wired
|
|
124
|
+
* to this host. Override when you need a {@link SockaDoSession} subclass.
|
|
125
|
+
*/
|
|
126
|
+
protected createSockaSession(ctx: Context<{
|
|
127
|
+
Bindings: TEnv;
|
|
128
|
+
}> | undefined, websocket: WebSocket): TSession | Promise<TSession>;
|
|
129
|
+
/**
|
|
130
|
+
* Broadcast a contract-typed push to **every** connected session in this DO.
|
|
131
|
+
*
|
|
132
|
+
* Use from HTTP handlers, alarms, or other code paths with no originating
|
|
133
|
+
* WebSocket session. No-op when the room is empty.
|
|
134
|
+
*/
|
|
135
|
+
protected broadcastPushToAll<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Durable Object base class for socka WebSocket rooms.
|
|
139
|
+
*
|
|
140
|
+
* Subclasses declare {@link contract} and {@link buildSockaSessionConfig}; the
|
|
141
|
+
* base wires WebSocket upgrade and hibernation resume to
|
|
142
|
+
* `new SockaDoSession(websocket, host)`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* class ChatRoomDO extends SockaWebSocketDO<typeof chatContract, SessionData, Env> {
|
|
147
|
+
* protected readonly contract = chatContract;
|
|
148
|
+
* protected buildSockaSessionConfig(ctx) { … }
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
92
151
|
*/
|
|
93
|
-
declare abstract class SockaWebSocketDO<
|
|
94
|
-
constructor(ctx: DurableObjectState, env: TEnv, options: SockaWebSocketDOOptions<TEnv, TSession>);
|
|
152
|
+
declare abstract class SockaWebSocketDO<TContract extends SockaContractBound, TData, TEnv extends object> extends SockaWebSocketDOBase<TContract, TData, SockaDoSession<TContract, TData, TEnv>, TEnv> {
|
|
95
153
|
}
|
|
96
154
|
|
|
97
155
|
/**
|
|
@@ -104,4 +162,4 @@ declare function toErrorReply(id: string, error: string): {
|
|
|
104
162
|
error: string;
|
|
105
163
|
};
|
|
106
164
|
|
|
107
|
-
export { SockaDoSession, type SockaDoSessionConfig, SockaWebSocketDO, type SockaWebSocketDOOptions, toErrorReply };
|
|
165
|
+
export { type SockaDoHost, SockaDoSession, type SockaDoSessionConfig, type SockaDoSessionConfigInput, SockaWebSocketDO, SockaWebSocketDOBase, type SockaWebSocketDOOptions, toErrorReply };
|
package/dist/do/index.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { SockaWebSocketSession } from '../chunk-
|
|
1
|
+
import { SockaWebSocketSession, broadcastContractPushToAll } from '../chunk-TTXY7O5P.js';
|
|
2
2
|
import { reportSockaError } from '../chunk-IFIGKR3W.js';
|
|
3
3
|
import { BaseSession, BaseWebSocketDO } from '@firtoz/websocket-do';
|
|
4
4
|
|
|
5
|
+
// src/do/SockaDoHost.ts
|
|
6
|
+
function hasSockaDoHostShape(value) {
|
|
7
|
+
return "contract" in value && "sessions" in value && "buildSockaSessionConfig" in value && value.sessions instanceof Map && typeof value.buildSockaSessionConfig === "function";
|
|
8
|
+
}
|
|
9
|
+
function isSockaDoHost(value) {
|
|
10
|
+
return !(value instanceof Map) && hasSockaDoHostShape(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/do/SockaDoSession.ts
|
|
5
14
|
function runSockaDoSessionOnAttached(config, session) {
|
|
6
15
|
const cb = config.onAttached;
|
|
7
16
|
if (!cb) return;
|
|
@@ -31,8 +40,28 @@ function wrapHandlersForInnerSockaEngine(contract, userHandlers, outer) {
|
|
|
31
40
|
}
|
|
32
41
|
return out;
|
|
33
42
|
}
|
|
43
|
+
function resolveSockaDoSessionInit(sessionsOrHost, configOrAttachCtx) {
|
|
44
|
+
if (isSockaDoHost(sessionsOrHost)) {
|
|
45
|
+
const attachCtx = configOrAttachCtx;
|
|
46
|
+
return {
|
|
47
|
+
sessions: sessionsOrHost.sessions,
|
|
48
|
+
config: {
|
|
49
|
+
contract: sessionsOrHost.contract,
|
|
50
|
+
...sessionsOrHost.buildSockaSessionConfig(attachCtx)
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
sessions: sessionsOrHost,
|
|
56
|
+
config: configOrAttachCtx
|
|
57
|
+
};
|
|
58
|
+
}
|
|
34
59
|
var SockaDoSession = class extends BaseSession {
|
|
35
|
-
constructor(websocket,
|
|
60
|
+
constructor(websocket, sessionsOrHost, configOrAttachCtx) {
|
|
61
|
+
const { sessions, config } = resolveSockaDoSessionInit(
|
|
62
|
+
sessionsOrHost,
|
|
63
|
+
configOrAttachCtx
|
|
64
|
+
);
|
|
36
65
|
const wireFormat = config.wireFormat ?? "json";
|
|
37
66
|
super(
|
|
38
67
|
websocket,
|
|
@@ -126,13 +155,40 @@ var SockaDoSession = class extends BaseSession {
|
|
|
126
155
|
return this.peerCount(options) > 0;
|
|
127
156
|
}
|
|
128
157
|
};
|
|
129
|
-
var
|
|
158
|
+
var SockaWebSocketDOBase = class extends BaseWebSocketDO {
|
|
130
159
|
constructor(ctx, env, options) {
|
|
131
160
|
super(ctx, env, {
|
|
132
|
-
createSession: (sessionCtx, websocket) =>
|
|
133
|
-
pairServerWebSocketAcceptOptions: options
|
|
161
|
+
createSession: (sessionCtx, websocket) => this.createSockaSession(sessionCtx, websocket),
|
|
162
|
+
pairServerWebSocketAcceptOptions: options?.pairServerWebSocketAcceptOptions
|
|
134
163
|
});
|
|
135
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Factory for each connected WebSocket. Default: {@link SockaDoSession} wired
|
|
167
|
+
* to this host. Override when you need a {@link SockaDoSession} subclass.
|
|
168
|
+
*/
|
|
169
|
+
createSockaSession(ctx, websocket) {
|
|
170
|
+
const host = {
|
|
171
|
+
contract: this.contract,
|
|
172
|
+
sessions: this.sessions,
|
|
173
|
+
buildSockaSessionConfig: (attachCtx) => this.buildSockaSessionConfig(attachCtx)
|
|
174
|
+
};
|
|
175
|
+
return new SockaDoSession(
|
|
176
|
+
websocket,
|
|
177
|
+
host,
|
|
178
|
+
ctx
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Broadcast a contract-typed push to **every** connected session in this DO.
|
|
183
|
+
*
|
|
184
|
+
* Use from HTTP handlers, alarms, or other code paths with no originating
|
|
185
|
+
* WebSocket session. No-op when the room is empty.
|
|
186
|
+
*/
|
|
187
|
+
broadcastPushToAll(name, body) {
|
|
188
|
+
return broadcastContractPushToAll(this.sessions, this.contract, name, body);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var SockaWebSocketDO = class extends SockaWebSocketDOBase {
|
|
136
192
|
};
|
|
137
193
|
|
|
138
194
|
// src/do/dispatch.ts
|
|
@@ -140,6 +196,6 @@ function toErrorReply(id, error) {
|
|
|
140
196
|
return { type: "error", id, error };
|
|
141
197
|
}
|
|
142
198
|
|
|
143
|
-
export { SockaDoSession, SockaWebSocketDO, toErrorReply };
|
|
199
|
+
export { SockaDoSession, SockaWebSocketDO, SockaWebSocketDOBase, toErrorReply };
|
|
144
200
|
//# sourceMappingURL=index.js.map
|
|
145
201
|
//# sourceMappingURL=index.js.map
|
package/dist/do/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/do/SockaDoSession.ts","../../src/do/SockaWebSocketDO.ts","../../src/do/dispatch.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Context } from \"hono\";\nimport { BaseSession } from \"@firtoz/websocket-do\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContractBound,\n\tInferSockaHandlers,\n} from \"../core/contract\";\nimport {\n\tSockaWebSocketSession,\n\ttype SockaPushSession,\n\ttype SockaWebSocketSessionConfigLoose,\n} from \"../server/SockaWebSocketSession\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport type { SockaReportError } from \"../core/socka-report-error\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\ntype SockaDoOuterSession<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = import(\"./SockaDoSession\").SockaDoSession<TContract, TData, TEnv>;\n\ntype SockaDoSessionCreateData<TData, TEnv extends object> = [TData] extends [\n\tEmptySockaSessionData,\n]\n\t? {\n\t\t\tcreateData?: (ctx: Context<{ Bindings: TEnv }>) => TData;\n\t\t}\n\t: {\n\t\t\tcreateData: (ctx: Context<{ Bindings: TEnv }>) => TData;\n\t\t};\n\nexport type SockaDoSessionConfig<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = {\n\tcontract: TContract;\n\t/** Default `\"json\"`. Use `\"msgpack\"` for binary frames (must match client). */\n\twireFormat?: SockaWireFormat;\n\thandlers: InferSockaHandlers<\n\t\tTContract,\n\t\tSockaDoOuterSession<TContract, TData, TEnv>\n\t>;\n\thandleClose: (\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => Promise<void>;\n\tonHandlerError?: (\n\t\terror: unknown,\n\t\trpcName: string,\n\t\tinput: unknown,\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => void;\n\tonValidationError?: (\n\t\terror: unknown,\n\t\toriginalMessage: unknown,\n\t) => Promise<void>;\n\t/**\n\t * Optional sink for non-RPC failures (e.g. `onAttached`). Defaults to\n\t * `console.error`; see `SockaReportError` in `@firtoz/socka/core`.\n\t */\n\treportError?: (event: SockaReportError) => void;\n\t/**\n\t * Called after this session is registered in the DO `sessions` map (next\n\t * microtask). Use for join broadcasts and other logic that must run only\n\t * when peers can see this connection.\n\t */\n\tonAttached?: (\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => void | Promise<void>;\n\tserializeJson?: (value: unknown) => string;\n\tdeserializeJson?: (raw: string) => unknown;\n} & SockaDoSessionCreateData<TData, TEnv>;\n\nfunction runSockaDoSessionOnAttached<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n>(\n\tconfig: SockaDoSessionConfig<TContract, TData, TEnv>,\n\tsession: SockaDoSession<TContract, TData, TEnv>,\n): void {\n\tconst cb = config.onAttached;\n\tif (!cb) return;\n\ttry {\n\t\tconst result = cb(session);\n\t\tvoid Promise.resolve(result).catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverOnAttached\",\n\t\t\t\terror,\n\t\t\t});\n\t\t});\n\t} catch (error) {\n\t\treportSockaError(config.reportError, { kind: \"serverOnAttached\", error });\n\t}\n}\n\nfunction wrapHandlersForInnerSockaEngine<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n>(\n\tcontract: TContract,\n\tuserHandlers: InferSockaHandlers<\n\t\tTContract,\n\t\tSockaDoSession<TContract, TData, TEnv>\n\t>,\n\touter: SockaDoSession<TContract, TData, TEnv>,\n): InferSockaHandlers<\n\tTContract,\n\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n> {\n\tconst calls = contract.calls;\n\tconst out: Record<\n\t\tstring,\n\t\t| ((\n\t\t\t\tinput: unknown,\n\t\t\t\tinner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t ) => unknown | Promise<unknown>)\n\t\t| ((\n\t\t\t\tinner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t ) => unknown | Promise<unknown>)\n\t> = {};\n\n\tfor (const key of Object.keys(calls) as Array<keyof typeof calls & string>) {\n\t\tconst proc = calls[key];\n\t\tconst userFn = userHandlers[key as keyof typeof userHandlers];\n\t\tif (proc.input) {\n\t\t\tout[key] = (\n\t\t\t\tinput,\n\t\t\t\t_inner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t\t) =>\n\t\t\t\t(\n\t\t\t\t\tuserFn as (\n\t\t\t\t\t\ti: unknown,\n\t\t\t\t\t\ts: SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t) => unknown | Promise<unknown>\n\t\t\t\t)(input, outer);\n\t\t} else {\n\t\t\tout[key] = (\n\t\t\t\t_inner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t\t) =>\n\t\t\t\t(\n\t\t\t\t\tuserFn as (\n\t\t\t\t\t\ts: SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t) => unknown | Promise<unknown>\n\t\t\t\t)(outer);\n\t\t}\n\t}\n\n\treturn out as InferSockaHandlers<\n\t\tTContract,\n\t\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n\t>;\n}\n\n/**\n * Durable Object WebSocket session driven by a socka contract.\n * Dispatches client requests to typed handler functions, validates\n * input/output via Standard Schema, and auto-sends response/error frames.\n */\nexport class SockaDoSession<\n\t\tTContract extends SockaContractBound,\n\t\tTData = EmptySockaSessionData,\n\t\tTEnv extends object = Cloudflare.Env,\n\t>\n\textends BaseSession<TData, unknown, unknown, TEnv>\n\timplements SockaPushSession<TContract>\n{\n\tprivate socka!: SockaWebSocketSession<TContract, EmptySockaSessionData>;\n\n\tconstructor(\n\t\twebsocket: WebSocket,\n\t\tsessions: Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>,\n\t\tconfig: SockaDoSessionConfig<TContract, TData, TEnv>,\n\t) {\n\t\tconst wireFormat = config.wireFormat ?? \"json\";\n\t\tsuper(\n\t\t\twebsocket,\n\t\t\tsessions as Map<WebSocket, BaseSession<TData, unknown, unknown, TEnv>>,\n\t\t\t{\n\t\t\t\tcreateData:\n\t\t\t\t\tconfig.createData ??\n\t\t\t\t\t((_ctx: Context<{ Bindings: TEnv }>) => ({}) as TData),\n\t\t\t\thandleMessage: async () => {\n\t\t\t\t\t// Raw message handling goes through handleRawMessage / handleBufferMessage\n\t\t\t\t},\n\t\t\t\thandleBufferMessage: async (message) => {\n\t\t\t\t\tawait this.socka.handleBinaryMessage(message);\n\t\t\t\t},\n\t\t\t\thandleClose: async (baseSession) => {\n\t\t\t\t\tawait config.handleClose(\n\t\t\t\t\t\tbaseSession as SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t\tconst sockaConfig: SockaWebSocketSessionConfigLoose<\n\t\t\tTContract,\n\t\t\tEmptySockaSessionData\n\t\t> = {\n\t\t\tstrictUpgradeRequest: false,\n\t\t\tcontract: config.contract,\n\t\t\twireFormat,\n\t\t\thandlers: wrapHandlersForInnerSockaEngine(\n\t\t\t\tconfig.contract,\n\t\t\t\tconfig.handlers,\n\t\t\t\tthis,\n\t\t\t),\n\t\t\thandleClose: async () => {\n\t\t\t\t// Outer DO lifecycle uses SockaDoSessionConfig.handleClose; inner engine no-op.\n\t\t\t},\n\t\t\tonHandlerError: config.onHandlerError\n\t\t\t\t? (err, rpcName, input, _inner) => {\n\t\t\t\t\t\tconfig.onHandlerError?.(err, rpcName, input, this);\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t\tonValidationError: config.onValidationError,\n\t\t\tserializeJson: config.serializeJson,\n\t\t\tdeserializeJson: config.deserializeJson,\n\t\t};\n\t\tthis.socka = new SockaWebSocketSession(\n\t\t\twebsocket,\n\t\t\tsessions as unknown as Map<\n\t\t\t\tWebSocket,\n\t\t\t\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n\t\t\t>,\n\t\t\tsockaConfig,\n\t\t);\n\t\t// Defer past the outer `await createSession()` continuation so\n\t\t// `BaseSession.startFresh` has run and `session.data` exists (single\n\t\t// `queueMicrotask` runs before that continuation).\n\t\tqueueMicrotask(() => {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\trunSockaDoSessionOnAttached(config, this);\n\t\t\t});\n\t\t});\n\t}\n\n\tpublic async handleRawMessage(rawMessage: string): Promise<void> {\n\t\treturn this.socka.handleRawMessage(rawMessage);\n\t}\n\n\tpublic emitWireEvent(event: string, body: unknown): void {\n\t\tthis.socka.emitWireEvent(event, body);\n\t}\n\n\tpublic emitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\treturn this.socka.emitPush(name, body);\n\t}\n\n\tpublic broadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf = false,\n\t): Promise<void> {\n\t\treturn this.socka.broadcastPush(name, body, excludeSelf);\n\t}\n\n\t/**\n\t * {@link SockaWebSocketSession.listPeers} for this Durable Object room.\n\t */\n\tpublic listPeers(options?: { excludeSelf?: boolean }): TData[] {\n\t\tconst out: TData[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(s.data);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Like {@link listPeers} but maps each peer {@link SockaDoSession}.\n\t */\n\tpublic listPeersWith<R>(\n\t\tmap: (session: SockaDoSession<TContract, TData, TEnv>) => R,\n\t\toptions?: { excludeSelf?: boolean },\n\t): R[] {\n\t\tconst out: R[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(map(s as SockaDoSession<TContract, TData, TEnv>));\n\t\t}\n\t\treturn out;\n\t}\n\n\tpublic peerCount(options?: { excludeSelf?: boolean }): number {\n\t\tlet n = 0;\n\t\tfor (const [ws] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tn += 1;\n\t\t}\n\t\treturn n;\n\t}\n\n\tpublic hasPeers(options?: { excludeSelf?: boolean }): boolean {\n\t\treturn this.peerCount(options) > 0;\n\t}\n}\n","import type { Context } from \"hono\";\nimport type { SessionEnv } from \"@firtoz/websocket-do\";\nimport { BaseWebSocketDO } from \"@firtoz/websocket-do\";\nimport type { SockaContractBound } from \"../core/contract\";\nimport type { SockaDoSession } from \"./SockaDoSession\";\n\nexport type SockaWebSocketDOOptions<\n\tTEnv extends object,\n\t/**\n\t * `any` for the contract type parameter: `SockaDoSession` is invariant, so\n\t * `SockaDoSession<typeof myContract, …>` is not a subtype of\n\t * `SockaDoSession<SockaContractBound, …>` for `extends` even when `myContract` is\n\t * a valid `SockaContractBound`. Use a concrete `SockaDoSession` subclass as `TSession`.\n\t */\n\tTSession extends SockaDoSession<\n\t\t// biome-ignore lint/suspicious/noExplicitAny: SockaDoSession is invariant in TContract\n\t\tany,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany,\n\t\tTEnv\n\t>,\n> = {\n\tcreateSockaSession: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t) => TSession | Promise<TSession>;\n\t/** Same as `pairServerWebSocketAcceptOptions` on `BaseWebSocketDOOptions` from `@firtoz/websocket-do`. */\n\tpairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;\n};\n\n/**\n * Durable Object base class for WebSocket apps using {@link SockaDoSession}\n * (Standard Schema contract-driven).\n */\nexport abstract class SockaWebSocketDO<\n\tTSession extends SockaDoSession<\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany\n\t> = SockaDoSession<\n\t\tSockaContractBound,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: session family type erasure\n\t\tany\n\t>,\n\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n> extends BaseWebSocketDO<TSession, TEnv> {\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\toptions: SockaWebSocketDOOptions<TEnv, TSession>,\n\t) {\n\t\tsuper(ctx, env, {\n\t\t\tcreateSession: (sessionCtx, websocket) =>\n\t\t\t\toptions.createSockaSession(sessionCtx, websocket),\n\t\t\tpairServerWebSocketAcceptOptions:\n\t\t\t\toptions.pairServerWebSocketAcceptOptions,\n\t\t});\n\t}\n}\n","/**\n * Shared error reply shape for correlate-by-id RPC when the handler throws.\n * Narrow at the call site to your server message union if needed.\n */\nexport function toErrorReply(\n\tid: string,\n\terror: string,\n): {\n\ttype: \"error\";\n\tid: string;\n\terror: string;\n} {\n\treturn { type: \"error\", id, error };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/do/SockaDoHost.ts","../../src/do/SockaDoSession.ts","../../src/do/SockaWebSocketDO.ts","../../src/do/dispatch.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Context } from \"hono\";\nimport type { SockaContractBound } from \"../core/contract\";\nimport type {\n\tSockaDoSession,\n\tSockaDoSessionConfigInput,\n} from \"./SockaDoSession\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\n/**\n * Durable Object (or test double) that owns the socka contract, shared\n * {@link SockaDoSession} map, and per-connection config for\n * {@link SockaDoSession}.\n */\nexport interface SockaDoHost<\n\tTContract extends SockaContractBound,\n\tTData = EmptySockaSessionData,\n\tTEnv extends object = Cloudflare.Env,\n\tTSession extends SockaDoSession<TContract, TData, TEnv> = SockaDoSession<\n\t\tTContract,\n\t\tTData,\n\t\tTEnv\n\t>,\n> {\n\treadonly contract: TContract;\n\treadonly sessions: Map<WebSocket, TSession>;\n\tbuildSockaSessionConfig(\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t): SockaDoSessionConfigInput<TContract, TData, TEnv>;\n}\n\nfunction hasSockaDoHostShape(value: object): value is {\n\tcontract: SockaContractBound;\n\tsessions: Map<WebSocket, unknown>;\n\tbuildSockaSessionConfig: (ctx: unknown) => unknown;\n} {\n\treturn (\n\t\t\"contract\" in value &&\n\t\t\"sessions\" in value &&\n\t\t\"buildSockaSessionConfig\" in value &&\n\t\tvalue.sessions instanceof Map &&\n\t\ttypeof value.buildSockaSessionConfig === \"function\"\n\t);\n}\n\n/** @internal */\nexport function isSockaDoHost<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n\tTSession extends SockaDoSession<TContract, TData, TEnv>,\n>(\n\tvalue:\n\t\t| Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>\n\t\t| SockaDoHost<TContract, TData, TEnv, TSession>,\n): value is SockaDoHost<TContract, TData, TEnv, TSession> {\n\treturn !(value instanceof Map) && hasSockaDoHostShape(value);\n}\n","import type { Context } from \"hono\";\nimport { BaseSession } from \"@firtoz/websocket-do\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContractBound,\n\tInferSockaHandlers,\n} from \"../core/contract\";\nimport {\n\tSockaWebSocketSession,\n\ttype SockaPushSession,\n\ttype SockaWebSocketSessionConfigLoose,\n} from \"../server/SockaWebSocketSession\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport type { SockaReportError } from \"../core/socka-report-error\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\nimport type { SockaDoHost } from \"./SockaDoHost\";\nimport { isSockaDoHost } from \"./SockaDoHost\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\ntype SockaDoOuterSession<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = import(\"./SockaDoSession\").SockaDoSession<TContract, TData, TEnv>;\n\ntype SockaDoSessionCreateData<TData, TEnv extends object> = [TData] extends [\n\tEmptySockaSessionData,\n]\n\t? {\n\t\t\tcreateData?: (ctx: Context<{ Bindings: TEnv }>) => TData;\n\t\t}\n\t: {\n\t\t\tcreateData: (ctx: Context<{ Bindings: TEnv }>) => TData;\n\t\t};\n\ntype SockaDoSessionConfigFields<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = {\n\t/** Default `\"json\"`. Use `\"msgpack\"` for binary frames (must match client). */\n\twireFormat?: SockaWireFormat;\n\thandlers: InferSockaHandlers<\n\t\tTContract,\n\t\tSockaDoOuterSession<TContract, TData, TEnv>\n\t>;\n\thandleClose: (\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => Promise<void>;\n\tonHandlerError?: (\n\t\terror: unknown,\n\t\trpcName: string,\n\t\tinput: unknown,\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => void;\n\tonValidationError?: (\n\t\terror: unknown,\n\t\toriginalMessage: unknown,\n\t) => Promise<void>;\n\t/**\n\t * Optional sink for non-RPC failures (e.g. `onAttached`). Defaults to\n\t * `console.error`; see `SockaReportError` in `@firtoz/socka/core`.\n\t */\n\treportError?: (event: SockaReportError) => void;\n\t/**\n\t * Called after this session is registered in the DO `sessions` map (next\n\t * microtask). Use for join broadcasts and other logic that must run only\n\t * when peers can see this connection.\n\t */\n\tonAttached?: (\n\t\tsession: SockaDoOuterSession<TContract, TData, TEnv>,\n\t) => void | Promise<void>;\n\tserializeJson?: (value: unknown) => string;\n\tdeserializeJson?: (raw: string) => unknown;\n};\n\n/** {@link SockaDoSessionConfig} without `contract` — supplied by the DO host. */\nexport type SockaDoSessionConfigInput<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = SockaDoSessionConfigFields<TContract, TData, TEnv> &\n\tSockaDoSessionCreateData<TData, TEnv>;\n\nexport type SockaDoSessionConfig<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> = {\n\tcontract: TContract;\n} & SockaDoSessionConfigInput<TContract, TData, TEnv>;\n\nfunction runSockaDoSessionOnAttached<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n>(\n\tconfig: SockaDoSessionConfig<TContract, TData, TEnv>,\n\tsession: SockaDoSession<TContract, TData, TEnv>,\n): void {\n\tconst cb = config.onAttached;\n\tif (!cb) return;\n\ttry {\n\t\tconst result = cb(session);\n\t\tvoid Promise.resolve(result).catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverOnAttached\",\n\t\t\t\terror,\n\t\t\t});\n\t\t});\n\t} catch (error) {\n\t\treportSockaError(config.reportError, { kind: \"serverOnAttached\", error });\n\t}\n}\n\nfunction wrapHandlersForInnerSockaEngine<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n>(\n\tcontract: TContract,\n\tuserHandlers: InferSockaHandlers<\n\t\tTContract,\n\t\tSockaDoSession<TContract, TData, TEnv>\n\t>,\n\touter: SockaDoSession<TContract, TData, TEnv>,\n): InferSockaHandlers<\n\tTContract,\n\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n> {\n\tconst calls = contract.calls;\n\tconst out: Record<\n\t\tstring,\n\t\t| ((\n\t\t\t\tinput: unknown,\n\t\t\t\tinner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t ) => unknown | Promise<unknown>)\n\t\t| ((\n\t\t\t\tinner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t ) => unknown | Promise<unknown>)\n\t> = {};\n\n\tfor (const key of Object.keys(calls) as Array<keyof typeof calls & string>) {\n\t\tconst proc = calls[key];\n\t\tconst userFn = userHandlers[key as keyof typeof userHandlers];\n\t\tif (proc.input) {\n\t\t\tout[key] = (\n\t\t\t\tinput,\n\t\t\t\t_inner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t\t) =>\n\t\t\t\t(\n\t\t\t\t\tuserFn as (\n\t\t\t\t\t\ti: unknown,\n\t\t\t\t\t\ts: SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t) => unknown | Promise<unknown>\n\t\t\t\t)(input, outer);\n\t\t} else {\n\t\t\tout[key] = (\n\t\t\t\t_inner: SockaWebSocketSession<TContract, EmptySockaSessionData>,\n\t\t\t) =>\n\t\t\t\t(\n\t\t\t\t\tuserFn as (\n\t\t\t\t\t\ts: SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t) => unknown | Promise<unknown>\n\t\t\t\t)(outer);\n\t\t}\n\t}\n\n\treturn out as InferSockaHandlers<\n\t\tTContract,\n\t\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n\t>;\n}\n\nfunction resolveSockaDoSessionInit<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n\tTSession extends SockaDoSession<TContract, TData, TEnv>,\n>(\n\tsessionsOrHost:\n\t\t| Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>\n\t\t| SockaDoHost<TContract, TData, TEnv, TSession>,\n\tconfigOrAttachCtx?:\n\t\t| SockaDoSessionConfig<TContract, TData, TEnv>\n\t\t| Context<{ Bindings: TEnv }>,\n): {\n\tsessions: Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>;\n\tconfig: SockaDoSessionConfig<TContract, TData, TEnv>;\n} {\n\tif (isSockaDoHost(sessionsOrHost)) {\n\t\tconst attachCtx = configOrAttachCtx as\n\t\t\t| Context<{ Bindings: TEnv }>\n\t\t\t| undefined;\n\t\treturn {\n\t\t\tsessions: sessionsOrHost.sessions as Map<\n\t\t\t\tWebSocket,\n\t\t\t\tSockaDoSession<TContract, TData, TEnv>\n\t\t\t>,\n\t\t\tconfig: {\n\t\t\t\tcontract: sessionsOrHost.contract,\n\t\t\t\t...sessionsOrHost.buildSockaSessionConfig(attachCtx),\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tsessions: sessionsOrHost,\n\t\tconfig: configOrAttachCtx as SockaDoSessionConfig<TContract, TData, TEnv>,\n\t};\n}\n\n/**\n * Durable Object WebSocket session driven by a socka contract.\n * Dispatches client requests to typed handler functions, validates\n * input/output via Standard Schema, and auto-sends response/error frames.\n */\nexport class SockaDoSession<\n\t\tTContract extends SockaContractBound,\n\t\tTData = EmptySockaSessionData,\n\t\tTEnv extends object = Cloudflare.Env,\n\t>\n\textends BaseSession<TData, unknown, unknown, TEnv>\n\timplements SockaPushSession<TContract>\n{\n\tprivate socka!: SockaWebSocketSession<TContract, EmptySockaSessionData>;\n\n\tconstructor(\n\t\twebsocket: WebSocket,\n\t\thost: SockaDoHost<\n\t\t\tTContract,\n\t\t\tTData,\n\t\t\tTEnv,\n\t\t\tSockaDoSession<TContract, TData, TEnv>\n\t\t>,\n\t\tattachCtx?: Context<{ Bindings: TEnv }> | undefined,\n\t);\n\tconstructor(\n\t\twebsocket: WebSocket,\n\t\tsessions: Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>,\n\t\tconfig: SockaDoSessionConfig<TContract, TData, TEnv>,\n\t);\n\tconstructor(\n\t\twebsocket: WebSocket,\n\t\tsessionsOrHost:\n\t\t\t| Map<WebSocket, SockaDoSession<TContract, TData, TEnv>>\n\t\t\t| SockaDoHost<\n\t\t\t\t\tTContract,\n\t\t\t\t\tTData,\n\t\t\t\t\tTEnv,\n\t\t\t\t\tSockaDoSession<TContract, TData, TEnv>\n\t\t\t >,\n\t\tconfigOrAttachCtx?:\n\t\t\t| SockaDoSessionConfig<TContract, TData, TEnv>\n\t\t\t| Context<{ Bindings: TEnv }>,\n\t) {\n\t\tconst { sessions, config } = resolveSockaDoSessionInit(\n\t\t\tsessionsOrHost,\n\t\t\tconfigOrAttachCtx,\n\t\t);\n\t\tconst wireFormat = config.wireFormat ?? \"json\";\n\t\tsuper(\n\t\t\twebsocket,\n\t\t\tsessions as Map<WebSocket, BaseSession<TData, unknown, unknown, TEnv>>,\n\t\t\t{\n\t\t\t\tcreateData:\n\t\t\t\t\tconfig.createData ??\n\t\t\t\t\t((_ctx: Context<{ Bindings: TEnv }>) => ({}) as TData),\n\t\t\t\thandleMessage: async () => {\n\t\t\t\t\t// Raw message handling goes through handleRawMessage / handleBufferMessage\n\t\t\t\t},\n\t\t\t\thandleBufferMessage: async (message) => {\n\t\t\t\t\tawait this.socka.handleBinaryMessage(message);\n\t\t\t\t},\n\t\t\t\thandleClose: async (baseSession) => {\n\t\t\t\t\tawait config.handleClose(\n\t\t\t\t\t\tbaseSession as SockaDoSession<TContract, TData, TEnv>,\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t\tconst sockaConfig: SockaWebSocketSessionConfigLoose<\n\t\t\tTContract,\n\t\t\tEmptySockaSessionData\n\t\t> = {\n\t\t\tstrictUpgradeRequest: false,\n\t\t\tcontract: config.contract,\n\t\t\twireFormat,\n\t\t\thandlers: wrapHandlersForInnerSockaEngine(\n\t\t\t\tconfig.contract,\n\t\t\t\tconfig.handlers,\n\t\t\t\tthis,\n\t\t\t),\n\t\t\thandleClose: async () => {\n\t\t\t\t// Outer DO lifecycle uses SockaDoSessionConfig.handleClose; inner engine no-op.\n\t\t\t},\n\t\t\tonHandlerError: config.onHandlerError\n\t\t\t\t? (err, rpcName, input, _inner) => {\n\t\t\t\t\t\tconfig.onHandlerError?.(err, rpcName, input, this);\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t\tonValidationError: config.onValidationError,\n\t\t\tserializeJson: config.serializeJson,\n\t\t\tdeserializeJson: config.deserializeJson,\n\t\t};\n\t\tthis.socka = new SockaWebSocketSession(\n\t\t\twebsocket,\n\t\t\tsessions as unknown as Map<\n\t\t\t\tWebSocket,\n\t\t\t\tSockaWebSocketSession<TContract, EmptySockaSessionData>\n\t\t\t>,\n\t\t\tsockaConfig,\n\t\t);\n\t\t// Defer past the outer `await createSession()` continuation so\n\t\t// `BaseSession.startFresh` has run and `session.data` exists (single\n\t\t// `queueMicrotask` runs before that continuation).\n\t\tqueueMicrotask(() => {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\trunSockaDoSessionOnAttached(config, this);\n\t\t\t});\n\t\t});\n\t}\n\n\tpublic async handleRawMessage(rawMessage: string): Promise<void> {\n\t\treturn this.socka.handleRawMessage(rawMessage);\n\t}\n\n\tpublic emitWireEvent(event: string, body: unknown): void {\n\t\tthis.socka.emitWireEvent(event, body);\n\t}\n\n\tpublic emitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\treturn this.socka.emitPush(name, body);\n\t}\n\n\tpublic broadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf = false,\n\t): Promise<void> {\n\t\treturn this.socka.broadcastPush(name, body, excludeSelf);\n\t}\n\n\t/**\n\t * {@link SockaWebSocketSession.listPeers} for this Durable Object room.\n\t */\n\tpublic listPeers(options?: { excludeSelf?: boolean }): TData[] {\n\t\tconst out: TData[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(s.data);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Like {@link listPeers} but maps each peer {@link SockaDoSession}.\n\t */\n\tpublic listPeersWith<R>(\n\t\tmap: (session: SockaDoSession<TContract, TData, TEnv>) => R,\n\t\toptions?: { excludeSelf?: boolean },\n\t): R[] {\n\t\tconst out: R[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(map(s as SockaDoSession<TContract, TData, TEnv>));\n\t\t}\n\t\treturn out;\n\t}\n\n\tpublic peerCount(options?: { excludeSelf?: boolean }): number {\n\t\tlet n = 0;\n\t\tfor (const [ws] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tn += 1;\n\t\t}\n\t\treturn n;\n\t}\n\n\tpublic hasPeers(options?: { excludeSelf?: boolean }): boolean {\n\t\treturn this.peerCount(options) > 0;\n\t}\n}\n","import type { Context } from \"hono\";\nimport type { SessionEnv } from \"@firtoz/websocket-do\";\nimport { BaseWebSocketDO } from \"@firtoz/websocket-do\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContractBound,\n} from \"../core/contract\";\nimport { broadcastContractPushToAll } from \"../server/SockaWebSocketSession\";\nimport type { SockaDoHost } from \"./SockaDoHost\";\nimport {\n\tSockaDoSession,\n\ttype SockaDoSessionConfigInput,\n} from \"./SockaDoSession\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\nexport type SockaWebSocketDOOptions = {\n\t/** Same as `pairServerWebSocketAcceptOptions` on `BaseWebSocketDOOptions` from `@firtoz/websocket-do`. */\n\tpairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;\n};\n\n/**\n * Low-level DO base when {@link createSockaSession} returns a custom\n * {@link SockaDoSession} subclass. Most apps extend {@link SockaWebSocketDO}\n * instead (default session type, three type parameters).\n */\nexport abstract class SockaWebSocketDOBase<\n\tTContract extends SockaContractBound,\n\tTData = EmptySockaSessionData,\n\tTSession extends SockaDoSession<\n\t\tTContract,\n\t\tTData,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: TEnv inferred from TSession via SessionEnv\n\t\tany\n\t> = SockaDoSession<TContract, TData, Cloudflare.Env>,\n\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n> extends BaseWebSocketDO<TSession, TEnv> {\n\tprotected abstract readonly contract: TContract;\n\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\toptions?: SockaWebSocketDOOptions,\n\t) {\n\t\tsuper(ctx, env, {\n\t\t\tcreateSession: (sessionCtx, websocket) =>\n\t\t\t\tthis.createSockaSession(sessionCtx, websocket),\n\t\t\tpairServerWebSocketAcceptOptions:\n\t\t\t\toptions?.pairServerWebSocketAcceptOptions,\n\t\t});\n\t}\n\n\t/**\n\t * Per-connection socka config (handlers, `createData`, lifecycle hooks).\n\t * `contract` comes from {@link contract} on this host — do not repeat it here.\n\t *\n\t * Called on each new attach and on hibernation resume (`ctx` is `undefined`\n\t * on resume). **`createData`** runs only on fresh upgrade via\n\t * {@link BaseSession.startFresh}, not on {@link BaseSession.resume}.\n\t */\n\tprotected abstract buildSockaSessionConfig(\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t): SockaDoSessionConfigInput<TContract, TData, TEnv>;\n\n\t/**\n\t * Factory for each connected WebSocket. Default: {@link SockaDoSession} wired\n\t * to this host. Override when you need a {@link SockaDoSession} subclass.\n\t */\n\tprotected createSockaSession(\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t): TSession | Promise<TSession> {\n\t\tconst host: SockaDoHost<TContract, TData, TEnv, TSession> = {\n\t\t\tcontract: this.contract,\n\t\t\tsessions: this.sessions,\n\t\t\tbuildSockaSessionConfig: (attachCtx) =>\n\t\t\t\tthis.buildSockaSessionConfig(attachCtx),\n\t\t};\n\t\treturn new SockaDoSession<TContract, TData, TEnv>(\n\t\t\twebsocket,\n\t\t\thost,\n\t\t\tctx,\n\t\t) as TSession;\n\t}\n\n\t/**\n\t * Broadcast a contract-typed push to **every** connected session in this DO.\n\t *\n\t * Use from HTTP handlers, alarms, or other code paths with no originating\n\t * WebSocket session. No-op when the room is empty.\n\t */\n\tprotected broadcastPushToAll<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\treturn broadcastContractPushToAll(this.sessions, this.contract, name, body);\n\t}\n}\n\n/**\n * Durable Object base class for socka WebSocket rooms.\n *\n * Subclasses declare {@link contract} and {@link buildSockaSessionConfig}; the\n * base wires WebSocket upgrade and hibernation resume to\n * `new SockaDoSession(websocket, host)`.\n *\n * @example\n * ```ts\n * class ChatRoomDO extends SockaWebSocketDO<typeof chatContract, SessionData, Env> {\n * protected readonly contract = chatContract;\n * protected buildSockaSessionConfig(ctx) { … }\n * }\n * ```\n */\nexport abstract class SockaWebSocketDO<\n\tTContract extends SockaContractBound,\n\tTData,\n\tTEnv extends object,\n> extends SockaWebSocketDOBase<\n\tTContract,\n\tTData,\n\tSockaDoSession<TContract, TData, TEnv>,\n\tTEnv\n> {}\n","/**\n * Shared error reply shape for correlate-by-id RPC when the handler throws.\n * Narrow at the call site to your server message union if needed.\n */\nexport function toErrorReply(\n\tid: string,\n\terror: string,\n): {\n\ttype: \"error\";\n\tid: string;\n\terror: string;\n} {\n\treturn { type: \"error\", id, error };\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
2
|
import { WSEvents } from 'hono/ws';
|
|
3
3
|
import { c as SockaContractBound } from '../socka-report-error-nTXJIzNb.js';
|
|
4
|
-
import { a as SockaWebSocketSession, c as SockaWebSocketInit, b as SockaWebSocketSessionConfigUnion } from '../SockaWebSocketSession-
|
|
4
|
+
import { a as SockaWebSocketSession, c as SockaWebSocketInit, b as SockaWebSocketSessionConfigUnion } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
5
5
|
import '@standard-schema/spec';
|
|
6
6
|
|
|
7
7
|
type SockaHonoCloudflareOptions<TContract extends SockaContractBound, TData> = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sockaHonoStrictInitFromContext } from '../chunk-QGURL3DJ.js';
|
|
2
2
|
import { dispatchSockaInboundMessage } from '../chunk-JR2GENNT.js';
|
|
3
|
-
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-
|
|
3
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-TTXY7O5P.js';
|
|
4
4
|
import { reportSockaError } from '../chunk-IFIGKR3W.js';
|
|
5
5
|
|
|
6
6
|
// src/hono/cloudflare-workers.ts
|
package/dist/hono/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { S as SockaStrictWebSocketInit, b as SockaWebSocketSessionConfigUnion, a as SockaWebSocketSession, c as SockaWebSocketInit } from '../SockaWebSocketSession-
|
|
2
|
+
import { S as SockaStrictWebSocketInit, b as SockaWebSocketSessionConfigUnion, a as SockaWebSocketSession, c as SockaWebSocketInit } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
3
3
|
import { WSEvents } from 'hono/ws';
|
|
4
4
|
import { WebSocket as WebSocket$1 } from 'ws';
|
|
5
5
|
import { c as SockaContractBound } from '../socka-report-error-nTXJIzNb.js';
|
package/dist/hono/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sockaHonoStrictInitFromContext } from '../chunk-QGURL3DJ.js';
|
|
2
2
|
export { sockaHonoStrictInitFromContext } from '../chunk-QGURL3DJ.js';
|
|
3
3
|
import { dispatchSockaInboundMessage } from '../chunk-JR2GENNT.js';
|
|
4
|
-
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-
|
|
4
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-TTXY7O5P.js';
|
|
5
5
|
import { reportSockaError } from '../chunk-IFIGKR3W.js';
|
|
6
6
|
|
|
7
7
|
// src/hono/node-ws.ts
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion, c as SockaWebSocketInit,
|
|
2
|
-
export {
|
|
1
|
+
import { a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion, c as SockaWebSocketInit, d as SockaWebSocketSessionConfig } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
2
|
+
export { h as SockaEmitCapable, i as SockaPushSession, S as SockaStrictWebSocketInit, j as SockaWebSocketSessionConfigLoose, e as broadcastContractPushToAll, f as broadcastSockaEventToAll, g as broadcastSockaEventToPeers, r as runSockaSessionOnAttached } from '../SockaWebSocketSession-BaGvSerM.js';
|
|
3
3
|
import { c as SockaContractBound, y as SockaWireFormat } from '../socka-report-error-nTXJIzNb.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dispatchSockaInboundMessage } from '../chunk-JR2GENNT.js';
|
|
2
2
|
export { dispatchSockaInboundMessage } from '../chunk-JR2GENNT.js';
|
|
3
|
-
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-
|
|
4
|
-
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-
|
|
3
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-TTXY7O5P.js';
|
|
4
|
+
export { SockaWebSocketSession, broadcastContractPushToAll, broadcastSockaEventToAll, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-TTXY7O5P.js';
|
|
5
5
|
import { reportSockaError } from '../chunk-IFIGKR3W.js';
|
|
6
6
|
|
|
7
7
|
// src/server/attachSockaWebSocket.ts
|
package/docs/durable-objects.md
CHANGED
|
@@ -2,34 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
On Cloudflare **Durable Objects**, socka splits into two pieces:
|
|
4
4
|
|
|
5
|
-
1. **`SockaDoSession`** — one instance per connected **`WebSocket`**.
|
|
6
|
-
2. **`SockaWebSocketDO`** — subclasses **`BaseWebSocketDO`** from **`@firtoz/websocket-do`**.
|
|
5
|
+
1. **`SockaDoSession`** — one instance per connected **`WebSocket`**. Handlers, **`handleClose`**, and optional **`onAttached`** live in config; **`broadcastPush`** fans out over the shared **`sessions`** map.
|
|
6
|
+
2. **`SockaWebSocketDO`** — subclasses **`BaseWebSocketDO`** from **`@firtoz/websocket-do`**. You declare the **`contract`** once on the DO and implement **`buildSockaSessionConfig`**; the base wires HTTP → WebSocket upgrade → **`new SockaDoSession(websocket, host)`**.
|
|
7
7
|
|
|
8
8
|
You still define one **`defineSocka`** contract; this page is only about hosting it on a **Durable Object**.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Recommended: `SockaWebSocketDO` (default session)
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
Most apps do **not** need a custom **`SockaDoSession`** subclass. Extend **`SockaWebSocketDO`**, set **`contract`**, and implement **`buildSockaSessionConfig`**. Add a constructor only when you need setup beyond **`super(ctx, env)`** (for example **`ctx.blockConcurrencyWhile`** for SQLite migrations).
|
|
13
|
+
|
|
14
|
+
Runnable example: **[`examples/chatroom-do/src/do.ts`](../../../examples/chatroom-do/src/do.ts)**.
|
|
13
15
|
|
|
14
16
|
```ts
|
|
15
|
-
|
|
17
|
+
import {
|
|
18
|
+
SockaWebSocketDO,
|
|
19
|
+
type SockaDoSessionConfigInput,
|
|
20
|
+
} from "@firtoz/socka/do";
|
|
21
|
+
import { myContract } from "./contract";
|
|
22
|
+
|
|
23
|
+
type SessionData = { userId: string; displayName: string };
|
|
24
|
+
|
|
25
|
+
export class MyDO extends SockaWebSocketDO<
|
|
16
26
|
typeof myContract,
|
|
17
27
|
SessionData,
|
|
18
28
|
Env
|
|
19
29
|
> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
protected readonly contract = myContract;
|
|
31
|
+
app = this.getBaseApp();
|
|
32
|
+
|
|
33
|
+
protected buildSockaSessionConfig(
|
|
34
|
+
ctx: Context<{ Bindings: Env }> | undefined,
|
|
35
|
+
): SockaDoSessionConfigInput<typeof myContract, SessionData, Env> {
|
|
36
|
+
return {
|
|
37
|
+
wireFormat: "json",
|
|
38
|
+
createData: (c) => ({ userId: crypto.randomUUID(), displayName: "…" }),
|
|
39
|
+
handlers: {
|
|
40
|
+
sendMessage: async (input, session) => {
|
|
41
|
+
await this.db.insert(…);
|
|
42
|
+
await session.broadcastPush("roomMessage", row);
|
|
43
|
+
return { ok: true as const };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
handleClose: async (session) => { … },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// HTTP admin — no WebSocket session on this path
|
|
51
|
+
async deleteMessage(id: string) {
|
|
52
|
+
await this.db.delete(…);
|
|
53
|
+
await this.broadcastPushToAll("messageDeleted", { id });
|
|
26
54
|
}
|
|
27
55
|
}
|
|
28
56
|
```
|
|
29
57
|
|
|
30
|
-
|
|
58
|
+
**`buildSockaSessionConfig`** omits **`contract`** (the DO owns it). See **[Hibernation and `session.data`](#hibernation-and-sessiondata)** for when **`ctx`** and **`createData`** run (including after hibernate resume).
|
|
59
|
+
|
|
60
|
+
## Custom `SockaDoSession` subclass (optional)
|
|
61
|
+
|
|
62
|
+
Override **`createSockaSession`** on **`SockaWebSocketDOBase`** (four type parameters) when you need a session subtype. Or construct from the host:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
class MySession extends SockaDoSession<typeof myContract, SessionData, Env> {
|
|
66
|
+
constructor(ws: WebSocket, do: MyDO, ctx?: Context<{ Bindings: Env }>) {
|
|
67
|
+
super(ws, do, ctx);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
31
71
|
|
|
32
|
-
|
|
72
|
+
The legacy **`(websocket, sessions, config)`** constructor remains for tests and non-DO tooling.
|
|
33
73
|
|
|
34
74
|
## Cloudflare Worker checklist
|
|
35
75
|
|
|
@@ -44,53 +84,38 @@ This is the **Cloudflare** side (bindings, Wrangler, generated types)—not sock
|
|
|
44
84
|
|
|
45
85
|
**`wireFormat`** defaults to **`"json"`** (text frames). Use **`"msgpack"`** only if the client also uses **`msgpack`**. Mismatched **`wireFormat`** between client and session config will fail to decode. See **[Reference](./reference.md#wire-encoding-json-and-msgpack)** and **[Internals](./internals.md)**.
|
|
46
86
|
|
|
47
|
-
## `SockaDoSession`
|
|
87
|
+
## `SockaDoSession` (manual wiring)
|
|
48
88
|
|
|
49
|
-
|
|
50
|
-
import { SockaDoSession } from "@firtoz/socka/do";
|
|
51
|
-
import { myContract } from "./contract";
|
|
89
|
+
For tests or advanced cases, you can still construct a session directly:
|
|
52
90
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
list: async (session) => fetchMessages(),
|
|
58
|
-
insert: async (input, session) => saveMessage(input.message),
|
|
59
|
-
},
|
|
60
|
-
handleClose: async (session) => {
|
|
61
|
-
// e.g. remove session.websocket from your game / presence tables
|
|
62
|
-
},
|
|
63
|
-
});
|
|
91
|
+
```ts
|
|
92
|
+
new SockaDoSession(websocket, doHost, attachCtx);
|
|
93
|
+
// or
|
|
94
|
+
new SockaDoSession(websocket, sessions, { contract, handlers, handleClose, … });
|
|
64
95
|
```
|
|
65
96
|
|
|
66
97
|
Handler types use **`InferSockaHandlers<typeof myContract, SockaDoSession<typeof myContract, …>>`**. Same semantics as **`SockaWebSocketSession`**: calls **with** **`output`** get a validated **`serverResponse`**; calls **without** **`output`** are fire-and-forget on success (no success frame). Throw **`SockaError`** for expected domain failures so the client receives a structured **`serverError`** frame (with optional **`rpc`** on the wire). For client-side **`reportError`** when using output-less calls, see **[Reference](./reference.md#optional-output-fire-and-forget)** and **[Client](./client.md#fire-and-forget)**.
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
## `SockaWebSocketDO` and routing
|
|
99
|
+
## Hibernation and `session.data`
|
|
71
100
|
|
|
72
|
-
|
|
101
|
+
**Fresh WebSocket upgrade** — **`buildSockaSessionConfig(ctx)`** runs with the Hono **`Context`**. **`createData`** runs once via **`BaseSession.startFresh(ctx)`** and can read **`ctx.req`** (query params, headers, etc.).
|
|
73
102
|
|
|
74
|
-
|
|
103
|
+
**Hibernation resume** — **`@firtoz/websocket-do`** calls **`createSession(undefined, websocket)`** then **`session.resume()`**, **not** **`startFresh`**. So:
|
|
75
104
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
app = this.getBaseApp();
|
|
105
|
+
- **`buildSockaSessionConfig(undefined)`** runs again to rebuild handlers (they may close over **`this`** — that is fine).
|
|
106
|
+
- **`createData` is not called on resume.** **`session.data`** is restored from the WebSocket attachment. Do not assume **`ctx`** exists inside **`createData`** on resume — it will not run.
|
|
79
107
|
|
|
80
|
-
|
|
81
|
-
super(ctx, env, {
|
|
82
|
-
createSockaSession: (_ctx, websocket) =>
|
|
83
|
-
new MySession(websocket, this.sessions /*, … */),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
```
|
|
108
|
+
If you mutate **`session.data`** after connect, call **`await session.update()`** so the attachment is rewritten before hibernate. If you skip **`update`**, resume can observe stale **`session.data`**. For large or authoritative state, keep a **stable id** in **`session.data`** and use **D1 / KV / SQLite** as the source of truth—the attachment is for small, session-scoped working state.
|
|
88
109
|
|
|
89
110
|
**One Durable Object instance per room** is a common pattern: derive the DO id from your room key so each instance has its own **`sessions`** map—see **[Multi-room](./multi-room.md)**.
|
|
90
111
|
|
|
91
|
-
##
|
|
112
|
+
## Pushes from HTTP / non-WebSocket handlers
|
|
113
|
+
|
|
114
|
+
Many DO apps expose **Hono HTTP routes** on **`app`** (admin moderation, internal APIs, alarms) in addition to **`/websocket`**. After mutating storage from those handlers, you often need a contract-typed push to **every** connected client — with **no** originating WebSocket session.
|
|
115
|
+
|
|
116
|
+
Use **`await this.broadcastPushToAll("messageDeleted", { id })`** on your DO subclass. The DO **`contract`** is the single source of truth. See **[Pushes — Pushes from HTTP / non-WebSocket handlers](./pushes.md#pushes-from-http--non-websocket-handlers)**.
|
|
92
117
|
|
|
93
|
-
|
|
118
|
+
Without a DO subclass, use **`broadcastContractPushToAll(this.sessions, contract, name, body)`** from **`@firtoz/socka/server`**.
|
|
94
119
|
|
|
95
120
|
## See also
|
|
96
121
|
|
package/docs/pushes.md
CHANGED
|
@@ -35,7 +35,20 @@ Types are exported from **`@firtoz/socka/core`** (same as **`@firtoz/socka`**)
|
|
|
35
35
|
- **`await session.emitPush("itemsChanged", payload)`** — send one **validated** push to **this** socket (typical for private notifications).
|
|
36
36
|
- **`await session.broadcastPush("itemsChanged", payload, excludeSelf?)`** — send to **every session in the same **`sessions`** map**, optionally skipping the caller.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
### Pushes from HTTP / non-WebSocket handlers
|
|
39
|
+
|
|
40
|
+
When the origin is **not** a connected client (admin HTTP routes on the DO Hono `app`, alarms, cron, service bindings), there is no WebSocket session to call **`broadcastPush`** on.
|
|
41
|
+
|
|
42
|
+
- **`await this.broadcastPushToAll("itemsChanged", payload)`** on **`SockaWebSocketDO`** — validates against the DO **`contract`** and fans out to **every** session in **`this.sessions`**. No **`excludeSelf`**, no anchor session. No-op when the room is empty.
|
|
43
|
+
- **`await broadcastContractPushToAll(sessions, contract, name, body)`** from **`@firtoz/socka/server`** — same semantics when you have the shared **`sessions`** map and contract but are not inside a **`SockaWebSocketDO`** subclass.
|
|
44
|
+
|
|
45
|
+
Pass **`contract`** on the DO (`protected readonly contract = myContract`) so **`broadcastPushToAll`** stays typed and validated — see **[Durable Objects](./durable-objects.md)**.
|
|
46
|
+
|
|
47
|
+
**Do not** loop over **`sessions`** and call **`broadcastPush`** on each session — **`broadcastPush`** already iterates the whole map once. A loop would multiply traffic if every iteration ran a full fan-out.
|
|
48
|
+
|
|
49
|
+
**Do not** pick an arbitrary session (for example **`sessions.values().next().value`**) as an anchor for room-wide pushes — that reads like a bug and is easy to misuse with **`excludeSelf: true`**.
|
|
50
|
+
|
|
51
|
+
Lower-level helpers (for example **`broadcastSockaEventToPeers`** / **`broadcastSockaEventToAll`** from **`@firtoz/socka/server`**) exist for advanced cases; prefer **`broadcastPushToAll`** or **`broadcastContractPushToAll`** so schemas stay centralized.
|
|
39
52
|
|
|
40
53
|
**Ordering** — Delivery order is per connection; there is no cross-client guarantee beyond your own handler ordering. For causal ordering across clients, include a **version** or **timestamp** in the payload.
|
|
41
54
|
|
|
@@ -38,41 +38,30 @@ export const roomContract = defineSocka({
|
|
|
38
38
|
|
|
39
39
|
## Durable Object
|
|
40
40
|
|
|
41
|
-
Subclass **`SockaWebSocketDO
|
|
41
|
+
Subclass **`SockaWebSocketDO`**. Declare **`protected readonly contract`** and implement **`buildSockaSessionConfig`**. Full patterns: **[Durable Objects](./durable-objects.md)**, example **[`examples/chatroom-do/src/do.ts`](../../examples/chatroom-do/src/do.ts)**.
|
|
42
42
|
|
|
43
43
|
```ts
|
|
44
44
|
// do.ts (sketch)
|
|
45
45
|
import {
|
|
46
|
-
SockaDoSession,
|
|
47
46
|
SockaWebSocketDO,
|
|
48
|
-
type
|
|
47
|
+
type SockaDoSessionConfigInput,
|
|
49
48
|
} from "@firtoz/socka/do";
|
|
50
49
|
import { roomContract } from "./contract";
|
|
51
50
|
|
|
52
51
|
type SessionData = { userId: string };
|
|
53
52
|
|
|
54
|
-
export class
|
|
53
|
+
export class RoomDo extends SockaWebSocketDO<
|
|
55
54
|
typeof roomContract,
|
|
56
55
|
SessionData,
|
|
57
56
|
Env
|
|
58
57
|
> {
|
|
59
|
-
|
|
60
|
-
ws: WebSocket,
|
|
61
|
-
sessions: Map<WebSocket, RoomSockaSession>,
|
|
62
|
-
config: SockaDoSessionConfig<typeof roomContract, SessionData, Env>,
|
|
63
|
-
) {
|
|
64
|
-
super(ws, sessions, config);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
58
|
+
protected readonly contract = roomContract;
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
new RoomSockaSession(ws, this.sessions, this.buildConfig()),
|
|
73
|
-
});
|
|
60
|
+
protected buildSockaSessionConfig(
|
|
61
|
+
ctx: Context<{ Bindings: Env }> | undefined,
|
|
62
|
+
): SockaDoSessionConfigInput<typeof roomContract, SessionData, Env> {
|
|
63
|
+
return { handlers: { … }, handleClose: async () => {} };
|
|
74
64
|
}
|
|
75
|
-
// `buildConfig()` returns SockaDoSessionConfig<typeof roomContract, SessionData, Env>
|
|
76
65
|
}
|
|
77
66
|
```
|
|
78
67
|
|
package/docs/recipes.md
CHANGED
|
@@ -63,7 +63,7 @@ app.get("/ws/:roomId", upgradeWebSocket((c) => {
|
|
|
63
63
|
|
|
64
64
|
## Durable Objects
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
- **`SockaWebSocketDO`**: extend with **`contract`** + **`buildSockaSessionConfig`** — see **[Durable Objects](./durable-objects.md)** and **[chatroom-do](../../examples/chatroom-do/src/do.ts)**.
|
|
67
67
|
|
|
68
68
|
## Client (browser)
|
|
69
69
|
|
package/docs/reference.md
CHANGED
|
@@ -124,8 +124,8 @@ Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**,
|
|
|
124
124
|
| `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
|
|
125
125
|
| `@firtoz/socka/test` | `createFakeWebSocket` for unit tests — see **[Testing](./testing.md)** |
|
|
126
126
|
| `@firtoz/socka/react` | `useSocka`, `useSockaSession`, `useSockaPresence`, provider + context |
|
|
127
|
-
| `@firtoz/socka/do` | `SockaDoSession`, `SockaWebSocketDO` |
|
|
128
|
-
| `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastSockaEventToPeers` |
|
|
127
|
+
| `@firtoz/socka/do` | `SockaDoSession`, `SockaDoHost`, `SockaWebSocketDO`, `SockaWebSocketDOBase`, `SockaDoSessionConfigInput`, `broadcastPushToAll` (on DO) |
|
|
128
|
+
| `@firtoz/socka/server` | `SockaWebSocketSession`, `attachSockaWebSocket`, `dispatchSockaInboundMessage`, `broadcastContractPushToAll`, `broadcastSockaEventToAll`, `broadcastSockaEventToPeers` |
|
|
129
129
|
| `@firtoz/socka/bun` | `createSockaBunWebSocketHandlers` for **`Bun.serve`** |
|
|
130
130
|
| `@firtoz/socka/hono` | `sockaHonoNodeWs` for **`@hono/node-ws`** |
|
|
131
131
|
| `@firtoz/socka/hono/cloudflare` | `sockaHonoCloudflare` for **`hono/cloudflare-workers`** |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/socka",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
@@ -98,10 +98,10 @@
|
|
|
98
98
|
"dependencies": {
|
|
99
99
|
"@firtoz/maybe-error": "^1.6.1",
|
|
100
100
|
"@standard-schema/spec": "^1.1.0",
|
|
101
|
-
"msgpackr": "^
|
|
101
|
+
"msgpackr": "^2.0.1"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
|
-
"@cloudflare/workers-types": "^4.
|
|
104
|
+
"@cloudflare/workers-types": "^4.20260503.1",
|
|
105
105
|
"@firtoz/websocket-do": "^13.0.2",
|
|
106
106
|
"@hono/node-server": "^1.19.2",
|
|
107
107
|
"@hono/node-ws": "^1.3.0",
|
|
@@ -133,11 +133,11 @@
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
|
-
"@cloudflare/workers-types": "^4.
|
|
136
|
+
"@cloudflare/workers-types": "^4.20260503.1",
|
|
137
137
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
138
|
-
"@hono/node-server": "^2.0.
|
|
139
|
-
"@hono/node-ws": "^1.3.
|
|
140
|
-
"@tanstack/intent": "^0.0.
|
|
138
|
+
"@hono/node-server": "^2.0.1",
|
|
139
|
+
"@hono/node-ws": "^1.3.1",
|
|
140
|
+
"@tanstack/intent": "^0.0.39",
|
|
141
141
|
"@testing-library/react": "^16.3.2",
|
|
142
142
|
"@types/react": "^19.2.14",
|
|
143
143
|
"@types/ws": "^8.18.1",
|
|
@@ -147,6 +147,6 @@
|
|
|
147
147
|
"tsup": "^8.5.1",
|
|
148
148
|
"typescript": "^6.0.3",
|
|
149
149
|
"valibot": "^1.3.1",
|
|
150
|
-
"zod": "^4.
|
|
150
|
+
"zod": "^4.4.2"
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -8,7 +8,7 @@ description: SockaDoSession and SockaWebSocketDO on Cloudflare Durable Objects
|
|
|
8
8
|
## Components
|
|
9
9
|
|
|
10
10
|
- **`SockaDoSession`** (**`@firtoz/socka/do`**): extends **`BaseSession`** from **`@firtoz/websocket-do`**. Incoming messages are decoded with **`decodeSockaWire`** after JSON parse (text) or **`parseWirePayload`** (msgpack). Valid **`clientRequest`** frames are dispatched to **`handlers`** (typed **`InferSockaHandlers<typeof contract>`**). Calls **with** **`output`** get **`encodeServerResponse`** on success; calls **without** **`output`** send **`encodeServerError`** only on failure; optional **`encodeServerEvent`** for contract pushes.
|
|
11
|
-
- **`SockaWebSocketDO`**:
|
|
11
|
+
- **`SockaWebSocketDO`**: extend with **`protected readonly contract`**, **`buildSockaSessionConfig(ctx)`** (constructor only when you need extra setup, e.g. DB migrate). Default session wiring is built-in; use **`SockaWebSocketDOBase`** only for a custom **`SockaDoSession`** subclass.
|
|
12
12
|
|
|
13
13
|
## Session config (`SockaDoSessionConfig`)
|
|
14
14
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/SockaWebSocketSession.ts"],"names":[],"mappings":"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","file":"chunk-6HY22C5G.js","sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { exhaustiveGuard } from \"@firtoz/maybe-error\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContractBound,\n} from \"../core/contract\";\nimport {\n\tSockaWireError,\n\tdecodeSockaWire,\n\tencodeServerResponse,\n\tencodeServerError,\n\tencodeServerEvent,\n\ttype SockaClientRequestFrame,\n\ttype SockaWireFrame,\n} from \"../core/envelope\";\nimport {\n\tencodeSockaWire,\n\tparseWirePayload,\n\ttype SockaWireFormat,\n} from \"../core/wire-codec\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport { parseStandardSchema } from \"../core/validate\";\nimport { SockaError } from \"../core/socka-error\";\nimport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n} from \"./SockaWebSocketSessionConfig\";\n\n/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */\ntype EmptySockaSessionData = Record<string, never>;\n\nexport type {\n\tSockaStrictWebSocketInit,\n\tSockaWebSocketInit,\n\tSockaWebSocketSessionConfig,\n\tSockaWebSocketSessionConfigLoose,\n\tSockaWebSocketSessionConfigUnion,\n};\n\nfunction isLooseUpgradeConfig<TContract extends SockaContractBound, TData>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n): config is SockaWebSocketSessionConfigLoose<TContract, TData> {\n\treturn (\n\t\t\"strictUpgradeRequest\" in config && config.strictUpgradeRequest === false\n\t);\n}\n\n/** Session that can send a wire-level server event (already validated). */\nexport type SockaEmitCapable = {\n\temitWireEvent(event: string, body: unknown): void;\n};\n\n/**\n * Contract-typed session surface for handlers that push to clients.\n */\nexport interface SockaPushSession<TContract extends SockaContractBound> {\n\temitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void>;\n\tbroadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf?: boolean,\n\t): Promise<void>;\n}\n\n/**\n * Broadcast a socka server event to every session in the map (optionally\n * excluding the caller). Payload must already be contract-validated.\n *\n * Exclusion uses the **WebSocket** identity (`self.websocket`), not the session\n * object reference, so the same `sessions` map can hold `SockaDoSession` while\n * `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).\n */\nexport function broadcastSockaEventToPeers(\n\tsessions: Map<WebSocket, SockaEmitCapable>,\n\tself: SockaEmitCapable & { readonly websocket: WebSocket },\n\tevent: string,\n\tbody: unknown,\n\texcludeSelf = false,\n): void {\n\tfor (const [ws, session] of sessions) {\n\t\tif (excludeSelf && ws === self.websocket) continue;\n\t\tsession.emitWireEvent(event, body);\n\t}\n}\n\n/**\n * Runtime-agnostic socka server session: standard {@link WebSocket} wire\n * dispatch without Cloudflare Durable Object APIs.\n */\nexport class SockaWebSocketSession<\n\tTContract extends SockaContractBound,\n\tTData = EmptySockaSessionData,\n> implements SockaPushSession<TContract>\n{\n\tprivate readonly config: SockaWebSocketSessionConfigUnion<TContract, TData>;\n\tprivate readonly wireFormat: SockaWireFormat;\n\tprivate _data!: TData;\n\n\tpublic constructor(\n\t\tpublic readonly websocket: WebSocket,\n\t\tprotected readonly sessions: Map<\n\t\t\tWebSocket,\n\t\t\tSockaWebSocketSession<TContract, TData>\n\t\t>,\n\t\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\t\tinit?: SockaWebSocketInit,\n\t) {\n\t\tthis.config = config;\n\t\tthis.wireFormat = config.wireFormat ?? \"json\";\n\t\tif (isLooseUpgradeConfig(config)) {\n\t\t\tconst createData = config.createData as\n\t\t\t\t| ((init: SockaWebSocketInit) => TData)\n\t\t\t\t| undefined;\n\t\t\tconst create = createData ?? ((_i: SockaWebSocketInit) => ({}) as TData);\n\t\t\tthis._data = create(init ?? {});\n\t\t} else {\n\t\t\tif (!init?.request) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"socka: strict upgrade (default) requires a Request on the upgrade init (e.g. Bun upgrade with `data: { …, request: req }`, or Hono default sockaInit), or use SockaWebSocketSessionConfigLoose with strictUpgradeRequest: false\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst strictInit: SockaStrictWebSocketInit = { request: init.request };\n\t\t\tif (config.createData) {\n\t\t\t\tthis._data = config.createData(strictInit);\n\t\t\t} else {\n\t\t\t\tthis._data = {} as TData;\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic get data(): TData {\n\t\treturn this._data;\n\t}\n\n\t/**\n\t * Session data for every connection in the same {@link sessions} map (same room),\n\t * optionally excluding this socket.\n\t */\n\tpublic listPeers(options?: { excludeSelf?: boolean }): TData[] {\n\t\tconst out: TData[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(s.data);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Like {@link listPeers} but maps each peer {@link SockaWebSocketSession}\n\t * (e.g. when you need more than {@link #data}).\n\t */\n\tpublic listPeersWith<R>(\n\t\tmap: (session: SockaWebSocketSession<TContract, TData>) => R,\n\t\toptions?: { excludeSelf?: boolean },\n\t): R[] {\n\t\tconst out: R[] = [];\n\t\tfor (const [ws, s] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tout.push(map(s));\n\t\t}\n\t\treturn out;\n\t}\n\n\t/** Count of sessions in this room (same {@link sessions} map), optionally excluding self. */\n\tpublic peerCount(options?: { excludeSelf?: boolean }): number {\n\t\tlet n = 0;\n\t\tfor (const [ws] of this.sessions) {\n\t\t\tif (options?.excludeSelf && ws === this.websocket) continue;\n\t\t\tn += 1;\n\t\t}\n\t\treturn n;\n\t}\n\n\t/** Whether any peer sessions exist (optionally excluding self). */\n\tpublic hasPeers(options?: { excludeSelf?: boolean }): boolean {\n\t\treturn this.peerCount(options) > 0;\n\t}\n\n\t/**\n\t * Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.\n\t * Server adapters should call this when the WebSocket closes, **before** deleting\n\t * this session from the shared `sessions` map.\n\t */\n\tpublic async invokeHandleClose(): Promise<void> {\n\t\tawait this.config.handleClose(this);\n\t}\n\n\tpublic async handleRawMessage(rawMessage: string): Promise<void> {\n\t\tif (this.wireFormat !== \"json\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected JSON frame in msgpack mode\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst deserialize = this.config.deserializeJson ?? JSON.parse;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = deserialize(rawMessage);\n\t\t} catch {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: invalid JSON\"),\n\t\t\t\trawMessage,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, rawMessage);\n\t}\n\n\tpublic async handleBinaryMessage(buffer: ArrayBuffer): Promise<void> {\n\t\tif (this.wireFormat !== \"msgpack\") {\n\t\t\tawait this.reportValidationError(\n\t\t\t\tnew Error(\"socka: unexpected binary frame in JSON mode\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = parseWirePayload(buffer, \"msgpack\");\n\t\t} catch (err) {\n\t\t\tawait this.reportValidationError(\n\t\t\t\terr instanceof Error ? err : new Error(\"socka: msgpack decode failed\"),\n\t\t\t\tbuffer,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tawait this.dispatchAfterParsed(parsed, buffer);\n\t}\n\n\tprivate async dispatchAfterParsed(\n\t\tparsed: unknown,\n\t\toriginalWire: unknown,\n\t): Promise<void> {\n\t\tlet decoded: ReturnType<typeof decodeSockaWire>;\n\t\ttry {\n\t\t\tdecoded = decodeSockaWire(parsed);\n\t\t} catch (err) {\n\t\t\tif (err instanceof SockaWireError) {\n\t\t\t\tawait this.reportValidationError(err, originalWire);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\n\t\tswitch (decoded.kind) {\n\t\t\tcase \"clientRequest\":\n\t\t\t\tawait this.dispatchClientRequest(decoded.frame, originalWire);\n\t\t\t\treturn;\n\t\t\tcase \"serverResponse\":\n\t\t\tcase \"serverError\":\n\t\t\tcase \"serverEvent\":\n\t\t\t\tawait this.reportValidationError(\n\t\t\t\t\tnew Error(\"socka: unexpected server-originated frame from client\"),\n\t\t\t\t\tparsed,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\texhaustiveGuard(decoded);\n\t\t}\n\t}\n\n\tprivate async dispatchClientRequest(\n\t\tframe: SockaClientRequestFrame,\n\t\t_originalWire: unknown,\n\t): Promise<void> {\n\t\tconst rpcName = frame.rpc;\n\t\tconst procedure = this.config.contract.calls[rpcName];\n\n\t\tif (!procedure) {\n\t\t\tconst errorFrame = encodeServerError(\n\t\t\t\tframe.id,\n\t\t\t\t`Unknown call: ${rpcName}`,\n\t\t\t\t{ rpc: rpcName },\n\t\t\t);\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedInput: unknown;\n\t\tif (procedure.input) {\n\t\t\ttry {\n\t\t\t\tvalidatedInput = await parseStandardSchema(procedure.input, frame.body);\n\t\t\t} catch (err) {\n\t\t\t\tconst msg =\n\t\t\t\t\terr instanceof Error ? err.message : \"Input validation failed\";\n\t\t\t\tconst errorFrame = encodeServerError(frame.id, msg, {\n\t\t\t\t\trpc: rpcName,\n\t\t\t\t});\n\t\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tlet result: unknown;\n\t\ttry {\n\t\t\tif (procedure.input) {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\tinput: unknown,\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(validatedInput, this);\n\t\t\t} else {\n\t\t\t\tconst handler = this.config.handlers[rpcName] as (\n\t\t\t\t\ts: SockaWebSocketSession<TContract, TData>,\n\t\t\t\t) => unknown | Promise<unknown>;\n\t\t\t\tresult = await handler(this);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.config.onHandlerError?.(err, rpcName, validatedInput, this);\n\t\t\tconst sockaErr =\n\t\t\t\terr instanceof SockaError\n\t\t\t\t\t? err\n\t\t\t\t\t: new SockaError(\n\t\t\t\t\t\t\terr instanceof Error ? err.message : \"Handler failed\",\n\t\t\t\t\t\t);\n\t\t\tconst errorFrame = encodeServerError(frame.id, sockaErr.message, {\n\t\t\t\trpc: rpcName,\n\t\t\t\tcode: sockaErr.code,\n\t\t\t\tdata: sockaErr.data,\n\t\t\t});\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tif (procedure.output === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet validatedOutput: unknown;\n\t\ttry {\n\t\t\tvalidatedOutput = await parseStandardSchema(procedure.output, result);\n\t\t} catch (err) {\n\t\t\tconst msg =\n\t\t\t\terr instanceof Error ? err.message : \"Output validation failed\";\n\t\t\tconst errorFrame = encodeServerError(frame.id, msg, { rpc: rpcName });\n\t\t\tthis.sendWireFrame(errorFrame);\n\t\t\treturn;\n\t\t}\n\n\t\tconst responseFrame = encodeServerResponse(\n\t\t\tframe.id,\n\t\t\trpcName,\n\t\t\tvalidatedOutput,\n\t\t);\n\t\tthis.sendWireFrame(responseFrame);\n\t}\n\n\tprivate encodeOutgoing(frame: SockaWireFrame): string | Uint8Array {\n\t\treturn encodeSockaWire(\n\t\t\tframe,\n\t\t\tthis.wireFormat,\n\t\t\tthis.config.serializeJson ?? JSON.stringify,\n\t\t);\n\t}\n\n\tprivate sendWireFrame(frame: SockaWireFrame): void {\n\t\tif (this.websocket.readyState !== WebSocket.OPEN) {\n\t\t\treturn;\n\t\t}\n\t\tconst encoded = this.encodeOutgoing(frame);\n\t\tif (typeof encoded === \"string\") {\n\t\t\tthis.websocket.send(encoded);\n\t\t\treturn;\n\t\t}\n\t\tconst copy = new Uint8Array(encoded.byteLength);\n\t\tcopy.set(encoded);\n\t\tthis.websocket.send(copy.buffer);\n\t}\n\n\t/**\n\t * Send a server event frame (wire). Prefer {@link emitPush} so\n\t * payloads are validated against the contract.\n\t */\n\tpublic emitWireEvent(event: string, body: unknown): void {\n\t\tconst frame = encodeServerEvent(event, body);\n\t\tthis.sendWireFrame(frame);\n\t}\n\n\tpublic async emitPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tthis.emitWireEvent(name, validated);\n\t}\n\n\tpublic async broadcastPush<K extends keyof TContract[\"pushes\"] & string>(\n\t\tname: K,\n\t\tbody: InferSockaPushPayload<TContract, K>,\n\t\texcludeSelf = false,\n\t): Promise<void> {\n\t\tconst schema = this.config.contract.pushes[name];\n\t\tif (!schema) {\n\t\t\tthrow new Error(`socka: unknown push ${String(name)}`);\n\t\t}\n\t\tconst validated = await parseStandardSchema(\n\t\t\tschema as StandardSchemaV1<unknown, InferSockaPushPayload<TContract, K>>,\n\t\t\tbody,\n\t\t);\n\t\tbroadcastSockaEventToPeers(\n\t\t\tthis.sessions,\n\t\t\tthis,\n\t\t\tname,\n\t\t\tvalidated,\n\t\t\texcludeSelf,\n\t\t);\n\t}\n\n\tprivate async reportValidationError(\n\t\terror: unknown,\n\t\toriginalMessage: unknown,\n\t): Promise<void> {\n\t\tif (this.config.onValidationError) {\n\t\t\tawait this.config.onValidationError(error, originalMessage);\n\t\t} else {\n\t\t\tconsole.error(\"socka: validation error:\", error, originalMessage);\n\t\t}\n\t}\n}\n\n/**\n * Invoke {@link SockaWebSocketSessionConfig.onAttached} after the session is\n * registered in the shared map.\n */\nexport function runSockaSessionOnAttached<\n\tTContract extends SockaContractBound,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfigUnion<TContract, TData>,\n\tsession: SockaWebSocketSession<TContract, TData>,\n): void {\n\tconst cb = config.onAttached;\n\tif (!cb) return;\n\ttry {\n\t\tconst result = cb(session);\n\t\tvoid Promise.resolve(result).catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverOnAttached\",\n\t\t\t\terror,\n\t\t\t});\n\t\t});\n\t} catch (error) {\n\t\treportSockaError(config.reportError, { kind: \"serverOnAttached\", error });\n\t}\n}\n"]}
|