@firtoz/socka 2.0.0 → 2.1.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 +191 -40
- package/dist/{SockaWebSocketSession-Bru8yFcK.d.ts → SockaWebSocketSession-Cza7Fti-.d.ts} +87 -5
- package/dist/bun/index.d.ts +28 -3
- package/dist/bun/index.js +28 -5
- package/dist/bun/index.js.map +1 -1
- package/dist/{chunk-MZCQHJXY.js → chunk-2FNWVCP3.js} +27 -8
- package/dist/chunk-2FNWVCP3.js.map +1 -0
- package/dist/{chunk-AM7PB26G.js → chunk-H3S3435J.js} +125 -3
- package/dist/chunk-H3S3435J.js.map +1 -0
- package/dist/{chunk-45D4T232.js → chunk-JVLUA3Q5.js} +64 -6
- package/dist/chunk-JVLUA3Q5.js.map +1 -0
- package/dist/chunk-KQO5AVKA.js +8 -0
- package/dist/chunk-KQO5AVKA.js.map +1 -0
- package/dist/client/index.d.ts +59 -3
- package/dist/client/index.js +2 -2
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +1 -1
- package/dist/do/index.d.ts +20 -2
- package/dist/do/index.js +35 -2
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +2 -2
- package/dist/hono/cloudflare-workers.js +4 -3
- package/dist/hono/cloudflare-workers.js.map +1 -1
- package/dist/hono/index.d.ts +20 -4
- package/dist/hono/index.js +5 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/react/index.d.ts +43 -4
- package/dist/react/index.js +103 -9
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.d.ts +17 -4
- package/dist/server/index.js +24 -4
- package/dist/server/index.js.map +1 -1
- package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-ixTynx4w.d.ts} +8 -1
- package/dist/test/index.d.ts +11 -0
- package/dist/test/index.js +84 -0
- package/dist/test/index.js.map +1 -0
- package/docs/README.md +15 -6
- package/docs/auth.md +27 -0
- package/docs/backpressure.md +16 -0
- package/docs/client.md +44 -3
- package/docs/comparison.md +1 -1
- package/docs/durable-objects.md +2 -2
- package/docs/getting-started.md +143 -84
- package/docs/history.md +26 -0
- package/docs/internals.md +56 -0
- package/docs/lifecycle.md +3 -3
- package/docs/multi-room.md +10 -8
- package/docs/peers.md +11 -7
- package/docs/presence.md +43 -0
- package/docs/{events.md → pushes.md} +1 -1
- package/docs/recipes.md +78 -0
- package/docs/reconnection.md +44 -0
- package/docs/reference.md +21 -30
- package/docs/server.md +14 -1
- package/docs/testing.md +20 -0
- package/docs/wire-format.md +25 -0
- package/examples/minimal-socka.ts +56 -3
- package/package.json +14 -10
- package/dist/chunk-45D4T232.js.map +0 -1
- package/dist/chunk-AM7PB26G.js.map +0 -1
- package/dist/chunk-MZCQHJXY.js.map +0 -1
package/dist/react/index.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { SockaSession } from '../chunk-
|
|
1
|
+
import { SockaSession } from '../chunk-H3S3435J.js';
|
|
2
2
|
import '../chunk-YMT4HAH7.js';
|
|
3
|
-
import '../chunk-
|
|
3
|
+
import '../chunk-2FNWVCP3.js';
|
|
4
4
|
import { createContext, useRef, useState, useEffect, useMemo, useContext } from 'react';
|
|
5
5
|
import { jsx } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
function useSocka(options, deps) {
|
|
8
|
-
const { onOpen, onClose, ...restOptions } = options;
|
|
8
|
+
const { onOpen, onClose, onReconnecting, onReconnected, ...restOptions } = options;
|
|
9
9
|
const onOpenRef = useRef(onOpen);
|
|
10
10
|
onOpenRef.current = onOpen;
|
|
11
11
|
const onCloseRef = useRef(onClose);
|
|
12
12
|
onCloseRef.current = onClose;
|
|
13
|
+
const onReconnectingRef = useRef(onReconnecting);
|
|
14
|
+
onReconnectingRef.current = onReconnecting;
|
|
15
|
+
const onReconnectedRef = useRef(onReconnected);
|
|
16
|
+
onReconnectedRef.current = onReconnected;
|
|
13
17
|
const [ready, setReady] = useState(false);
|
|
18
|
+
const [status, setStatus] = useState(
|
|
19
|
+
() => options.autoConnect === false ? "idle" : "connecting"
|
|
20
|
+
);
|
|
21
|
+
const [reconnectAttempt, setReconnectAttempt] = useState(0);
|
|
14
22
|
const sessionRef = useRef(null);
|
|
15
23
|
useEffect(() => {
|
|
16
24
|
let cancelled = false;
|
|
17
25
|
setReady(false);
|
|
26
|
+
setReconnectAttempt(0);
|
|
18
27
|
const session = new SockaSession({
|
|
19
28
|
...restOptions,
|
|
20
29
|
onOpen: (event) => {
|
|
@@ -28,8 +37,19 @@ function useSocka(options, deps) {
|
|
|
28
37
|
setReady(false);
|
|
29
38
|
}
|
|
30
39
|
onCloseRef.current?.(event);
|
|
40
|
+
},
|
|
41
|
+
onReconnecting: (info) => {
|
|
42
|
+
setReconnectAttempt(info.attempt);
|
|
43
|
+
onReconnectingRef.current?.(info);
|
|
44
|
+
},
|
|
45
|
+
onReconnected: (info) => {
|
|
46
|
+
setReconnectAttempt(0);
|
|
47
|
+
onReconnectedRef.current?.(info);
|
|
31
48
|
}
|
|
32
49
|
});
|
|
50
|
+
const unsubStatus = session.onStatusChange((s) => {
|
|
51
|
+
if (!cancelled) setStatus(s);
|
|
52
|
+
});
|
|
33
53
|
sessionRef.current = session;
|
|
34
54
|
void session.client.connect().then(
|
|
35
55
|
() => {
|
|
@@ -42,12 +62,19 @@ function useSocka(options, deps) {
|
|
|
42
62
|
);
|
|
43
63
|
return () => {
|
|
44
64
|
cancelled = true;
|
|
65
|
+
unsubStatus();
|
|
45
66
|
sessionRef.current = null;
|
|
46
67
|
session.rejectAllPending(new Error("WebSocket closed"));
|
|
47
68
|
session.close();
|
|
48
69
|
};
|
|
49
70
|
}, deps);
|
|
50
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
ready,
|
|
73
|
+
sessionRef,
|
|
74
|
+
status,
|
|
75
|
+
reconnecting: status === "reconnecting",
|
|
76
|
+
reconnectAttempt
|
|
77
|
+
};
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
// src/react/useSockaSession.ts
|
|
@@ -69,7 +96,7 @@ function createSockaSendProxyFromSession(contract, sessionRef) {
|
|
|
69
96
|
}
|
|
70
97
|
function useSockaSession(contract, options, deps) {
|
|
71
98
|
const { pushHandlers, ...sockaOpts } = options;
|
|
72
|
-
const { ready, sessionRef } = useSocka(
|
|
99
|
+
const { ready, sessionRef, status, reconnecting, reconnectAttempt } = useSocka(
|
|
73
100
|
{
|
|
74
101
|
...sockaOpts,
|
|
75
102
|
contract,
|
|
@@ -81,7 +108,68 @@ function useSockaSession(contract, options, deps) {
|
|
|
81
108
|
() => createSockaSendProxyFromSession(contract, sessionRef),
|
|
82
109
|
[contract, sessionRef]
|
|
83
110
|
);
|
|
84
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
ready,
|
|
113
|
+
send,
|
|
114
|
+
sessionRef,
|
|
115
|
+
status,
|
|
116
|
+
reconnecting,
|
|
117
|
+
reconnectAttempt
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function useSockaPresence(sessionRef, ready, options, deps) {
|
|
121
|
+
const [users, setUsers] = useState([]);
|
|
122
|
+
const [selfUserId, setSelfUserId] = useState();
|
|
123
|
+
const [loading, setLoading] = useState(true);
|
|
124
|
+
const optionsRef = useRef(options);
|
|
125
|
+
optionsRef.current = options;
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!ready) {
|
|
128
|
+
setUsers([]);
|
|
129
|
+
setSelfUserId(void 0);
|
|
130
|
+
setLoading(true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const s = sessionRef.current;
|
|
134
|
+
if (!s) {
|
|
135
|
+
setLoading(false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const o = optionsRef.current;
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
void (async () => {
|
|
141
|
+
setLoading(true);
|
|
142
|
+
try {
|
|
143
|
+
const snap = await o.snapshot();
|
|
144
|
+
if (cancelled) return;
|
|
145
|
+
setSelfUserId(snap.selfUserId);
|
|
146
|
+
setUsers(o.sortUsers ? [...snap.users].sort(o.sortUsers) : snap.users);
|
|
147
|
+
} finally {
|
|
148
|
+
if (!cancelled) setLoading(false);
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
const onJoin = (p) => {
|
|
152
|
+
const cur = optionsRef.current;
|
|
153
|
+
const u = cur.mapJoinUser(p);
|
|
154
|
+
setUsers((prev) => {
|
|
155
|
+
const next = prev.filter((x) => x.userId !== u.userId);
|
|
156
|
+
const merged = [...next, u];
|
|
157
|
+
return cur.sortUsers ? merged.sort(cur.sortUsers) : merged;
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const onLeave = (p) => {
|
|
161
|
+
const id = optionsRef.current.mapLeaveUserId(p);
|
|
162
|
+
setUsers((prev) => prev.filter((x) => x.userId !== id));
|
|
163
|
+
};
|
|
164
|
+
s.subscribe.on(o.joinPush, onJoin);
|
|
165
|
+
s.subscribe.on(o.leavePush, onLeave);
|
|
166
|
+
return () => {
|
|
167
|
+
cancelled = true;
|
|
168
|
+
s.subscribe.off(o.joinPush, onJoin);
|
|
169
|
+
s.subscribe.off(o.leavePush, onLeave);
|
|
170
|
+
};
|
|
171
|
+
}, [ready, sessionRef, ...deps]);
|
|
172
|
+
return { users, selfUserId, loading };
|
|
85
173
|
}
|
|
86
174
|
var SockaSessionContext = createContext(null);
|
|
87
175
|
function contextMatchesContract(ctx, contract) {
|
|
@@ -93,7 +181,10 @@ function SockaSessionProvider(props) {
|
|
|
93
181
|
const merged = {
|
|
94
182
|
contract,
|
|
95
183
|
ready: value.ready,
|
|
96
|
-
sessionRef: value.sessionRef
|
|
184
|
+
sessionRef: value.sessionRef,
|
|
185
|
+
status: value.status,
|
|
186
|
+
reconnecting: value.reconnecting,
|
|
187
|
+
reconnectAttempt: value.reconnectAttempt
|
|
97
188
|
};
|
|
98
189
|
return /* @__PURE__ */ jsx(SockaSessionContext.Provider, { value: merged, children });
|
|
99
190
|
}
|
|
@@ -117,10 +208,13 @@ function useSockaSessionContext(contract) {
|
|
|
117
208
|
return {
|
|
118
209
|
ready: ctx.ready,
|
|
119
210
|
send,
|
|
120
|
-
sessionRef: ctx.sessionRef
|
|
211
|
+
sessionRef: ctx.sessionRef,
|
|
212
|
+
status: ctx.status,
|
|
213
|
+
reconnecting: ctx.reconnecting,
|
|
214
|
+
reconnectAttempt: ctx.reconnectAttempt
|
|
121
215
|
};
|
|
122
216
|
}
|
|
123
217
|
|
|
124
|
-
export { SockaSessionProvider, createSockaSendProxyFromSession, useSocka, useSockaSession, useSockaSessionContext };
|
|
218
|
+
export { SockaSessionProvider, createSockaSendProxyFromSession, useSocka, useSockaPresence, useSockaSession, useSockaSessionContext };
|
|
125
219
|
//# sourceMappingURL=index.js.map
|
|
126
220
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/useSocka.ts","../../src/react/useSockaSession.ts","../../src/react/SockaSessionProvider.tsx"],"names":["useMemo"],"mappings":"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","file":"index.js","sourcesContent":["import type { DependencyList, RefObject } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { SockaSession, type SockaSessionOptions } from \"../client/SockaSession\";\n\n/** Options for {@link useSocka}. */\nexport type UseSockaOptions<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = SockaSessionOptions<TContract>;\n\n/**\n * Connects a {@link SockaSession} in an effect: rejects all pending calls and closes\n * the socket on cleanup or when `deps` change.\n */\nexport function useSocka<TContract extends SockaContract<SockaContractConfig>>(\n\toptions: UseSockaOptions<TContract>,\n\tdeps: DependencyList,\n): {\n\tready: boolean;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n} {\n\tconst { onOpen, onClose, ...restOptions } = options;\n\n\tconst onOpenRef = useRef(onOpen);\n\tonOpenRef.current = onOpen;\n\tconst onCloseRef = useRef(onClose);\n\tonCloseRef.current = onClose;\n\n\tconst [ready, setReady] = useState(false);\n\tconst sessionRef = useRef<SockaSession<TContract> | null>(null);\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\t\tsetReady(false);\n\n\t\tconst session = new SockaSession({\n\t\t\t...restOptions,\n\t\t\tonOpen: (event) => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(true);\n\t\t\t\t}\n\t\t\t\tonOpenRef.current?.(event);\n\t\t\t},\n\t\t\tonClose: (event) => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(false);\n\t\t\t\t}\n\t\t\t\tonCloseRef.current?.(event);\n\t\t\t},\n\t\t});\n\n\t\tsessionRef.current = session;\n\t\tvoid session.client.connect().then(\n\t\t\t() => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(true);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t/* connect failure: onError / onClose handle UX */\n\t\t\t},\n\t\t);\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tsessionRef.current = null;\n\t\t\tsession.rejectAllPending(new Error(\"WebSocket closed\"));\n\t\t\tsession.close();\n\t\t};\n\t}, deps); // deps: explicit reconnect contract for useSocka (see hook docs)\n\n\treturn { ready, sessionRef };\n}\n","import { useMemo, type DependencyList, type RefObject } from \"react\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport type { InferSockaSend, InferSockaPushHandlers } from \"../core/contract\";\nimport type { SockaSession } from \"../client/SockaSession\";\nimport { useSocka, type UseSockaOptions } from \"./useSocka\";\n\nexport type UseSockaSessionOptions<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = Omit<UseSockaOptions<TContract>, \"contract\" | \"pushHandlers\"> & {\n\tpushHandlers?: Partial<InferSockaPushHandlers<TContract>>;\n};\n\n/**\n * Builds the same typed `send` object as {@link useSockaSession} from a live session ref.\n * Used by {@link useSockaSessionContext} so consumers do not open extra connections.\n */\nexport function createSockaSendProxyFromSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n\tsessionRef: RefObject<SockaSession<TContract> | null>,\n): InferSockaSend<TContract> {\n\tconst proxy: Record<string, unknown> = {};\n\tfor (const name of Object.keys(contract.calls)) {\n\t\tproxy[name] = (...args: unknown[]) => {\n\t\t\tconst session = sessionRef.current;\n\t\t\tif (!session) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(\"socka: session ref is null; cannot send\"),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst fn = session.send[name as keyof typeof session.send] as (\n\t\t\t\t...a: unknown[]\n\t\t\t) => Promise<unknown>;\n\t\t\treturn fn.apply(session.send, args);\n\t\t};\n\t}\n\treturn proxy as InferSockaSend<TContract>;\n}\n\n/**\n * ```tsx\n * const { ready, send } = useSockaSession(myContract, { url }, deps);\n * await send.echo({ text: \"hi\" });\n * ```\n */\nexport function useSockaSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n\toptions: UseSockaSessionOptions<TContract>,\n\tdeps: DependencyList,\n): {\n\tready: boolean;\n\tsend: InferSockaSend<TContract>;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n} {\n\tconst { pushHandlers, ...sockaOpts } = options;\n\tconst { ready, sessionRef } = useSocka(\n\t\t{\n\t\t\t...sockaOpts,\n\t\t\tcontract,\n\t\t\tpushHandlers,\n\t\t},\n\t\tdeps,\n\t);\n\n\tconst send = useMemo(\n\t\t() => createSockaSendProxyFromSession(contract, sessionRef),\n\t\t[contract, sessionRef],\n\t);\n\n\treturn { ready, send, sessionRef };\n}\n","import type { DependencyList, ReactElement, ReactNode, RefObject } from \"react\";\nimport { createContext, useContext, useMemo } from \"react\";\nimport type { SockaSession } from \"../client/SockaSession\";\nimport type {\n\tSockaContract,\n\tSockaContractConfig,\n\tInferSockaSend,\n} from \"../core/contract\";\nimport {\n\tcreateSockaSendProxyFromSession,\n\tuseSockaSession,\n\ttype UseSockaSessionOptions,\n} from \"./useSockaSession\";\n\ntype AnySockaContract = SockaContract<SockaContractConfig>;\n\n/**\n * Session slice stored on React context by {@link SockaSessionProvider}. The typed\n * `send` object is built in {@link useSockaSessionContext} (same as {@link useSockaSession})\n * so children do not open duplicate WebSockets.\n */\nexport type SockaSessionContextValue<\n\tTContract extends SockaContract<SockaContractConfig> = AnySockaContract,\n> = {\n\treadonly contract: TContract;\n\treadonly ready: boolean;\n\treadonly sessionRef: RefObject<SockaSession<TContract> | null>;\n};\n\nconst SockaSessionContext =\n\tcreateContext<SockaSessionContextValue<AnySockaContract> | null>(null);\n\nfunction contextMatchesContract<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tctx: SockaSessionContextValue<AnySockaContract>,\n\tcontract: TContract,\n): ctx is SockaSessionContextValue<TContract> {\n\treturn ctx.contract === contract;\n}\n\nexport type SockaSessionProviderProps<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = {\n\treadonly contract: TContract;\n\treadonly deps: DependencyList;\n\treadonly children: ReactNode;\n} & UseSockaSessionOptions<TContract>;\n\n/**\n * Owns a single {@link SockaSession} / WebSocket and exposes it to descendants via\n * {@link useSockaSessionContext}. Mount once per connection (e.g. layout); avoid\n * calling {@link useSockaSession} in every leaf—use the context hook instead.\n */\nexport function SockaSessionProvider<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(props: SockaSessionProviderProps<TContract>): ReactElement {\n\tconst { contract, deps, children, ...sessionOptions } = props;\n\tconst value = useSockaSession(contract, sessionOptions, deps);\n\tconst merged: SockaSessionContextValue<TContract> = {\n\t\tcontract,\n\t\tready: value.ready,\n\t\tsessionRef: value.sessionRef,\n\t};\n\treturn (\n\t\t<SockaSessionContext.Provider value={merged}>\n\t\t\t{children}\n\t\t</SockaSessionContext.Provider>\n\t);\n}\n\nSockaSessionProvider.displayName = \"SockaSessionProvider\";\n\n/**\n * Reads the socka session from the nearest {@link SockaSessionProvider}.\n * Pass the **same** `contract` reference as the provider for typing and validation.\n */\nexport function useSockaSessionContext<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n): {\n\tready: boolean;\n\tsend: InferSockaSend<TContract>;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n} {\n\tconst ctx = useContext(SockaSessionContext);\n\tif (ctx === null) {\n\t\tthrow new Error(\n\t\t\t\"useSockaSessionContext must be used within a SockaSessionProvider\",\n\t\t);\n\t}\n\tif (!contextMatchesContract(ctx, contract)) {\n\t\tthrow new Error(\n\t\t\t\"useSockaSessionContext: `contract` must be the same reference as SockaSessionProvider's `contract`\",\n\t\t);\n\t}\n\tconst send = useMemo(\n\t\t() => createSockaSendProxyFromSession(contract, ctx.sessionRef),\n\t\t[contract, ctx.sessionRef],\n\t);\n\treturn {\n\t\tready: ctx.ready,\n\t\tsend,\n\t\tsessionRef: ctx.sessionRef,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/useSocka.ts","../../src/react/useSockaSession.ts","../../src/react/useSockaPresence.ts","../../src/react/SockaSessionProvider.tsx"],"names":["useState","useRef","useEffect","useMemo"],"mappings":"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","file":"index.js","sourcesContent":["import type { DependencyList, RefObject } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { SockaSession, type SockaSessionOptions } from \"../client/SockaSession\";\nimport type { SockaConnectionStatus } from \"../client/SockaWebSocketClient\";\n\n/** Options for {@link useSocka}. */\nexport type UseSockaOptions<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = SockaSessionOptions<TContract>;\n\n/**\n * Connects a {@link SockaSession} in an effect: rejects all pending calls and closes\n * the socket on cleanup or when `deps` change.\n */\nexport function useSocka<TContract extends SockaContract<SockaContractConfig>>(\n\toptions: UseSockaOptions<TContract>,\n\tdeps: DependencyList,\n): {\n\tready: boolean;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n\tstatus: SockaConnectionStatus;\n\treconnecting: boolean;\n\treconnectAttempt: number;\n} {\n\tconst { onOpen, onClose, onReconnecting, onReconnected, ...restOptions } =\n\t\toptions;\n\n\tconst onOpenRef = useRef(onOpen);\n\tonOpenRef.current = onOpen;\n\tconst onCloseRef = useRef(onClose);\n\tonCloseRef.current = onClose;\n\tconst onReconnectingRef = useRef(onReconnecting);\n\tonReconnectingRef.current = onReconnecting;\n\tconst onReconnectedRef = useRef(onReconnected);\n\tonReconnectedRef.current = onReconnected;\n\n\tconst [ready, setReady] = useState(false);\n\tconst [status, setStatus] = useState<SockaConnectionStatus>(() =>\n\t\toptions.autoConnect === false ? \"idle\" : \"connecting\",\n\t);\n\tconst [reconnectAttempt, setReconnectAttempt] = useState(0);\n\tconst sessionRef = useRef<SockaSession<TContract> | null>(null);\n\n\tuseEffect(() => {\n\t\tlet cancelled = false;\n\t\tsetReady(false);\n\t\tsetReconnectAttempt(0);\n\n\t\tconst session = new SockaSession({\n\t\t\t...restOptions,\n\t\t\tonOpen: (event) => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(true);\n\t\t\t\t}\n\t\t\t\tonOpenRef.current?.(event);\n\t\t\t},\n\t\t\tonClose: (event) => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(false);\n\t\t\t\t}\n\t\t\t\tonCloseRef.current?.(event);\n\t\t\t},\n\t\t\tonReconnecting: (info) => {\n\t\t\t\tsetReconnectAttempt(info.attempt);\n\t\t\t\tonReconnectingRef.current?.(info);\n\t\t\t},\n\t\t\tonReconnected: (info) => {\n\t\t\t\tsetReconnectAttempt(0);\n\t\t\t\tonReconnectedRef.current?.(info);\n\t\t\t},\n\t\t});\n\n\t\tconst unsubStatus = session.onStatusChange((s) => {\n\t\t\tif (!cancelled) setStatus(s);\n\t\t});\n\n\t\tsessionRef.current = session;\n\t\tvoid session.client.connect().then(\n\t\t\t() => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetReady(true);\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {\n\t\t\t\t/* connect failure: onError / onClose handle UX */\n\t\t\t},\n\t\t);\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tunsubStatus();\n\t\t\tsessionRef.current = null;\n\t\t\tsession.rejectAllPending(new Error(\"WebSocket closed\"));\n\t\t\tsession.close();\n\t\t};\n\t}, deps); // deps: explicit reconnect contract for useSocka (see hook docs)\n\n\treturn {\n\t\tready,\n\t\tsessionRef,\n\t\tstatus,\n\t\treconnecting: status === \"reconnecting\",\n\t\treconnectAttempt,\n\t};\n}\n","import { useMemo, type DependencyList, type RefObject } from \"react\";\nimport type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport type { InferSockaSend, InferSockaPushHandlers } from \"../core/contract\";\nimport type { SockaSession } from \"../client/SockaSession\";\nimport type { SockaConnectionStatus } from \"../client/SockaWebSocketClient\";\nimport { useSocka, type UseSockaOptions } from \"./useSocka\";\n\nexport type UseSockaSessionOptions<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = Omit<UseSockaOptions<TContract>, \"contract\" | \"pushHandlers\"> & {\n\tpushHandlers?: Partial<InferSockaPushHandlers<TContract>>;\n};\n\n/**\n * Builds the same typed `send` object as {@link useSockaSession} from a live session ref.\n * Used by {@link useSockaSessionContext} so consumers do not open extra connections.\n */\nexport function createSockaSendProxyFromSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n\tsessionRef: RefObject<SockaSession<TContract> | null>,\n): InferSockaSend<TContract> {\n\tconst proxy: Record<string, unknown> = {};\n\tfor (const name of Object.keys(contract.calls)) {\n\t\tproxy[name] = (...args: unknown[]) => {\n\t\t\tconst session = sessionRef.current;\n\t\t\tif (!session) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(\"socka: session ref is null; cannot send\"),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst fn = session.send[name as keyof typeof session.send] as (\n\t\t\t\t...a: unknown[]\n\t\t\t) => Promise<unknown>;\n\t\t\treturn fn.apply(session.send, args);\n\t\t};\n\t}\n\treturn proxy as InferSockaSend<TContract>;\n}\n\n/**\n * ```tsx\n * const { ready, send } = useSockaSession(myContract, { url }, deps);\n * await send.echo({ message: \"hi\" });\n * ```\n */\nexport function useSockaSession<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n\toptions: UseSockaSessionOptions<TContract>,\n\tdeps: DependencyList,\n): {\n\tready: boolean;\n\tsend: InferSockaSend<TContract>;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n\tstatus: SockaConnectionStatus;\n\treconnecting: boolean;\n\treconnectAttempt: number;\n} {\n\tconst { pushHandlers, ...sockaOpts } = options;\n\tconst { ready, sessionRef, status, reconnecting, reconnectAttempt } =\n\t\tuseSocka(\n\t\t\t{\n\t\t\t\t...sockaOpts,\n\t\t\t\tcontract,\n\t\t\t\tpushHandlers,\n\t\t\t},\n\t\t\tdeps,\n\t\t);\n\n\tconst send = useMemo(\n\t\t() => createSockaSendProxyFromSession(contract, sessionRef),\n\t\t[contract, sessionRef],\n\t);\n\n\treturn {\n\t\tready,\n\t\tsend,\n\t\tsessionRef,\n\t\tstatus,\n\t\treconnecting,\n\t\treconnectAttempt,\n\t};\n}\n","import type { DependencyList, RefObject } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { SockaSession } from \"../client/SockaSession\";\nimport type {\n\tInferSockaPushPayload,\n\tSockaContract,\n\tSockaContractConfig,\n} from \"../core/contract\";\n\nexport type SockaPresenceOptions<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTUser extends { userId: string },\n\tKJoin extends keyof TContract[\"pushes\"] & string,\n\tKLeave extends keyof TContract[\"pushes\"] & string,\n> = {\n\tsnapshot: () => Promise<{ selfUserId: string; users: TUser[] }>;\n\tjoinPush: KJoin;\n\tleavePush: KLeave;\n\tmapJoinUser: (p: InferSockaPushPayload<TContract, KJoin>) => TUser;\n\tmapLeaveUserId: (p: InferSockaPushPayload<TContract, KLeave>) => string;\n\t/** Optional display order after each update (e.g. by `displayName`). */\n\tsortUsers?: (a: TUser, b: TUser) => number;\n};\n\n/**\n * Loads a presence snapshot RPC once, then merges **`joinPush`** / **`leavePush`** deltas.\n * Pass the same **`deps`** you use for {@link useSocka} when room identity changes.\n * Options are read from a ref so you do not need to memoize the **`options`** object.\n */\nexport function useSockaPresence<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTUser extends { userId: string },\n\tKJoin extends keyof TContract[\"pushes\"] & string,\n\tKLeave extends keyof TContract[\"pushes\"] & string,\n>(\n\tsessionRef: RefObject<SockaSession<TContract> | null>,\n\tready: boolean,\n\toptions: SockaPresenceOptions<TContract, TUser, KJoin, KLeave>,\n\tdeps: DependencyList,\n): {\n\tusers: TUser[];\n\tselfUserId: string | undefined;\n\tloading: boolean;\n} {\n\tconst [users, setUsers] = useState<TUser[]>([]);\n\tconst [selfUserId, setSelfUserId] = useState<string | undefined>();\n\tconst [loading, setLoading] = useState(true);\n\tconst optionsRef = useRef(options);\n\toptionsRef.current = options;\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\tsetUsers([]);\n\t\t\tsetSelfUserId(undefined);\n\t\t\tsetLoading(true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst s = sessionRef.current;\n\t\tif (!s) {\n\t\t\tsetLoading(false);\n\t\t\treturn;\n\t\t}\n\n\t\tconst o = optionsRef.current;\n\t\tlet cancelled = false;\n\n\t\tvoid (async () => {\n\t\t\tsetLoading(true);\n\t\t\ttry {\n\t\t\t\tconst snap = await o.snapshot();\n\t\t\t\tif (cancelled) return;\n\t\t\t\tsetSelfUserId(snap.selfUserId);\n\t\t\t\tsetUsers(o.sortUsers ? [...snap.users].sort(o.sortUsers) : snap.users);\n\t\t\t} finally {\n\t\t\t\tif (!cancelled) setLoading(false);\n\t\t\t}\n\t\t})();\n\n\t\tconst onJoin = (p: InferSockaPushPayload<TContract, KJoin>) => {\n\t\t\tconst cur = optionsRef.current;\n\t\t\tconst u = cur.mapJoinUser(p);\n\t\t\tsetUsers((prev) => {\n\t\t\t\tconst next = prev.filter((x) => x.userId !== u.userId);\n\t\t\t\tconst merged = [...next, u];\n\t\t\t\treturn cur.sortUsers ? merged.sort(cur.sortUsers) : merged;\n\t\t\t});\n\t\t};\n\n\t\tconst onLeave = (p: InferSockaPushPayload<TContract, KLeave>) => {\n\t\t\tconst id = optionsRef.current.mapLeaveUserId(p);\n\t\t\tsetUsers((prev) => prev.filter((x) => x.userId !== id));\n\t\t};\n\n\t\ts.subscribe.on(o.joinPush, onJoin);\n\t\ts.subscribe.on(o.leavePush, onLeave);\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\ts.subscribe.off(o.joinPush, onJoin);\n\t\t\ts.subscribe.off(o.leavePush, onLeave);\n\t\t};\n\t}, [ready, sessionRef, ...deps]);\n\n\treturn { users, selfUserId, loading };\n}\n","import type { DependencyList, ReactElement, ReactNode, RefObject } from \"react\";\nimport { createContext, useContext, useMemo } from \"react\";\nimport type { SockaSession } from \"../client/SockaSession\";\nimport type { SockaConnectionStatus } from \"../client/SockaWebSocketClient\";\nimport type {\n\tSockaContract,\n\tSockaContractConfig,\n\tInferSockaSend,\n} from \"../core/contract\";\nimport {\n\tcreateSockaSendProxyFromSession,\n\tuseSockaSession,\n\ttype UseSockaSessionOptions,\n} from \"./useSockaSession\";\n\ntype AnySockaContract = SockaContract<SockaContractConfig>;\n\n/**\n * Session slice stored on React context by {@link SockaSessionProvider}. The typed\n * `send` object is built in {@link useSockaSessionContext} (same as {@link useSockaSession})\n * so children do not open duplicate WebSockets.\n */\nexport type SockaSessionContextValue<\n\tTContract extends SockaContract<SockaContractConfig> = AnySockaContract,\n> = {\n\treadonly contract: TContract;\n\treadonly ready: boolean;\n\treadonly sessionRef: RefObject<SockaSession<TContract> | null>;\n\treadonly status: SockaConnectionStatus;\n\treadonly reconnecting: boolean;\n\treadonly reconnectAttempt: number;\n};\n\nconst SockaSessionContext =\n\tcreateContext<SockaSessionContextValue<AnySockaContract> | null>(null);\n\nfunction contextMatchesContract<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tctx: SockaSessionContextValue<AnySockaContract>,\n\tcontract: TContract,\n): ctx is SockaSessionContextValue<TContract> {\n\treturn ctx.contract === contract;\n}\n\nexport type SockaSessionProviderProps<\n\tTContract extends SockaContract<SockaContractConfig>,\n> = {\n\treadonly contract: TContract;\n\treadonly deps: DependencyList;\n\treadonly children: ReactNode;\n} & UseSockaSessionOptions<TContract>;\n\n/**\n * Owns a single {@link SockaSession} / WebSocket and exposes it to descendants via\n * {@link useSockaSessionContext}. Mount once per connection (e.g. layout); avoid\n * calling {@link useSockaSession} in every leaf—use the context hook instead.\n */\nexport function SockaSessionProvider<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(props: SockaSessionProviderProps<TContract>): ReactElement {\n\tconst { contract, deps, children, ...sessionOptions } = props;\n\tconst value = useSockaSession(contract, sessionOptions, deps);\n\tconst merged: SockaSessionContextValue<TContract> = {\n\t\tcontract,\n\t\tready: value.ready,\n\t\tsessionRef: value.sessionRef,\n\t\tstatus: value.status,\n\t\treconnecting: value.reconnecting,\n\t\treconnectAttempt: value.reconnectAttempt,\n\t};\n\treturn (\n\t\t<SockaSessionContext.Provider value={merged}>\n\t\t\t{children}\n\t\t</SockaSessionContext.Provider>\n\t);\n}\n\nSockaSessionProvider.displayName = \"SockaSessionProvider\";\n\n/**\n * Reads the socka session from the nearest {@link SockaSessionProvider}.\n * Pass the **same** `contract` reference as the provider for typing and validation.\n */\nexport function useSockaSessionContext<\n\tTContract extends SockaContract<SockaContractConfig>,\n>(\n\tcontract: TContract,\n): {\n\tready: boolean;\n\tsend: InferSockaSend<TContract>;\n\tsessionRef: RefObject<SockaSession<TContract> | null>;\n\tstatus: SockaConnectionStatus;\n\treconnecting: boolean;\n\treconnectAttempt: number;\n} {\n\tconst ctx = useContext(SockaSessionContext);\n\tif (ctx === null) {\n\t\tthrow new Error(\n\t\t\t\"useSockaSessionContext must be used within a SockaSessionProvider\",\n\t\t);\n\t}\n\tif (!contextMatchesContract(ctx, contract)) {\n\t\tthrow new Error(\n\t\t\t\"useSockaSessionContext: `contract` must be the same reference as SockaSessionProvider's `contract`\",\n\t\t);\n\t}\n\tconst send = useMemo(\n\t\t() => createSockaSendProxyFromSession(contract, ctx.sessionRef),\n\t\t[contract, ctx.sessionRef],\n\t);\n\treturn {\n\t\tready: ctx.ready,\n\t\tsend,\n\t\tsessionRef: ctx.sessionRef,\n\t\tstatus: ctx.status,\n\t\treconnecting: ctx.reconnecting,\n\t\treconnectAttempt: ctx.reconnectAttempt,\n\t};\n}\n"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-
|
|
1
|
+
import { a as SockaWebSocketSession, b as SockaWebSocketSessionConfig, c as SockaWebSocketInit } from '../SockaWebSocketSession-Cza7Fti-.js';
|
|
2
|
+
export { f as SockaEmitCapable, d as SockaPushSession, S as SockaStrictWebSocketInit, e as broadcastSockaEventToPeers, r as runSockaSessionOnAttached } from '../SockaWebSocketSession-Cza7Fti-.js';
|
|
3
|
+
import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-ixTynx4w.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
|
6
6
|
type AttachedSockaWebSocket<TContract extends SockaContract<SockaContractConfig>, TData> = {
|
|
@@ -24,4 +24,17 @@ declare function attachSockaWebSocket<TContract extends SockaContract<SockaContr
|
|
|
24
24
|
*/
|
|
25
25
|
declare function dispatchSockaInboundMessage<TContract extends SockaContract<SockaContractConfig>, TData>(session: SockaWebSocketSession<TContract, TData>, wireFormat: SockaWireFormat, data: MessageEvent["data"]): Promise<void>;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
type SockaRoomBundle<TContract extends SockaContract<SockaContractConfig>, TData> = {
|
|
28
|
+
sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;
|
|
29
|
+
config: SockaWebSocketSessionConfig<TContract, TData>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Per-room {@link SockaWebSocketSession} maps and configs for Bun/Hono multi-room
|
|
33
|
+
* apps (one bundle per `roomId`).
|
|
34
|
+
*/
|
|
35
|
+
declare function createSockaRoomRegistry<TContract extends SockaContract<SockaContractConfig>, TData>(makeConfig: (roomId: string, sessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>) => SockaWebSocketSessionConfig<TContract, TData>): {
|
|
36
|
+
get(roomId: string): SockaRoomBundle<TContract, TData>;
|
|
37
|
+
readonly rooms: ReadonlyMap<string, SockaRoomBundle<TContract, TData>>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { type AttachedSockaWebSocket, type SockaRoomBundle, SockaWebSocketInit, SockaWebSocketSession, SockaWebSocketSessionConfig, attachSockaWebSocket, createSockaRoomRegistry, dispatchSockaInboundMessage };
|
package/dist/server/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { dispatchSockaInboundMessage } from '../chunk-5WQTYLIC.js';
|
|
2
2
|
export { dispatchSockaInboundMessage } from '../chunk-5WQTYLIC.js';
|
|
3
|
-
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-
|
|
4
|
-
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-
|
|
5
|
-
import { reportSockaError } from '../chunk-
|
|
3
|
+
import { SockaWebSocketSession, runSockaSessionOnAttached } from '../chunk-JVLUA3Q5.js';
|
|
4
|
+
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-JVLUA3Q5.js';
|
|
5
|
+
import { reportSockaError } from '../chunk-2FNWVCP3.js';
|
|
6
6
|
|
|
7
7
|
// src/server/attachSockaWebSocket.ts
|
|
8
8
|
function attachSockaWebSocket(websocket, sessions, config, init) {
|
|
@@ -58,6 +58,26 @@ function attachSockaWebSocket(websocket, sessions, config, init) {
|
|
|
58
58
|
return { session, dispose: shutdown };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// src/server/room-registry.ts
|
|
62
|
+
function createSockaRoomRegistry(makeConfig) {
|
|
63
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
64
|
+
return {
|
|
65
|
+
get(roomId) {
|
|
66
|
+
let r = rooms.get(roomId);
|
|
67
|
+
if (!r) {
|
|
68
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
69
|
+
const config = makeConfig(roomId, sessionMap);
|
|
70
|
+
r = { sessionMap, config };
|
|
71
|
+
rooms.set(roomId, r);
|
|
72
|
+
}
|
|
73
|
+
return r;
|
|
74
|
+
},
|
|
75
|
+
get rooms() {
|
|
76
|
+
return rooms;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { attachSockaWebSocket, createSockaRoomRegistry };
|
|
62
82
|
//# sourceMappingURL=index.js.map
|
|
63
83
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/attachSockaWebSocket.ts"],"names":[],"mappings":";;;;;;;AAyBO,SAAS,oBAAA,CAIf,SAAA,EACA,QAAA,EACA,MAAA,EACA,IAAA,EAC2C;AAC3C,EAAA,MAAM,UAAU,IAAI,qBAAA,CAAsB,SAAA,EAAW,QAAA,EAAU,QAAQ,IAAI,CAAA;AAC3E,EAAA,QAAA,CAAS,GAAA,CAAI,WAAW,OAAO,CAAA;AAC/B,EAAA,yBAAA,CAA0B,QAAQ,OAAO,CAAA;AAEzC,EAAA,IAAI,YAAA,GAAe,KAAA;AAEnB,EAAA,MAAM,WAAW,MAAY;AAC5B,IAAA,SAAA,CAAU,mBAAA,CAAoB,WAAW,SAAS,CAAA;AAClD,IAAA,SAAA,CAAU,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC9C,IAAA,QAAA,CAAS,OAAO,SAAS,CAAA;AAAA,EAC1B,CAAA;AAEA,EAAA,MAAM,WAAW,MAAY;AAC5B,IAAA,IAAI,YAAA,EAAc;AAClB,IAAA,YAAA,GAAe,IAAA;AACf,IAAA,KAAA,CAAM,YAA2B;AAChC,MAAA,IAAI;AACH,QAAA,MAAM,QAAQ,iBAAA,EAAkB;AAAA,MACjC,SAAS,KAAA,EAAO;AACf,QAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,UACpC,IAAA,EAAM,mBAAA;AAAA,UACN;AAAA,SACA,CAAA;AAAA,MACF,CAAA,SAAE;AACD,QAAA,QAAA,EAAS;AAAA,MACV;AAAA,IACD,CAAA,GAAG,CAAE,KAAA,CAAM,CAAC,KAAA,KAAmB;AAC9B,MAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,QACpC,IAAA,EAAM,gBAAA;AAAA,QACN,OAAA,EAAS,QAAA;AAAA,QACT;AAAA,OACA,CAAA;AACD,MAAA,QAAA,EAAS;AAAA,IACV,CAAC,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,SAAA,GAAY,CAAC,EAAA,KAA2B;AAC7C,IAAA,MAAM,EAAA,GAAK,OAAO,UAAA,IAAc,MAAA;AAChC,IAAA,KAAK,2BAAA,CAA4B,OAAA,EAAS,EAAA,EAAI,EAAA,CAAG,IAAI,CAAA,CAAE,KAAA;AAAA,MACtD,CAAC,KAAA,KAAmB;AACnB,QAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,UACpC,IAAA,EAAM,sBAAA;AAAA,UACN,OAAA,EAAS,QAAA;AAAA,UACT;AAAA,SACA,CAAA;AAAA,MACF;AAAA,KACD;AAAA,EACD,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC3B,IAAA,QAAA,EAAS;AAAA,EACV,CAAA;AAEA,EAAA,SAAA,CAAU,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAC/C,EAAA,SAAA,CAAU,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAE3C,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,QAAA,EAAS;AACrC","file":"index.js","sourcesContent":["import type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport { dispatchSockaInboundMessage } from \"./dispatchSockaInboundMessage\";\nimport {\n\tSockaWebSocketSession,\n\trunSockaSessionOnAttached,\n\ttype SockaWebSocketInit,\n\ttype SockaWebSocketSessionConfig,\n} from \"./SockaWebSocketSession\";\n\nexport type AttachedSockaWebSocket<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n> = {\n\tsession: SockaWebSocketSession<TContract, TData>;\n\t/** Remove listeners and delete this session from the map (idempotent). */\n\tdispose: () => void;\n};\n\n/**\n * Register WebSocket `message` / `close` handlers, insert the session into\n * `sessions`, and return `{ session, dispose }`. `dispose` runs\n * {@link SockaWebSocketSession.invokeHandleClose} once, then removes listeners\n * (also triggered by `close`).\n */\nexport function attachSockaWebSocket<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\twebsocket: WebSocket,\n\tsessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>,\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\tinit?: SockaWebSocketInit,\n): AttachedSockaWebSocket<TContract, TData> {\n\tconst session = new SockaWebSocketSession(websocket, sessions, config, init);\n\tsessions.set(websocket, session);\n\trunSockaSessionOnAttached(config, session);\n\n\tlet shuttingDown = false;\n\n\tconst finalize = (): void => {\n\t\twebsocket.removeEventListener(\"message\", onMessage);\n\t\twebsocket.removeEventListener(\"close\", onClose);\n\t\tsessions.delete(websocket);\n\t};\n\n\tconst shutdown = (): void => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tvoid (async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\tawait session.invokeHandleClose();\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\tfinalize();\n\t\t\t}\n\t\t})().catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverShutdown\",\n\t\t\t\tadapter: \"attach\",\n\t\t\t\terror,\n\t\t\t});\n\t\t\tfinalize();\n\t\t});\n\t};\n\n\tconst onMessage = (ev: MessageEvent): void => {\n\t\tconst wf = config.wireFormat ?? \"json\";\n\t\tvoid dispatchSockaInboundMessage(session, wf, ev.data).catch(\n\t\t\t(error: unknown) => {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"attach\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t};\n\n\tconst onClose = (): void => {\n\t\tshutdown();\n\t};\n\n\twebsocket.addEventListener(\"message\", onMessage);\n\twebsocket.addEventListener(\"close\", onClose);\n\n\treturn { session, dispose: shutdown };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/attachSockaWebSocket.ts","../../src/server/room-registry.ts"],"names":[],"mappings":";;;;;;;AAyBO,SAAS,oBAAA,CAIf,SAAA,EACA,QAAA,EACA,MAAA,EACA,IAAA,EAC2C;AAC3C,EAAA,MAAM,UAAU,IAAI,qBAAA,CAAsB,SAAA,EAAW,QAAA,EAAU,QAAQ,IAAI,CAAA;AAC3E,EAAA,QAAA,CAAS,GAAA,CAAI,WAAW,OAAO,CAAA;AAC/B,EAAA,yBAAA,CAA0B,QAAQ,OAAO,CAAA;AAEzC,EAAA,IAAI,YAAA,GAAe,KAAA;AAEnB,EAAA,MAAM,WAAW,MAAY;AAC5B,IAAA,SAAA,CAAU,mBAAA,CAAoB,WAAW,SAAS,CAAA;AAClD,IAAA,SAAA,CAAU,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAC9C,IAAA,QAAA,CAAS,OAAO,SAAS,CAAA;AAAA,EAC1B,CAAA;AAEA,EAAA,MAAM,WAAW,MAAY;AAC5B,IAAA,IAAI,YAAA,EAAc;AAClB,IAAA,YAAA,GAAe,IAAA;AACf,IAAA,KAAA,CAAM,YAA2B;AAChC,MAAA,IAAI;AACH,QAAA,MAAM,QAAQ,iBAAA,EAAkB;AAAA,MACjC,SAAS,KAAA,EAAO;AACf,QAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,UACpC,IAAA,EAAM,mBAAA;AAAA,UACN;AAAA,SACA,CAAA;AAAA,MACF,CAAA,SAAE;AACD,QAAA,QAAA,EAAS;AAAA,MACV;AAAA,IACD,CAAA,GAAG,CAAE,KAAA,CAAM,CAAC,KAAA,KAAmB;AAC9B,MAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,QACpC,IAAA,EAAM,gBAAA;AAAA,QACN,OAAA,EAAS,QAAA;AAAA,QACT;AAAA,OACA,CAAA;AACD,MAAA,QAAA,EAAS;AAAA,IACV,CAAC,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,SAAA,GAAY,CAAC,EAAA,KAA2B;AAC7C,IAAA,MAAM,EAAA,GAAK,OAAO,UAAA,IAAc,MAAA;AAChC,IAAA,KAAK,2BAAA,CAA4B,OAAA,EAAS,EAAA,EAAI,EAAA,CAAG,IAAI,CAAA,CAAE,KAAA;AAAA,MACtD,CAAC,KAAA,KAAmB;AACnB,QAAA,gBAAA,CAAiB,OAAO,WAAA,EAAa;AAAA,UACpC,IAAA,EAAM,sBAAA;AAAA,UACN,OAAA,EAAS,QAAA;AAAA,UACT;AAAA,SACA,CAAA;AAAA,MACF;AAAA,KACD;AAAA,EACD,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC3B,IAAA,QAAA,EAAS;AAAA,EACV,CAAA;AAEA,EAAA,SAAA,CAAU,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAC/C,EAAA,SAAA,CAAU,gBAAA,CAAiB,SAAS,OAAO,CAAA;AAE3C,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,QAAA,EAAS;AACrC;;;AC3EO,SAAS,wBAIf,UAAA,EAOC;AACD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA+C;AACjE,EAAA,OAAO;AAAA,IACN,IAAI,MAAA,EAAmD;AACtD,MAAA,IAAI,CAAA,GAAI,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AACxB,MAAA,IAAI,CAAC,CAAA,EAAG;AACP,QAAA,MAAM,UAAA,uBAAiB,GAAA,EAGrB;AACF,QAAA,MAAM,MAAA,GAAS,UAAA,CAAW,MAAA,EAAQ,UAAU,CAAA;AAC5C,QAAA,CAAA,GAAI,EAAE,YAAY,MAAA,EAAO;AACzB,QAAA,KAAA,CAAM,GAAA,CAAI,QAAQ,CAAC,CAAA;AAAA,MACpB;AACA,MAAA,OAAO,CAAA;AAAA,IACR,CAAA;AAAA,IACA,IAAI,KAAA,GAAgE;AACnE,MAAA,OAAO,KAAA;AAAA,IACR;AAAA,GACD;AACD","file":"index.js","sourcesContent":["import type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport { reportSockaError } from \"../core/socka-report-error\";\nimport { dispatchSockaInboundMessage } from \"./dispatchSockaInboundMessage\";\nimport {\n\tSockaWebSocketSession,\n\trunSockaSessionOnAttached,\n\ttype SockaWebSocketInit,\n\ttype SockaWebSocketSessionConfig,\n} from \"./SockaWebSocketSession\";\n\nexport type AttachedSockaWebSocket<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n> = {\n\tsession: SockaWebSocketSession<TContract, TData>;\n\t/** Remove listeners and delete this session from the map (idempotent). */\n\tdispose: () => void;\n};\n\n/**\n * Register WebSocket `message` / `close` handlers, insert the session into\n * `sessions`, and return `{ session, dispose }`. `dispose` runs\n * {@link SockaWebSocketSession.invokeHandleClose} once, then removes listeners\n * (also triggered by `close`).\n */\nexport function attachSockaWebSocket<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\twebsocket: WebSocket,\n\tsessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>,\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>,\n\tinit?: SockaWebSocketInit,\n): AttachedSockaWebSocket<TContract, TData> {\n\tconst session = new SockaWebSocketSession(websocket, sessions, config, init);\n\tsessions.set(websocket, session);\n\trunSockaSessionOnAttached(config, session);\n\n\tlet shuttingDown = false;\n\n\tconst finalize = (): void => {\n\t\twebsocket.removeEventListener(\"message\", onMessage);\n\t\twebsocket.removeEventListener(\"close\", onClose);\n\t\tsessions.delete(websocket);\n\t};\n\n\tconst shutdown = (): void => {\n\t\tif (shuttingDown) return;\n\t\tshuttingDown = true;\n\t\tvoid (async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\tawait session.invokeHandleClose();\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\tfinalize();\n\t\t\t}\n\t\t})().catch((error: unknown) => {\n\t\t\treportSockaError(config.reportError, {\n\t\t\t\tkind: \"serverShutdown\",\n\t\t\t\tadapter: \"attach\",\n\t\t\t\terror,\n\t\t\t});\n\t\t\tfinalize();\n\t\t});\n\t};\n\n\tconst onMessage = (ev: MessageEvent): void => {\n\t\tconst wf = config.wireFormat ?? \"json\";\n\t\tvoid dispatchSockaInboundMessage(session, wf, ev.data).catch(\n\t\t\t(error: unknown) => {\n\t\t\t\treportSockaError(config.reportError, {\n\t\t\t\t\tkind: \"serverInboundMessage\",\n\t\t\t\t\tadapter: \"attach\",\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t};\n\n\tconst onClose = (): void => {\n\t\tshutdown();\n\t};\n\n\twebsocket.addEventListener(\"message\", onMessage);\n\twebsocket.addEventListener(\"close\", onClose);\n\n\treturn { session, dispose: shutdown };\n}\n","import type { SockaContract, SockaContractConfig } from \"../core/contract\";\nimport type { SockaWebSocketSession } from \"./SockaWebSocketSession\";\nimport type { SockaWebSocketSessionConfig } from \"./SockaWebSocketSessionConfig\";\n\nexport type SockaRoomBundle<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n> = {\n\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>;\n\tconfig: SockaWebSocketSessionConfig<TContract, TData>;\n};\n\n/**\n * Per-room {@link SockaWebSocketSession} maps and configs for Bun/Hono multi-room\n * apps (one bundle per `roomId`).\n */\nexport function createSockaRoomRegistry<\n\tTContract extends SockaContract<SockaContractConfig>,\n\tTData,\n>(\n\tmakeConfig: (\n\t\troomId: string,\n\t\tsessionMap: Map<WebSocket, SockaWebSocketSession<TContract, TData>>,\n\t) => SockaWebSocketSessionConfig<TContract, TData>,\n): {\n\tget(roomId: string): SockaRoomBundle<TContract, TData>;\n\treadonly rooms: ReadonlyMap<string, SockaRoomBundle<TContract, TData>>;\n} {\n\tconst rooms = new Map<string, SockaRoomBundle<TContract, TData>>();\n\treturn {\n\t\tget(roomId: string): SockaRoomBundle<TContract, TData> {\n\t\t\tlet r = rooms.get(roomId);\n\t\t\tif (!r) {\n\t\t\t\tconst sessionMap = new Map<\n\t\t\t\t\tWebSocket,\n\t\t\t\t\tSockaWebSocketSession<TContract, TData>\n\t\t\t\t>();\n\t\t\t\tconst config = makeConfig(roomId, sessionMap);\n\t\t\t\tr = { sessionMap, config };\n\t\t\t\trooms.set(roomId, r);\n\t\t\t}\n\t\t\treturn r;\n\t\t},\n\t\tget rooms(): ReadonlyMap<string, SockaRoomBundle<TContract, TData>> {\n\t\t\treturn rooms;\n\t\t},\n\t};\n}\n"]}
|
|
@@ -117,6 +117,10 @@ type SockaServerErrorFrame = {
|
|
|
117
117
|
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
118
118
|
readonly id: string;
|
|
119
119
|
readonly error: string;
|
|
120
|
+
/** Optional machine-readable code (e.g. `FORBIDDEN`). */
|
|
121
|
+
readonly code?: string;
|
|
122
|
+
/** Optional structured detail for clients; keep small and JSON-serializable. */
|
|
123
|
+
readonly data?: unknown;
|
|
120
124
|
};
|
|
121
125
|
type SockaServerEventFrame = {
|
|
122
126
|
readonly socka: "serverEvent";
|
|
@@ -148,7 +152,10 @@ declare function encodeClientRequest(id: string, rpc: string, body: Record<strin
|
|
|
148
152
|
/** Builds a socka v1 server response frame. */
|
|
149
153
|
declare function encodeServerResponse(id: string, rpc: string, body: unknown): SockaServerResponseFrame;
|
|
150
154
|
/** Builds a socka v1 server error frame. */
|
|
151
|
-
declare function encodeServerError(id: string, error: string
|
|
155
|
+
declare function encodeServerError(id: string, error: string, extra?: {
|
|
156
|
+
readonly code?: string;
|
|
157
|
+
readonly data?: unknown;
|
|
158
|
+
}): SockaServerErrorFrame;
|
|
152
159
|
/** Builds a socka v1 server event frame. */
|
|
153
160
|
declare function encodeServerEvent(event: string, body: unknown): SockaServerEventFrame;
|
|
154
161
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare function createFakeWebSocket(initialReadyState?: number): {
|
|
2
|
+
socket: WebSocket;
|
|
3
|
+
sent: (string | ArrayBuffer | Blob)[];
|
|
4
|
+
dispatchMessage: (data: string | ArrayBuffer) => void;
|
|
5
|
+
dispatchOpen: () => void;
|
|
6
|
+
dispatchClose: () => void;
|
|
7
|
+
dispatchError: () => void;
|
|
8
|
+
setReadyState: (state: number) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { createFakeWebSocket };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/test-utils/fake-websocket.ts
|
|
2
|
+
function createFakeWebSocket(initialReadyState = WebSocket.CONNECTING) {
|
|
3
|
+
const sent = [];
|
|
4
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
5
|
+
let readyState = initialReadyState;
|
|
6
|
+
const addListener = (type, listener) => {
|
|
7
|
+
let set = listeners.get(type);
|
|
8
|
+
if (!set) {
|
|
9
|
+
set = /* @__PURE__ */ new Set();
|
|
10
|
+
listeners.set(type, set);
|
|
11
|
+
}
|
|
12
|
+
set.add(listener);
|
|
13
|
+
};
|
|
14
|
+
const socket = {
|
|
15
|
+
binaryType: "blob",
|
|
16
|
+
get readyState() {
|
|
17
|
+
return readyState;
|
|
18
|
+
},
|
|
19
|
+
addEventListener(type, listener, _options) {
|
|
20
|
+
const fn = typeof listener === "function" ? listener : (ev) => listener.handleEvent(ev);
|
|
21
|
+
addListener(type, fn);
|
|
22
|
+
},
|
|
23
|
+
removeEventListener(type, listener, _options) {
|
|
24
|
+
const set = listeners.get(type);
|
|
25
|
+
if (!set) return;
|
|
26
|
+
const fn = typeof listener === "function" ? listener : (ev) => listener.handleEvent(ev);
|
|
27
|
+
set.delete(fn);
|
|
28
|
+
},
|
|
29
|
+
send(data) {
|
|
30
|
+
if (ArrayBuffer.isView(data)) {
|
|
31
|
+
const v = data;
|
|
32
|
+
const copy = new ArrayBuffer(v.byteLength);
|
|
33
|
+
new Uint8Array(copy).set(
|
|
34
|
+
new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
|
|
35
|
+
);
|
|
36
|
+
sent.push(copy);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
sent.push(data);
|
|
40
|
+
},
|
|
41
|
+
close(_code, _reason) {
|
|
42
|
+
},
|
|
43
|
+
dispatchEvent(event) {
|
|
44
|
+
const set = listeners.get(event.type);
|
|
45
|
+
if (!set) return true;
|
|
46
|
+
for (const l of set) {
|
|
47
|
+
l(event);
|
|
48
|
+
}
|
|
49
|
+
return !event.defaultPrevented;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const dispatchMessage = (data) => {
|
|
53
|
+
const ev = new MessageEvent("message", { data });
|
|
54
|
+
socket.dispatchEvent(ev);
|
|
55
|
+
};
|
|
56
|
+
const dispatchOpen = () => {
|
|
57
|
+
readyState = WebSocket.OPEN;
|
|
58
|
+
socket.dispatchEvent(new Event("open"));
|
|
59
|
+
};
|
|
60
|
+
const dispatchClose = () => {
|
|
61
|
+
readyState = WebSocket.CLOSED;
|
|
62
|
+
socket.dispatchEvent(new CloseEvent("close", { code: 1e3 }));
|
|
63
|
+
};
|
|
64
|
+
const dispatchError = () => {
|
|
65
|
+
readyState = WebSocket.CLOSED;
|
|
66
|
+
socket.dispatchEvent(new Event("error"));
|
|
67
|
+
};
|
|
68
|
+
const setReadyState = (state) => {
|
|
69
|
+
readyState = state;
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
socket,
|
|
73
|
+
sent,
|
|
74
|
+
dispatchMessage,
|
|
75
|
+
dispatchOpen,
|
|
76
|
+
dispatchClose,
|
|
77
|
+
dispatchError,
|
|
78
|
+
setReadyState
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { createFakeWebSocket };
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/test-utils/fake-websocket.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Minimal fake WebSocket for unit tests (not published).\n */\ntype Listener = (event: Event) => void;\n\nexport function createFakeWebSocket(\n\tinitialReadyState: number = WebSocket.CONNECTING,\n): {\n\tsocket: WebSocket;\n\tsent: (string | ArrayBuffer | Blob)[];\n\tdispatchMessage: (data: string | ArrayBuffer) => void;\n\tdispatchOpen: () => void;\n\tdispatchClose: () => void;\n\tdispatchError: () => void;\n\tsetReadyState: (state: number) => void;\n} {\n\tconst sent: (string | ArrayBuffer | Blob)[] = [];\n\tconst listeners = new Map<string, Set<Listener>>();\n\n\tlet readyState: number = initialReadyState;\n\n\tconst addListener = (type: string, listener: Listener) => {\n\t\tlet set = listeners.get(type);\n\t\tif (!set) {\n\t\t\tset = new Set();\n\t\t\tlisteners.set(type, set);\n\t\t}\n\t\tset.add(listener);\n\t};\n\n\tconst socket = {\n\t\tbinaryType: \"blob\",\n\t\tget readyState() {\n\t\t\treturn readyState;\n\t\t},\n\t\taddEventListener(\n\t\t\ttype: string,\n\t\t\tlistener: EventListenerOrEventListenerObject,\n\t\t\t_options?: boolean | AddEventListenerOptions,\n\t\t): void {\n\t\t\tconst fn =\n\t\t\t\ttypeof listener === \"function\"\n\t\t\t\t\t? listener\n\t\t\t\t\t: (ev: Event) => listener.handleEvent(ev);\n\t\t\taddListener(type, fn as Listener);\n\t\t},\n\t\tremoveEventListener(\n\t\t\ttype: string,\n\t\t\tlistener: EventListenerOrEventListenerObject,\n\t\t\t_options?: boolean | EventListenerOptions,\n\t\t): void {\n\t\t\tconst set = listeners.get(type);\n\t\t\tif (!set) return;\n\t\t\tconst fn =\n\t\t\t\ttypeof listener === \"function\"\n\t\t\t\t\t? listener\n\t\t\t\t\t: (ev: Event) => listener.handleEvent(ev);\n\t\t\tset.delete(fn as Listener);\n\t\t},\n\t\tsend(data: string | ArrayBuffer | Blob | ArrayBufferView): void {\n\t\t\tif (ArrayBuffer.isView(data)) {\n\t\t\t\tconst v = data;\n\t\t\t\tconst copy = new ArrayBuffer(v.byteLength);\n\t\t\t\tnew Uint8Array(copy).set(\n\t\t\t\t\tnew Uint8Array(v.buffer, v.byteOffset, v.byteLength),\n\t\t\t\t);\n\t\t\t\tsent.push(copy);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsent.push(data);\n\t\t},\n\t\tclose(_code?: number, _reason?: string): void {\n\t\t\t// no-op for tests unless needed\n\t\t},\n\t\tdispatchEvent(event: Event): boolean {\n\t\t\tconst set = listeners.get(event.type);\n\t\t\tif (!set) return true;\n\t\t\tfor (const l of set) {\n\t\t\t\tl(event);\n\t\t\t}\n\t\t\treturn !event.defaultPrevented;\n\t\t},\n\t} as unknown as WebSocket;\n\n\tconst dispatchMessage = (data: string | ArrayBuffer): void => {\n\t\tconst ev = new MessageEvent(\"message\", { data });\n\t\tsocket.dispatchEvent(ev);\n\t};\n\n\tconst dispatchOpen = (): void => {\n\t\treadyState = WebSocket.OPEN;\n\t\tsocket.dispatchEvent(new Event(\"open\"));\n\t};\n\n\tconst dispatchClose = (): void => {\n\t\treadyState = WebSocket.CLOSED;\n\t\tsocket.dispatchEvent(new CloseEvent(\"close\", { code: 1000 }));\n\t};\n\n\tconst dispatchError = (): void => {\n\t\treadyState = WebSocket.CLOSED;\n\t\tsocket.dispatchEvent(new Event(\"error\"));\n\t};\n\n\tconst setReadyState = (state: number): void => {\n\t\treadyState = state;\n\t};\n\n\treturn {\n\t\tsocket,\n\t\tsent,\n\t\tdispatchMessage,\n\t\tdispatchOpen,\n\t\tdispatchClose,\n\t\tdispatchError,\n\t\tsetReadyState,\n\t};\n}\n"]}
|
package/docs/README.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# @firtoz/socka — documentation
|
|
2
2
|
|
|
3
|
-
In-repo guides for the **[Socka](../README.md)** library (**npm** [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka)). For Cursor agents, see also [`../skills/`](../skills/).
|
|
3
|
+
In-repo guides for the **[Socka](../README.md)** library (**npm** [`@firtoz/socka`](https://www.npmjs.com/package/@firtoz/socka)). These docs target **people shipping apps** with socka. For Cursor agents, see also [`../skills/`](../skills/).
|
|
4
4
|
|
|
5
5
|
| Doc | Description |
|
|
6
6
|
|-----|-------------|
|
|
7
|
-
| [Getting started](./getting-started.md) |
|
|
8
|
-
| [Peers](./peers.md) | Which
|
|
7
|
+
| [Getting started](./getting-started.md) | Multi-room chat tutorial (RPC + pushes + history); links to **chatroom-*** examples |
|
|
8
|
+
| [Peers](./peers.md) | Which dependencies to install per import path and why |
|
|
9
9
|
| [Multi-room](./multi-room.md) | Scopes, patterns per runtime, pitfalls |
|
|
10
10
|
| [Lifecycle](./lifecycle.md) | `onAttached`, inbound RPCs, `handleClose` ordering |
|
|
11
11
|
| [Server](./server.md) | Node `ws`, Bun, Hono, `attachSockaWebSocket`, `createData`, `session.data` |
|
|
12
12
|
| [Durable Objects](./durable-objects.md) | `SockaDoSession`, `SockaWebSocketDO`, routing, hibernation |
|
|
13
|
-
| [Client](./client.md) | `SockaSession`, React, deferred connect
|
|
14
|
-
| [
|
|
15
|
-
| [
|
|
13
|
+
| [Client](./client.md) | `SockaSession`, React (`useSocka` / `useSockaSession`), deferred connect |
|
|
14
|
+
| [Reconnection](./reconnection.md) | Exponential backoff, `onReconnecting` / `onReconnected`, hydrate after reconnect |
|
|
15
|
+
| [Presence](./presence.md) | `listPeers`, `peerCount`, snapshot RPC + `userJoined` / `userLeft` pushes |
|
|
16
|
+
| [Auth](./auth.md) | Cookies, tokens, and upgrade-time authorization |
|
|
17
|
+
| [Recipes](./recipes.md) | Copy-paste wiring per runtime |
|
|
18
|
+
| [History](./history.md) | Pagination/cursor, retention, `historyCleared`-style invalidation |
|
|
19
|
+
| [Pushes](./pushes.md) | `emitPush` / `broadcastPush`, `session.subscribe`, ordering notes |
|
|
20
|
+
| [Wire format](./wire-format.md) | JSON vs msgpack tradeoffs |
|
|
21
|
+
| [Backpressure](./backpressure.md) | Current behavior and app-level mitigations |
|
|
22
|
+
| [Testing](./testing.md) | Fake `WebSocket`, handler isolation, integration fixtures |
|
|
23
|
+
| [Reference](./reference.md) | Configuration tables, type inference, errors, imports |
|
|
24
|
+
| [Internals](./internals.md) | Wire protocol details, frame kinds, source file links (contributors & curious readers) |
|
|
16
25
|
| [Comparison](./comparison.md) | vs DIY WS, **socket.io**, **tRPC** |
|
|
17
26
|
|
|
18
27
|
**Roadmap** — [Deferred and post–v1 ideas](../roadmap.md).
|
package/docs/auth.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Authentication and authorization
|
|
2
|
+
|
|
3
|
+
Socka does not ship a built-in auth layer: you decide **who** may open a WebSocket and **what** each RPC may do. Typical patterns:
|
|
4
|
+
|
|
5
|
+
## Read credentials on upgrade
|
|
6
|
+
|
|
7
|
+
- **`@firtoz/socka/server`** with **`strictUpgradeRequest: true`** — in **`createData`**, read **`init.request`** (cookies via **`Cookie`**, **`Authorization`**, URL query, path segments).
|
|
8
|
+
- **`SockaDoSession`** with **`createData: (ctx) => …`** — use Hono **`ctx.req`**, **`ctx.get("…")`**, or **`ctx.req.raw.headers`**.
|
|
9
|
+
|
|
10
|
+
Reject before returning session data: throw **`SockaError`** with **`{ code, data }`** so the client receives a correlated **`serverError`** frame (see **[Reference — RPC handler errors](./reference.md#rpc-handler-errors)**).
|
|
11
|
+
|
|
12
|
+
## Browsers and the WebSocket API
|
|
13
|
+
|
|
14
|
+
The browser **`WebSocket`** constructor cannot set arbitrary headers on the handshake. Common approaches:
|
|
15
|
+
|
|
16
|
+
- **Cookie** — `SameSite` cookies sent automatically to your origin; read them in **`createData`** from **`init.request`**.
|
|
17
|
+
- **Query string** — `wss://app.example.com/ws/room?token=…` (treat tokens as secrets; prefer short-lived tokens and HTTPS/WSS only).
|
|
18
|
+
- **Subprotocol** — rarely needed; socka uses its own wire framing on the same socket.
|
|
19
|
+
|
|
20
|
+
## After the socket is open
|
|
21
|
+
|
|
22
|
+
You can also enforce auth inside **RPC handlers** using **`session.data`** (set in **`createData`**) and return **`SockaError`** for forbidden operations.
|
|
23
|
+
|
|
24
|
+
## See also
|
|
25
|
+
|
|
26
|
+
- **[Multi-room](./multi-room.md)** — scoping **`sessionMap`** per tenant/room.
|
|
27
|
+
- **[Client](./client.md)** — lifecycle and reconnect; re-auth after reconnect may repeat **`listHistory`** / snapshot RPCs.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Backpressure
|
|
2
|
+
|
|
3
|
+
Today, socka does **not** expose explicit **backpressure** or **pause/resume** on **`emitPush`** / **`broadcastPush`**. Delivery follows the underlying **WebSocket** and **TCP** behavior: if a client is slow, **buffers** grow in the runtime/network stack.
|
|
4
|
+
|
|
5
|
+
## When it matters
|
|
6
|
+
|
|
7
|
+
- Very **large** or **frequent** pushes (e.g. big blobs, rapid fire).
|
|
8
|
+
- Many **slow** subscribers in a room.
|
|
9
|
+
|
|
10
|
+
## Practical guidance
|
|
11
|
+
|
|
12
|
+
- **Chunk** large logical messages in **application code** (multiple smaller pushes or an RPC that streams chunks).
|
|
13
|
+
- Prefer **smaller push payloads** and **pagination** for history-style data.
|
|
14
|
+
- For most apps, **TCP** flow control is enough; if you routinely saturate buffers, measure and consider **rate limits** or **per-client queues** in your domain layer.
|
|
15
|
+
|
|
16
|
+
Future library work could add explicit flow control; until then, treat **backpressure as an app concern** for extreme cases.
|