@firtoz/socka 2.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/README.md +120 -0
- package/assets/banner.png +0 -0
- package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +107 -0
- package/dist/bun/index.d.ts +38 -0
- package/dist/bun/index.js +121 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/chunk-45D4T232.js +236 -0
- package/dist/chunk-45D4T232.js.map +1 -0
- package/dist/chunk-5WQTYLIC.js +46 -0
- package/dist/chunk-5WQTYLIC.js.map +1 -0
- package/dist/chunk-AM7PB26G.js +421 -0
- package/dist/chunk-AM7PB26G.js.map +1 -0
- package/dist/chunk-MZCQHJXY.js +158 -0
- package/dist/chunk-MZCQHJXY.js.map +1 -0
- package/dist/chunk-YMT4HAH7.js +20 -0
- package/dist/chunk-YMT4HAH7.js.map +1 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/do/index.d.ts +80 -0
- package/dist/do/index.js +110 -0
- package/dist/do/index.js.map +1 -0
- package/dist/hono/cloudflare-workers.d.ts +21 -0
- package/dist/hono/cloudflare-workers.js +68 -0
- package/dist/hono/cloudflare-workers.js.map +1 -0
- package/dist/hono/index.d.ts +30 -0
- package/dist/hono/index.js +74 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/react/index.d.ts +72 -0
- package/dist/react/index.js +126 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.js +63 -0
- package/dist/server/index.js.map +1 -0
- package/dist/socka-report-error-DzFI2Tr7.d.ts +206 -0
- package/docs/README.md +18 -0
- package/docs/client.md +85 -0
- package/docs/comparison.md +36 -0
- package/docs/durable-objects.md +74 -0
- package/docs/events.md +48 -0
- package/docs/getting-started.md +138 -0
- package/docs/lifecycle.md +31 -0
- package/docs/multi-room.md +31 -0
- package/docs/peers.md +85 -0
- package/docs/reference.md +123 -0
- package/docs/server.md +124 -0
- package/examples/minimal-socka.ts +31 -0
- package/package.json +148 -0
- package/roadmap.md +8 -0
- package/skills/socka/core-rpc/SKILL.md +36 -0
- package/skills/socka/do-session/SKILL.md +33 -0
- package/skills/socka/standard-schema/SKILL.md +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @firtoz/socka
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@firtoz/socka)
|
|
4
|
+
[](https://www.npmjs.com/package/@firtoz/socka)
|
|
5
|
+
[](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
|
9
|
+
[](https://standardschema.dev)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
**Typed WebSocket RPC for TypeScript.** Define one contract, get **`session.send.*`** in the client and **`handlers`** on the server—validated, correlated, done.
|
|
14
|
+
|
|
15
|
+
**npm:** [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka). *Socka* is the project name in prose; **install and `import` paths always use `@firtoz/socka` or `@firtoz/socka/...`**. The published artifact is **compiled ESM + `.d.ts` in `dist/`** (see `package.json` `exports`).
|
|
16
|
+
|
|
17
|
+
## 30-second example (Bun)
|
|
18
|
+
|
|
19
|
+
**`contract.ts`** (shared):
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineSocka } from "@firtoz/socka/core";
|
|
23
|
+
import * as z from "zod";
|
|
24
|
+
|
|
25
|
+
export const myContract = defineSocka({
|
|
26
|
+
calls: {
|
|
27
|
+
echo: {
|
|
28
|
+
input: z.object({ text: z.string() }),
|
|
29
|
+
output: z.object({ text: z.string() }),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**`server.ts`**:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createSockaBunWebSocketHandlers } from "@firtoz/socka/bun";
|
|
39
|
+
import { myContract } from "./contract";
|
|
40
|
+
|
|
41
|
+
const { websocket } = createSockaBunWebSocketHandlers({
|
|
42
|
+
contract: myContract,
|
|
43
|
+
handlers: {
|
|
44
|
+
echo: async (input) => ({ text: input.text }),
|
|
45
|
+
},
|
|
46
|
+
handleClose: async () => {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
Bun.serve({
|
|
50
|
+
port: 3450,
|
|
51
|
+
fetch(req, server) {
|
|
52
|
+
if (new URL(req.url).pathname === "/ws") {
|
|
53
|
+
if (server.upgrade(req)) return undefined;
|
|
54
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
return new Response("OK");
|
|
57
|
+
},
|
|
58
|
+
websocket,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`client.ts`** (browser or Bun):
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { SockaSession } from "@firtoz/socka/client";
|
|
66
|
+
import { myContract } from "./contract";
|
|
67
|
+
|
|
68
|
+
const session = new SockaSession({
|
|
69
|
+
contract: myContract,
|
|
70
|
+
url: "ws://localhost:3450/ws",
|
|
71
|
+
});
|
|
72
|
+
const { text } = await session.send.echo({ text: "hello" });
|
|
73
|
+
console.log(text);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run **`bun run server.ts`**, then point the client at **`ws://localhost:3450/ws`**.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install @firtoz/socka
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Also: `pnpm add @firtoz/socka` · `bun add @firtoz/socka`
|
|
85
|
+
|
|
86
|
+
Optional peers depend on which subpath you import—see **[Peers](./docs/peers.md)**.
|
|
87
|
+
|
|
88
|
+
## Other runtimes
|
|
89
|
+
|
|
90
|
+
| Runtime | Subpath | Guide |
|
|
91
|
+
|--------|---------|--------|
|
|
92
|
+
| **Node** + [`ws`](https://github.com/websockets/ws), or any standard **`WebSocket`** | `@firtoz/socka/server` | **[Server](./docs/server.md)** — `attachSockaWebSocket` |
|
|
93
|
+
| **Bun** `Bun.serve` / `ServerWebSocket` | `@firtoz/socka/bun` | **[Server](./docs/server.md)** |
|
|
94
|
+
| **Hono** on Node (`@hono/node-ws`) | `@firtoz/socka/hono` | **[Server](./docs/server.md)** |
|
|
95
|
+
| **Hono** on Cloudflare Workers | `@firtoz/socka/hono/cloudflare` | **[Server](./docs/server.md)** |
|
|
96
|
+
| **Cloudflare Durable Objects** | `@firtoz/socka/do` | **[Durable Objects](./docs/durable-objects.md)** |
|
|
97
|
+
|
|
98
|
+
## Why not socket.io, tRPC, or DIY?
|
|
99
|
+
|
|
100
|
+
- **Schema-first RPC + push** — one contract; no parallel “event” protocol for server pushes.
|
|
101
|
+
- **Correlated envelopes** — request/response IDs and validation hooks are built in.
|
|
102
|
+
- **Same contract** across Bun, Hono, Node `ws`, and Durable Objects (see **[Comparison](./docs/comparison.md)** for socket.io / tRPC / hand-rolled).
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
Hub: **[`docs/README.md`](./docs/README.md)** (getting started, peers, lifecycle, multi-room, reference).
|
|
107
|
+
|
|
108
|
+
**Roadmap:** [post–v1 and deferred work](./roadmap.md). Agent skills: [`skills/`](./skills/).
|
|
109
|
+
|
|
110
|
+
## Full-stack examples
|
|
111
|
+
|
|
112
|
+
Self-contained **tic-tac-toe** apps in the monorepo [`examples/`](../../examples/) (same game, different servers):
|
|
113
|
+
|
|
114
|
+
| Stack | Folder | Port |
|
|
115
|
+
|--------|--------|------|
|
|
116
|
+
| **Bun** (`@firtoz/socka/bun`) | [`tic-tac-toe-bun`](../../examples/tic-tac-toe-bun) | **3461** |
|
|
117
|
+
| **Hono + Node** (`@firtoz/socka/hono`) | [`tic-tac-toe-hono`](../../examples/tic-tac-toe-hono) | **3462** |
|
|
118
|
+
| **Cloudflare DO** (`@firtoz/socka/do`) | [`tic-tac-toe-do`](../../examples/tic-tac-toe-do) | **3463** |
|
|
119
|
+
|
|
120
|
+
Each app: **`bun run dev`** (or **`wrangler dev`** for the DO example).
|
|
Binary file
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat, e as InferSockaHandlers, y as SockaReportError, g as InferSockaPushPayload } from './socka-report-error-DzFI2Tr7.js';
|
|
2
|
+
|
|
3
|
+
/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
|
|
4
|
+
type EmptySockaSessionData$1 = Record<string, never>;
|
|
5
|
+
/** Optional upgrade context for {@link SockaWebSocketSession}. */
|
|
6
|
+
type SockaWebSocketInit = {
|
|
7
|
+
/** Original HTTP request for the WebSocket upgrade, when available. */
|
|
8
|
+
request?: Request;
|
|
9
|
+
};
|
|
10
|
+
type SockaWebSocketCreateData<TData> = [TData] extends [EmptySockaSessionData$1] ? {
|
|
11
|
+
createData?: (init: SockaWebSocketInit) => TData;
|
|
12
|
+
} : {
|
|
13
|
+
createData: (init: SockaWebSocketInit) => TData;
|
|
14
|
+
};
|
|
15
|
+
type SockaSessionForHandlers<TContract extends SockaContract<SockaContractConfig>, TData> = SockaWebSocketSession<TContract, TData>;
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for {@link SockaWebSocketSession}. Handlers receive the session
|
|
18
|
+
* instance as the second argument (or the only argument when the procedure has no input).
|
|
19
|
+
*/
|
|
20
|
+
type SockaWebSocketSessionConfig<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData$1> = {
|
|
21
|
+
contract: TContract;
|
|
22
|
+
/** Default `"json"`. Use `"msgpack"` for binary frames (must match client). */
|
|
23
|
+
wireFormat?: SockaWireFormat;
|
|
24
|
+
handlers: InferSockaHandlers<TContract, SockaSessionForHandlers<TContract, TData>>;
|
|
25
|
+
/** Called when this WebSocket closes, before the socket is removed from `sessions`. */
|
|
26
|
+
handleClose: (session: SockaSessionForHandlers<TContract, TData>) => Promise<void>;
|
|
27
|
+
onHandlerError?: (error: unknown, rpcName: string, input: unknown, session: SockaSessionForHandlers<TContract, TData>) => void;
|
|
28
|
+
onValidationError?: (error: unknown, originalMessage: unknown) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Optional sink for non-RPC failures (onAttached, adapter I/O). Defaults to
|
|
31
|
+
* `console.error` with `socka:` prefixes; see `SockaReportError` in `@firtoz/socka/core`.
|
|
32
|
+
*/
|
|
33
|
+
reportError?: (event: SockaReportError) => void;
|
|
34
|
+
serializeJson?: (value: unknown) => string;
|
|
35
|
+
deserializeJson?: (raw: string) => unknown;
|
|
36
|
+
/**
|
|
37
|
+
* Called once after this session is registered in the shared `sessions` map
|
|
38
|
+
* (safe to broadcast to peers). Sync or async; async rejections are logged.
|
|
39
|
+
*/
|
|
40
|
+
onAttached?: (session: SockaSessionForHandlers<TContract, TData>) => void | Promise<void>;
|
|
41
|
+
} & SockaWebSocketCreateData<TData>;
|
|
42
|
+
|
|
43
|
+
/** Session data with no fields — `createData` may be omitted (defaults to `{}`). */
|
|
44
|
+
type EmptySockaSessionData = Record<string, never>;
|
|
45
|
+
|
|
46
|
+
/** Session that can send a wire-level server event (already validated). */
|
|
47
|
+
type SockaEmitCapable = {
|
|
48
|
+
emitWireEvent(event: string, body: unknown): void;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Contract-typed session surface for handlers that push to clients.
|
|
52
|
+
*/
|
|
53
|
+
interface SockaPushSession<TContract extends SockaContract<SockaContractConfig>> {
|
|
54
|
+
emitPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
|
|
55
|
+
broadcastPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>, excludeSelf?: boolean): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Broadcast a socka server event to every session in the map (optionally
|
|
59
|
+
* excluding the caller). Payload must already be contract-validated.
|
|
60
|
+
*
|
|
61
|
+
* Exclusion uses the **WebSocket** identity (`self.websocket`), not the session
|
|
62
|
+
* object reference, so the same `sessions` map can hold `SockaDoSession` while
|
|
63
|
+
* `broadcastPush` runs on `this.socka` (inner {@link SockaWebSocketSession}).
|
|
64
|
+
*/
|
|
65
|
+
declare function broadcastSockaEventToPeers(sessions: Map<WebSocket, SockaEmitCapable>, self: SockaEmitCapable & {
|
|
66
|
+
readonly websocket: WebSocket;
|
|
67
|
+
}, event: string, body: unknown, excludeSelf?: boolean): void;
|
|
68
|
+
/**
|
|
69
|
+
* Runtime-agnostic socka server session: standard {@link WebSocket} wire
|
|
70
|
+
* dispatch without Cloudflare Durable Object APIs.
|
|
71
|
+
*/
|
|
72
|
+
declare class SockaWebSocketSession<TContract extends SockaContract<SockaContractConfig>, TData = EmptySockaSessionData> implements SockaPushSession<TContract> {
|
|
73
|
+
readonly websocket: WebSocket;
|
|
74
|
+
protected readonly sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
|
|
75
|
+
private readonly config;
|
|
76
|
+
private readonly wireFormat;
|
|
77
|
+
private _data;
|
|
78
|
+
constructor(websocket: WebSocket, sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>, config: SockaWebSocketSessionConfig<TContract, TData>, init?: SockaWebSocketInit);
|
|
79
|
+
get data(): TData;
|
|
80
|
+
/**
|
|
81
|
+
* Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.
|
|
82
|
+
* Server adapters should call this when the WebSocket closes, **before** deleting
|
|
83
|
+
* this session from the shared `sessions` map.
|
|
84
|
+
*/
|
|
85
|
+
invokeHandleClose(): Promise<void>;
|
|
86
|
+
handleRawMessage(rawMessage: string): Promise<void>;
|
|
87
|
+
handleBinaryMessage(buffer: ArrayBuffer): Promise<void>;
|
|
88
|
+
private dispatchAfterParsed;
|
|
89
|
+
private dispatchClientRequest;
|
|
90
|
+
private encodeOutgoing;
|
|
91
|
+
private sendWireFrame;
|
|
92
|
+
/**
|
|
93
|
+
* Send a server event frame (wire). Prefer {@link emitPush} so
|
|
94
|
+
* payloads are validated against the contract.
|
|
95
|
+
*/
|
|
96
|
+
emitWireEvent(event: string, body: unknown): void;
|
|
97
|
+
emitPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>): Promise<void>;
|
|
98
|
+
broadcastPush<K extends keyof TContract["pushes"] & string>(name: K, body: InferSockaPushPayload<TContract, K>, excludeSelf?: boolean): Promise<void>;
|
|
99
|
+
private reportValidationError;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Invoke {@link SockaWebSocketSessionConfig.onAttached} after the session is
|
|
103
|
+
* registered in the shared map.
|
|
104
|
+
*/
|
|
105
|
+
declare function runSockaSessionOnAttached<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfig<TContract, TData>, session: SockaWebSocketSession<TContract, TData>): void;
|
|
106
|
+
|
|
107
|
+
export { SockaWebSocketSession as S, type SockaWebSocketSessionConfig as a, type SockaWebSocketInit as b, broadcastSockaEventToPeers as c, type SockaEmitCapable as d, type SockaPushSession as e, runSockaSessionOnAttached as r };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ServerWebSocket } from 'bun';
|
|
2
|
+
import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-DzFI2Tr7.js';
|
|
3
|
+
import { S as SockaWebSocketSession, a as SockaWebSocketSessionConfig } from '../SockaWebSocketSession-Bru8yFcK.js';
|
|
4
|
+
import '@standard-schema/spec';
|
|
5
|
+
|
|
6
|
+
type SockaBunResolveScope<TContract extends SockaContract<SockaContractConfig>, TData, TWsData = undefined> = (ws: ServerWebSocket<TWsData>) => {
|
|
7
|
+
sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
|
|
8
|
+
config: SockaWebSocketSessionConfig<TContract, TData>;
|
|
9
|
+
};
|
|
10
|
+
type SockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData, TWsData = undefined> = {
|
|
11
|
+
sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
|
|
12
|
+
/** Pass into `Bun.serve({ ..., websocket })`. */
|
|
13
|
+
websocket: {
|
|
14
|
+
open: (ws: ServerWebSocket<TWsData>) => void;
|
|
15
|
+
message: (ws: ServerWebSocket<TWsData>, message: unknown) => void | Promise<void>;
|
|
16
|
+
close: (ws: ServerWebSocket<TWsData>) => void | Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
wireFormat: SockaWireFormat;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* WebSocket handlers for `Bun.serve` when using `ServerWebSocket` (no
|
|
22
|
+
* `addEventListener`). Inbound frames are dispatched with the same logic as
|
|
23
|
+
* `attachSockaWebSocket`.
|
|
24
|
+
*
|
|
25
|
+
* **Single-room:** pass a {@link SockaWebSocketSessionConfig} and optional shared `sessionMap`.
|
|
26
|
+
*
|
|
27
|
+
* **Multi-room:** pass `{ resolveScope }` where `resolveScope(ws)` returns the
|
|
28
|
+
* `sessionMap` and `config` for that socket’s scope (e.g. from `ws.data.roomId`).
|
|
29
|
+
* The returned `sessionMap` is an empty placeholder; real maps come from `resolveScope`.
|
|
30
|
+
*/
|
|
31
|
+
declare function createSockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData>(config: SockaWebSocketSessionConfig<TContract, TData>, options?: {
|
|
32
|
+
sessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
|
|
33
|
+
}): SockaBunWebSocketHandlers<TContract, TData, undefined>;
|
|
34
|
+
declare function createSockaBunWebSocketHandlers<TContract extends SockaContract<SockaContractConfig>, TData, TWsData>(options: {
|
|
35
|
+
resolveScope: SockaBunResolveScope<TContract, TData, TWsData>;
|
|
36
|
+
}): SockaBunWebSocketHandlers<TContract, TData, TWsData>;
|
|
37
|
+
|
|
38
|
+
export { type SockaBunResolveScope, type SockaBunWebSocketHandlers, createSockaBunWebSocketHandlers };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { dispatchSockaInboundMessage } from '../chunk-5WQTYLIC.js';
|
|
2
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-45D4T232.js';
|
|
3
|
+
import { reportSockaError } from '../chunk-MZCQHJXY.js';
|
|
4
|
+
|
|
5
|
+
// src/bun/index.ts
|
|
6
|
+
function bunHandlersFromResolveScope(resolveScope) {
|
|
7
|
+
const websocket = {
|
|
8
|
+
open(ws) {
|
|
9
|
+
const { sessionMap, config } = resolveScope(ws);
|
|
10
|
+
const domWs = ws;
|
|
11
|
+
const session = new SockaWebSocketSession(domWs, sessionMap, config);
|
|
12
|
+
sessionMap.set(domWs, session);
|
|
13
|
+
runSockaSessionOnAttached(config, session);
|
|
14
|
+
},
|
|
15
|
+
async message(ws, message) {
|
|
16
|
+
const { sessionMap, config } = resolveScope(ws);
|
|
17
|
+
const domWs = ws;
|
|
18
|
+
const session = sessionMap.get(domWs);
|
|
19
|
+
if (!session) return;
|
|
20
|
+
const wireFormat = config.wireFormat ?? "json";
|
|
21
|
+
try {
|
|
22
|
+
await dispatchSockaInboundMessage(
|
|
23
|
+
session,
|
|
24
|
+
wireFormat,
|
|
25
|
+
message
|
|
26
|
+
);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
reportSockaError(config.reportError, {
|
|
29
|
+
kind: "serverInboundMessage",
|
|
30
|
+
adapter: "bun",
|
|
31
|
+
error
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async close(ws) {
|
|
36
|
+
const { sessionMap, config } = resolveScope(ws);
|
|
37
|
+
const domWs = ws;
|
|
38
|
+
const session = sessionMap.get(domWs);
|
|
39
|
+
try {
|
|
40
|
+
if (session) {
|
|
41
|
+
await session.invokeHandleClose();
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
reportSockaError(config.reportError, {
|
|
45
|
+
kind: "serverHandleClose",
|
|
46
|
+
error
|
|
47
|
+
});
|
|
48
|
+
} finally {
|
|
49
|
+
sessionMap.delete(domWs);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
sessionMap: /* @__PURE__ */ new Map(),
|
|
55
|
+
websocket,
|
|
56
|
+
wireFormat: "json"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function bunHandlersFromConfig(config, maybeOptions) {
|
|
60
|
+
const sessionMap = maybeOptions?.sessionMap ?? /* @__PURE__ */ new Map();
|
|
61
|
+
const wireFormat = config.wireFormat ?? "json";
|
|
62
|
+
const websocket = {
|
|
63
|
+
open(ws) {
|
|
64
|
+
const domWs = ws;
|
|
65
|
+
const session = new SockaWebSocketSession(domWs, sessionMap, config);
|
|
66
|
+
sessionMap.set(domWs, session);
|
|
67
|
+
runSockaSessionOnAttached(config, session);
|
|
68
|
+
},
|
|
69
|
+
async message(ws, message) {
|
|
70
|
+
const domWs = ws;
|
|
71
|
+
const session = sessionMap.get(domWs);
|
|
72
|
+
if (!session) return;
|
|
73
|
+
try {
|
|
74
|
+
await dispatchSockaInboundMessage(
|
|
75
|
+
session,
|
|
76
|
+
wireFormat,
|
|
77
|
+
message
|
|
78
|
+
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
reportSockaError(config.reportError, {
|
|
81
|
+
kind: "serverInboundMessage",
|
|
82
|
+
adapter: "bun",
|
|
83
|
+
error
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async close(ws) {
|
|
88
|
+
const domWs = ws;
|
|
89
|
+
const session = sessionMap.get(domWs);
|
|
90
|
+
try {
|
|
91
|
+
if (session) {
|
|
92
|
+
await session.invokeHandleClose();
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
reportSockaError(config.reportError, {
|
|
96
|
+
kind: "serverHandleClose",
|
|
97
|
+
error
|
|
98
|
+
});
|
|
99
|
+
} finally {
|
|
100
|
+
sessionMap.delete(domWs);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return { sessionMap, websocket, wireFormat };
|
|
105
|
+
}
|
|
106
|
+
function createSockaBunWebSocketHandlers(configOrOptions, maybeOptions) {
|
|
107
|
+
const isResolveScope = typeof configOrOptions === "object" && configOrOptions !== null && "resolveScope" in configOrOptions && typeof configOrOptions.resolveScope === "function";
|
|
108
|
+
if (isResolveScope) {
|
|
109
|
+
return bunHandlersFromResolveScope(
|
|
110
|
+
configOrOptions.resolveScope
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return bunHandlersFromConfig(
|
|
114
|
+
configOrOptions,
|
|
115
|
+
maybeOptions
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { createSockaBunWebSocketHandlers };
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/bun/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ServerWebSocket } from \"bun\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport type { SockaWireFormat } from \"../core/wire-codec\";\nimport { dispatchSockaInboundMessage } from \"../server/dispatchSockaInboundMessage\";\nimport {\n\tSockaWebSocketSession,\n\trunSockaSessionOnAttached,\n\ttype SockaWebSocketSessionConfig,\n} from \"../server/SockaWebSocketSession\";\n\nexport type SockaBunResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = (ws: ServerWebSocket<TWsData>) => {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>;\n};\n\nexport type SockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData = undefined,\n> = {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t/** Pass into `Bun.serve({ ..., websocket })`. */\n\twebsocket: {\n\t\topen: (ws: ServerWebSocket<TWsData>) => void;\n\t\tmessage: (\n\t\t\tws: ServerWebSocket<TWsData>,\n\t\t\tmessage: unknown,\n\t\t) => void | Promise<void>;\n\t\tclose: (ws: ServerWebSocket<TWsData>) => void | Promise<void>;\n\t};\n\twireFormat: SockaWireFormat;\n};\n\nfunction bunHandlersFromResolveScope<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>,\n): SockaBunWebSocketHandlers<TContract, TData, TWsData> {\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tTWsData\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = new SockaWebSocketSession(domWs, sessionMap, config);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<TWsData>, message: unknown) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\tconst wireFormat = config.wireFormat ?? \"json\";\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<TWsData>) {\n\t\t\tconst { sessionMap, config } = resolveScope(ws);\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn {\n\t\tsessionMap: new Map(),\n\t\twebsocket,\n\t\twireFormat: \"json\",\n\t};\n}\n\nfunction bunHandlersFromConfig<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined> {\n\tconst sessionMap =\n\t\tmaybeOptions?.sessionMap ??\n\t\tnew Map<WebSocket, SockaWebSocketSession<TContract, TData>>();\n\tconst wireFormat = config.wireFormat ?? \"json\";\n\n\tconst websocket: SockaBunWebSocketHandlers<\n\t\tTContract,\n\t\tTData,\n\t\tundefined\n\t>[\"websocket\"] = {\n\t\topen(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = new SockaWebSocketSession(domWs, sessionMap, config);\n\t\t\tsessionMap.set(domWs, session);\n\t\t\trunSockaSessionOnAttached(config, session);\n\t\t},\n\t\tasync message(ws: ServerWebSocket<undefined>, message: unknown) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\tif (!session) return;\n\t\t\ttry {\n\t\t\t\tawait dispatchSockaInboundMessage(\n\t\t\t\t\tsession,\n\t\t\t\t\twireFormat,\n\t\t\t\t\tmessage as MessageEvent[\"data\"],\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"bun\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t\tasync close(ws: ServerWebSocket<undefined>) {\n\t\t\tconst domWs = ws as unknown as WebSocket;\n\t\t\tconst session = sessionMap.get(domWs);\n\t\t\ttry {\n\t\t\t\tif (session) {\n\t\t\t\t\tawait session.invokeHandleClose();\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverHandleClose\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t} finally {\n\t\t\t\tsessionMap.delete(domWs);\n\t\t\t}\n\t\t},\n\t};\n\n\treturn { sessionMap, websocket, wireFormat };\n}\n\n/**\n * WebSocket handlers for `Bun.serve` when using `ServerWebSocket` (no\n * `addEventListener`). Inbound frames are dispatched with the same logic as\n * `attachSockaWebSocket`.\n *\n * **Single-room:** pass a {@link SockaWebSocketSessionConfig} and optional shared `sessionMap`.\n *\n * **Multi-room:** pass `{ resolveScope }` where `resolveScope(ws)` returns the\n * `sessionMap` and `config` for that socket’s scope (e.g. from `ws.data.roomId`).\n * The returned `sessionMap` is an empty placeholder; real maps come from `resolveScope`.\n */\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\toptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n): SockaBunWebSocketHandlers<TContract, TData, undefined>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n\tTWsData,\n>(options: {\n\tresolveScope: SockaBunResolveScope<TContract, TData, TWsData>;\n}): SockaBunWebSocketHandlers<TContract, TData, TWsData>;\n\nexport function createSockaBunWebSocketHandlers<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tconfigOrOptions:\n\t\t| SockaWebSocketSessionConfig<TContract, TData>\n\t\t| { resolveScope: SockaBunResolveScope<TContract, TData, unknown> },\n\tmaybeOptions?: {\n\t\tsessionMap?: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\t},\n):\n\t| SockaBunWebSocketHandlers<TContract, TData, undefined>\n\t| SockaBunWebSocketHandlers<TContract, TData, unknown> {\n\tconst isResolveScope =\n\t\ttypeof configOrOptions === \"object\" &&\n\t\tconfigOrOptions !== null &&\n\t\t\"resolveScope\" in configOrOptions &&\n\t\ttypeof (configOrOptions as { resolveScope: unknown }).resolveScope ===\n\t\t\t\"function\";\n\n\tif (isResolveScope) {\n\t\treturn bunHandlersFromResolveScope(\n\t\t\t(\n\t\t\t\tconfigOrOptions as {\n\t\t\t\t\tresolveScope: SockaBunResolveScope<TContract, TData, unknown>;\n\t\t\t\t}\n\t\t\t).resolveScope,\n\t\t);\n\t}\n\treturn bunHandlersFromConfig(\n\t\tconfigOrOptions as SockaWebSocketSessionConfig<TContract, TData>,\n\t\tmaybeOptions,\n\t);\n}\n"]}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { parseWirePayload, decodeSockaWire, SockaWireError, encodeServerError, parseStandardSchema, SockaError, encodeServerResponse, encodeSockaWire, encodeServerEvent, reportSockaError } from './chunk-MZCQHJXY.js';
|
|
2
|
+
import { exhaustiveGuard } from '@firtoz/maybe-error';
|
|
3
|
+
|
|
4
|
+
function broadcastSockaEventToPeers(sessions, self, event, body, excludeSelf = false) {
|
|
5
|
+
for (const [ws, session] of sessions) {
|
|
6
|
+
if (excludeSelf && ws === self.websocket) continue;
|
|
7
|
+
session.emitWireEvent(event, body);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
var SockaWebSocketSession = class {
|
|
11
|
+
constructor(websocket, sessions, config, init) {
|
|
12
|
+
this.websocket = websocket;
|
|
13
|
+
this.sessions = sessions;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.wireFormat = config.wireFormat ?? "json";
|
|
16
|
+
const create = config.createData ?? ((_i) => ({}));
|
|
17
|
+
this._data = create(init ?? {});
|
|
18
|
+
}
|
|
19
|
+
get data() {
|
|
20
|
+
return this._data;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Invokes the user {@link typeof SockaWebSocketSessionConfig.handleClose} callback.
|
|
24
|
+
* Server adapters should call this when the WebSocket closes, **before** deleting
|
|
25
|
+
* this session from the shared `sessions` map.
|
|
26
|
+
*/
|
|
27
|
+
async invokeHandleClose() {
|
|
28
|
+
await this.config.handleClose(this);
|
|
29
|
+
}
|
|
30
|
+
async handleRawMessage(rawMessage) {
|
|
31
|
+
if (this.wireFormat !== "json") {
|
|
32
|
+
await this.reportValidationError(
|
|
33
|
+
new Error("socka: unexpected JSON frame in msgpack mode"),
|
|
34
|
+
rawMessage
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const deserialize = this.config.deserializeJson ?? JSON.parse;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = deserialize(rawMessage);
|
|
42
|
+
} catch {
|
|
43
|
+
await this.reportValidationError(
|
|
44
|
+
new Error("socka: invalid JSON"),
|
|
45
|
+
rawMessage
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await this.dispatchAfterParsed(parsed, rawMessage);
|
|
50
|
+
}
|
|
51
|
+
async handleBinaryMessage(buffer) {
|
|
52
|
+
if (this.wireFormat !== "msgpack") {
|
|
53
|
+
await this.reportValidationError(
|
|
54
|
+
new Error("socka: unexpected binary frame in JSON mode"),
|
|
55
|
+
buffer
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = parseWirePayload(buffer, "msgpack");
|
|
62
|
+
} catch (err) {
|
|
63
|
+
await this.reportValidationError(
|
|
64
|
+
err instanceof Error ? err : new Error("socka: msgpack decode failed"),
|
|
65
|
+
buffer
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await this.dispatchAfterParsed(parsed, buffer);
|
|
70
|
+
}
|
|
71
|
+
async dispatchAfterParsed(parsed, originalWire) {
|
|
72
|
+
let decoded;
|
|
73
|
+
try {
|
|
74
|
+
decoded = decodeSockaWire(parsed);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof SockaWireError) {
|
|
77
|
+
await this.reportValidationError(err, originalWire);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
switch (decoded.kind) {
|
|
83
|
+
case "clientRequest":
|
|
84
|
+
await this.dispatchClientRequest(decoded.frame, originalWire);
|
|
85
|
+
return;
|
|
86
|
+
case "serverResponse":
|
|
87
|
+
case "serverError":
|
|
88
|
+
case "serverEvent":
|
|
89
|
+
await this.reportValidationError(
|
|
90
|
+
new Error("socka: unexpected server-originated frame from client"),
|
|
91
|
+
parsed
|
|
92
|
+
);
|
|
93
|
+
return;
|
|
94
|
+
default:
|
|
95
|
+
exhaustiveGuard(decoded);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async dispatchClientRequest(frame, _originalWire) {
|
|
99
|
+
const rpcName = frame.rpc;
|
|
100
|
+
const procedure = this.config.contract.calls[rpcName];
|
|
101
|
+
if (!procedure) {
|
|
102
|
+
const errorFrame = encodeServerError(
|
|
103
|
+
frame.id,
|
|
104
|
+
`Unknown call: ${rpcName}`
|
|
105
|
+
);
|
|
106
|
+
this.sendWireFrame(errorFrame);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let validatedInput;
|
|
110
|
+
if (procedure.input) {
|
|
111
|
+
try {
|
|
112
|
+
validatedInput = await parseStandardSchema(procedure.input, frame.body);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : "Input validation failed";
|
|
115
|
+
const errorFrame = encodeServerError(frame.id, msg);
|
|
116
|
+
this.sendWireFrame(errorFrame);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
if (procedure.input) {
|
|
123
|
+
const handler = this.config.handlers[rpcName];
|
|
124
|
+
result = await handler(validatedInput, this);
|
|
125
|
+
} else {
|
|
126
|
+
const handler = this.config.handlers[rpcName];
|
|
127
|
+
result = await handler(this);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.config.onHandlerError?.(err, rpcName, validatedInput, this);
|
|
131
|
+
const sockaErr = err instanceof SockaError ? err : new SockaError(
|
|
132
|
+
err instanceof Error ? err.message : "Handler failed"
|
|
133
|
+
);
|
|
134
|
+
const errorFrame = encodeServerError(frame.id, sockaErr.message);
|
|
135
|
+
this.sendWireFrame(errorFrame);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let validatedOutput;
|
|
139
|
+
try {
|
|
140
|
+
validatedOutput = await parseStandardSchema(procedure.output, result);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
const msg = err instanceof Error ? err.message : "Output validation failed";
|
|
143
|
+
const errorFrame = encodeServerError(frame.id, msg);
|
|
144
|
+
this.sendWireFrame(errorFrame);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const responseFrame = encodeServerResponse(
|
|
148
|
+
frame.id,
|
|
149
|
+
rpcName,
|
|
150
|
+
validatedOutput
|
|
151
|
+
);
|
|
152
|
+
this.sendWireFrame(responseFrame);
|
|
153
|
+
}
|
|
154
|
+
encodeOutgoing(frame) {
|
|
155
|
+
return encodeSockaWire(
|
|
156
|
+
frame,
|
|
157
|
+
this.wireFormat,
|
|
158
|
+
this.config.serializeJson ?? JSON.stringify
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
sendWireFrame(frame) {
|
|
162
|
+
if (this.websocket.readyState !== WebSocket.OPEN) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const encoded = this.encodeOutgoing(frame);
|
|
166
|
+
if (typeof encoded === "string") {
|
|
167
|
+
this.websocket.send(encoded);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const copy = new Uint8Array(encoded.byteLength);
|
|
171
|
+
copy.set(encoded);
|
|
172
|
+
this.websocket.send(copy.buffer);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Send a server event frame (wire). Prefer {@link emitPush} so
|
|
176
|
+
* payloads are validated against the contract.
|
|
177
|
+
*/
|
|
178
|
+
emitWireEvent(event, body) {
|
|
179
|
+
const frame = encodeServerEvent(event, body);
|
|
180
|
+
this.sendWireFrame(frame);
|
|
181
|
+
}
|
|
182
|
+
async emitPush(name, body) {
|
|
183
|
+
const schema = this.config.contract.pushes[name];
|
|
184
|
+
if (!schema) {
|
|
185
|
+
throw new Error(`socka: unknown push ${String(name)}`);
|
|
186
|
+
}
|
|
187
|
+
const validated = await parseStandardSchema(
|
|
188
|
+
schema,
|
|
189
|
+
body
|
|
190
|
+
);
|
|
191
|
+
this.emitWireEvent(name, validated);
|
|
192
|
+
}
|
|
193
|
+
async broadcastPush(name, body, excludeSelf = false) {
|
|
194
|
+
const schema = this.config.contract.pushes[name];
|
|
195
|
+
if (!schema) {
|
|
196
|
+
throw new Error(`socka: unknown push ${String(name)}`);
|
|
197
|
+
}
|
|
198
|
+
const validated = await parseStandardSchema(
|
|
199
|
+
schema,
|
|
200
|
+
body
|
|
201
|
+
);
|
|
202
|
+
broadcastSockaEventToPeers(
|
|
203
|
+
this.sessions,
|
|
204
|
+
this,
|
|
205
|
+
name,
|
|
206
|
+
validated,
|
|
207
|
+
excludeSelf
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
async reportValidationError(error, originalMessage) {
|
|
211
|
+
if (this.config.onValidationError) {
|
|
212
|
+
await this.config.onValidationError(error, originalMessage);
|
|
213
|
+
} else {
|
|
214
|
+
console.error("socka: validation error:", error, originalMessage);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
function runSockaSessionOnAttached(config, session) {
|
|
219
|
+
const cb = config.onAttached;
|
|
220
|
+
if (!cb) return;
|
|
221
|
+
try {
|
|
222
|
+
const result = cb(session);
|
|
223
|
+
void Promise.resolve(result).catch((error) => {
|
|
224
|
+
reportSockaError(config.reportError, {
|
|
225
|
+
kind: "serverOnAttached",
|
|
226
|
+
error
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
reportSockaError(config.reportError, { kind: "serverOnAttached", error });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached };
|
|
235
|
+
//# sourceMappingURL=chunk-45D4T232.js.map
|
|
236
|
+
//# sourceMappingURL=chunk-45D4T232.js.map
|