@firtoz/socka 2.0.0 → 3.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 +195 -42
- package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
- package/dist/bun/index.d.ts +30 -5
- package/dist/bun/index.js +28 -5
- package/dist/bun/index.js.map +1 -1
- package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
- package/dist/chunk-IFIGKR3W.js.map +1 -0
- package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
- package/dist/chunk-LVVCHLNW.js.map +1 -0
- package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
- package/dist/chunk-P3JEEOJL.js.map +1 -0
- package/dist/chunk-QGURL3DJ.js +8 -0
- package/dist/chunk-QGURL3DJ.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 +2 -21
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/do/index.d.ts +20 -2
- package/dist/do/index.js +36 -2
- package/dist/do/index.js.map +1 -1
- package/dist/hono/cloudflare-workers.d.ts +4 -4
- package/dist/hono/cloudflare-workers.js +4 -3
- package/dist/hono/cloudflare-workers.js.map +1 -1
- package/dist/hono/index.d.ts +22 -6
- 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 +18 -5
- 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-CXwpAUgl.d.ts} +80 -8
- 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 +16 -7
- package/docs/auth.md +27 -0
- package/docs/backpressure.md +16 -0
- package/docs/client.md +48 -3
- package/docs/comparison.md +2 -2
- package/docs/durable-objects.md +3 -3
- 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 +77 -0
- package/docs/reconnection.md +44 -0
- package/docs/reference.md +27 -32
- package/docs/server.md +19 -3
- package/docs/testing.md +20 -0
- package/docs/wire-format.md +29 -0
- package/examples/minimal-socka.ts +56 -3
- package/package.json +14 -10
- package/roadmap.md +2 -2
- package/skills/socka/core-rpc/SKILL.md +2 -2
- package/skills/socka/do-session/SKILL.md +2 -2
- package/skills/socka/standard-schema/SKILL.md +1 -1
- package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
- 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.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,
|
|
1
|
+
import { a as SockaWebSocketSession, b as SockaWebSocketSessionConfigUnion, c as SockaWebSocketInit, e as SockaWebSocketSessionConfig } from '../SockaWebSocketSession-B1w7RAid.js';
|
|
2
|
+
export { g as SockaEmitCapable, d as SockaPushSession, S as SockaStrictWebSocketInit, h as SockaWebSocketSessionConfigLoose, f as broadcastSockaEventToPeers, r as runSockaSessionOnAttached } from '../SockaWebSocketSession-B1w7RAid.js';
|
|
3
|
+
import { S as SockaContract, a as SockaContractConfig, w as SockaWireFormat } from '../socka-report-error-CXwpAUgl.js';
|
|
4
4
|
import '@standard-schema/spec';
|
|
5
5
|
|
|
6
6
|
type AttachedSockaWebSocket<TContract extends SockaContract<SockaContractConfig>, TData> = {
|
|
@@ -14,7 +14,7 @@ type AttachedSockaWebSocket<TContract extends SockaContract<SockaContractConfig>
|
|
|
14
14
|
* {@link SockaWebSocketSession.invokeHandleClose} once, then removes listeners
|
|
15
15
|
* (also triggered by `close`).
|
|
16
16
|
*/
|
|
17
|
-
declare function attachSockaWebSocket<TContract extends SockaContract<SockaContractConfig>, TData>(websocket: WebSocket, sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>, config:
|
|
17
|
+
declare function attachSockaWebSocket<TContract extends SockaContract<SockaContractConfig>, TData>(websocket: WebSocket, sessions: Map<WebSocket, SockaWebSocketSession<TContract, TData>>, config: SockaWebSocketSessionConfigUnion<TContract, TData>, init?: SockaWebSocketInit): AttachedSockaWebSocket<TContract, TData>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Decode a WebSocket `message` payload and dispatch it to the session (same
|
|
@@ -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, SockaWebSocketSessionConfigUnion, 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-LVVCHLNW.js';
|
|
4
|
+
export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-LVVCHLNW.js';
|
|
5
|
+
import { reportSockaError } from '../chunk-IFIGKR3W.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
|
|
1
|
+
{"version":3,"sources":["../../src/server/attachSockaWebSocket.ts","../../src/server/room-registry.ts"],"names":[],"mappings":"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","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 SockaWebSocketSessionConfigUnion,\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: SockaWebSocketSessionConfigUnion<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"]}
|
|
@@ -11,12 +11,18 @@ declare const RESERVED_SOCKA_PROCEDURE_NAMES: readonly ["then", "catch", "finall
|
|
|
11
11
|
type ReservedSockaProcedureName = (typeof RESERVED_SOCKA_PROCEDURE_NAMES)[number];
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Defines one client-initiated call:
|
|
15
|
-
*
|
|
14
|
+
* Defines one client-initiated call: optional `input` and optional `output` schemas
|
|
15
|
+
* (Standard Schema v1: Zod v4, Valibot, ArkType, etc.).
|
|
16
|
+
*
|
|
17
|
+
* - **`output` present** (including `z.void()`): request/response RPC; the server sends
|
|
18
|
+
* a validated `serverResponse` on success.
|
|
19
|
+
* - **`output` omitted**: fire-and-forget on success (no `serverResponse`); the client
|
|
20
|
+
* `send` method resolves after the request is sent. Use `output: z.void()` when you
|
|
21
|
+
* still want a correlated ack. See the package README and {@link defineSocka}.
|
|
16
22
|
*/
|
|
17
23
|
type SockaProcedureDef = {
|
|
18
24
|
readonly input?: StandardSchemaV1;
|
|
19
|
-
readonly output
|
|
25
|
+
readonly output?: StandardSchemaV1;
|
|
20
26
|
};
|
|
21
27
|
/** Configuration object accepted by {@link defineSocka}. */
|
|
22
28
|
type SockaContractConfig = {
|
|
@@ -38,23 +44,27 @@ type SockaContract<T extends SockaContractConfig = SockaContractConfig> = {
|
|
|
38
44
|
pushes: Record<string, StandardSchemaV1>;
|
|
39
45
|
} ? T["pushes"] : Record<string, never>;
|
|
40
46
|
};
|
|
47
|
+
/** Inferred client return type for a call: payload type or `void` when `output` is omitted. */
|
|
48
|
+
type InferSockaCallReturn<P extends SockaProcedureDef> = P["output"] extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<P["output"]> : void;
|
|
41
49
|
type CallFn<P extends SockaProcedureDef> = P extends {
|
|
42
50
|
input: infer I extends StandardSchemaV1;
|
|
43
|
-
} ? (input: StandardSchemaV1.InferInput<I>) => Promise<
|
|
51
|
+
} ? (input: StandardSchemaV1.InferInput<I>) => Promise<InferSockaCallReturn<P>> : () => Promise<InferSockaCallReturn<P>>;
|
|
44
52
|
/**
|
|
45
53
|
* Infers the typed `session.send.*` method map for a contract.
|
|
46
54
|
*/
|
|
47
55
|
type InferSockaSend<C extends SockaContract> = {
|
|
48
56
|
[K in keyof C["calls"]]: CallFn<C["calls"][K]>;
|
|
49
57
|
};
|
|
50
|
-
type HandlerOut<P extends SockaProcedureDef> = StandardSchemaV1.InferOutput<P["output"]> | Promise<StandardSchemaV1.InferOutput<P["output"]
|
|
58
|
+
type HandlerOut<P extends SockaProcedureDef> = P["output"] extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<P["output"]> | Promise<StandardSchemaV1.InferOutput<P["output"]>> : void | Promise<void>;
|
|
51
59
|
type HandlerFn<P extends SockaProcedureDef, TSession> = P extends {
|
|
52
60
|
input: infer I extends StandardSchemaV1;
|
|
53
61
|
} ? (input: StandardSchemaV1.InferInput<I>, session: TSession) => HandlerOut<P> : (session: TSession) => HandlerOut<P>;
|
|
54
62
|
/**
|
|
55
63
|
* Infers the typed server handler map for a contract. Handlers with an input
|
|
56
64
|
* schema take `(input, session)`; calls without input take `(session)` only.
|
|
57
|
-
*
|
|
65
|
+
* When `output` is present, the return value is validated and sent as `serverResponse`.
|
|
66
|
+
* When `output` is omitted (fire-and-forget), the handler should return `void`; the
|
|
67
|
+
* server does not send a success response.
|
|
58
68
|
*/
|
|
59
69
|
type InferSockaHandlers<C extends SockaContract, TSession> = {
|
|
60
70
|
[K in keyof C["calls"]]: HandlerFn<C["calls"][K], TSession>;
|
|
@@ -79,6 +89,7 @@ type InferSockaPushHandlers<C extends SockaContract> = {
|
|
|
79
89
|
* calls: {
|
|
80
90
|
* list: { output: z.array(itemSchema) },
|
|
81
91
|
* insert: { input: z.object({ item: itemSchema }), output: z.void() },
|
|
92
|
+
* notify: { input: z.object({ text: z.string() }) },
|
|
82
93
|
* },
|
|
83
94
|
* });
|
|
84
95
|
* ```
|
|
@@ -90,6 +101,33 @@ declare function defineSocka<const T extends SockaContractConfig>(config: T & {
|
|
|
90
101
|
calls: ValidateSockaCallKeys<T["calls"]>;
|
|
91
102
|
}): SockaContract<T>;
|
|
92
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Thrown on the server and surfaced on the client when the wire uses a shared
|
|
106
|
+
* `{ type: "error", id, error }` envelope for correlated RPC failures.
|
|
107
|
+
*/
|
|
108
|
+
declare class SockaError extends Error {
|
|
109
|
+
readonly requestId?: string;
|
|
110
|
+
/** Procedure name when provided on the wire (`serverError.rpc`). */
|
|
111
|
+
readonly rpc?: string;
|
|
112
|
+
readonly code?: string;
|
|
113
|
+
readonly data?: unknown;
|
|
114
|
+
constructor(message: string, options?: {
|
|
115
|
+
requestId?: string;
|
|
116
|
+
rpc?: string;
|
|
117
|
+
code?: string;
|
|
118
|
+
data?: unknown;
|
|
119
|
+
cause?: unknown;
|
|
120
|
+
});
|
|
121
|
+
/** Builds a {@link SockaError} from a standard RPC error envelope. */
|
|
122
|
+
static fromWire(msg: {
|
|
123
|
+
id: string;
|
|
124
|
+
error: string;
|
|
125
|
+
rpc?: string;
|
|
126
|
+
code?: string;
|
|
127
|
+
data?: unknown;
|
|
128
|
+
}): SockaError;
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
/**
|
|
94
132
|
* Versioned socka wire framing. After JSON parse or msgpack unpack, every frame
|
|
95
133
|
* must satisfy {@link decodeSockaWire}; procedure bodies are validated with Standard Schema on each side.
|
|
@@ -117,6 +155,12 @@ type SockaServerErrorFrame = {
|
|
|
117
155
|
readonly v: typeof SOCKA_WIRE_VERSION;
|
|
118
156
|
readonly id: string;
|
|
119
157
|
readonly error: string;
|
|
158
|
+
/** Procedure name when the error relates to a client RPC (helps clients without a pending entry). */
|
|
159
|
+
readonly rpc?: string;
|
|
160
|
+
/** Optional machine-readable code (e.g. `FORBIDDEN`). */
|
|
161
|
+
readonly code?: string;
|
|
162
|
+
/** Optional structured detail for clients; keep small and JSON-serializable. */
|
|
163
|
+
readonly data?: unknown;
|
|
120
164
|
};
|
|
121
165
|
type SockaServerEventFrame = {
|
|
122
166
|
readonly socka: "serverEvent";
|
|
@@ -148,7 +192,11 @@ declare function encodeClientRequest(id: string, rpc: string, body: Record<strin
|
|
|
148
192
|
/** Builds a socka v1 server response frame. */
|
|
149
193
|
declare function encodeServerResponse(id: string, rpc: string, body: unknown): SockaServerResponseFrame;
|
|
150
194
|
/** Builds a socka v1 server error frame. */
|
|
151
|
-
declare function encodeServerError(id: string, error: string
|
|
195
|
+
declare function encodeServerError(id: string, error: string, extra?: {
|
|
196
|
+
readonly code?: string;
|
|
197
|
+
readonly data?: unknown;
|
|
198
|
+
readonly rpc?: string;
|
|
199
|
+
}): SockaServerErrorFrame;
|
|
152
200
|
/** Builds a socka v1 server event frame. */
|
|
153
201
|
declare function encodeServerEvent(event: string, body: unknown): SockaServerEventFrame;
|
|
154
202
|
|
|
@@ -182,6 +230,30 @@ type SockaReportError = {
|
|
|
182
230
|
kind: "clientEventValidation";
|
|
183
231
|
eventName: string;
|
|
184
232
|
error: unknown;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* `serverError` for a fire-and-forget call (no pending client promise). Prefer
|
|
236
|
+
* setting `reportError` on the session when using output-less procedures.
|
|
237
|
+
*/
|
|
238
|
+
| {
|
|
239
|
+
kind: "clientFireAndForgetRpcError";
|
|
240
|
+
error: SockaError;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* `serverError` with no matching pending entry for a call that expects a response
|
|
244
|
+
* (e.g. stale id after reconnect or duplicate frame).
|
|
245
|
+
*/
|
|
246
|
+
| {
|
|
247
|
+
kind: "clientOrphanServerError";
|
|
248
|
+
error: SockaError;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Server sent `serverResponse` for a procedure with no `output` (misbehaving server).
|
|
252
|
+
*/
|
|
253
|
+
| {
|
|
254
|
+
kind: "clientUnexpectedServerResponse";
|
|
255
|
+
rpc: string;
|
|
256
|
+
requestId: string;
|
|
185
257
|
} | {
|
|
186
258
|
kind: "serverOnAttached";
|
|
187
259
|
error: unknown;
|
|
@@ -203,4 +275,4 @@ declare function defaultReportError(event: SockaReportError): void;
|
|
|
203
275
|
/** Invokes the optional `reportError` callback when provided, otherwise `defaultReportError`. */
|
|
204
276
|
declare function reportSockaError(reportError: ((event: SockaReportError) => void) | undefined, event: SockaReportError): void;
|
|
205
277
|
|
|
206
|
-
export { type DecodedSockaWire as D, type InferSockaSend as I, RESERVED_SOCKA_PROCEDURE_NAMES as R, type SockaContract as S, type ValidateSockaCallKeys as V, type SockaContractConfig as a, type
|
|
278
|
+
export { type DecodedSockaWire as D, type InferSockaSend as I, RESERVED_SOCKA_PROCEDURE_NAMES as R, type SockaContract as S, type ValidateSockaCallKeys as V, type SockaContractConfig as a, type SockaProcedureDef as b, type InferSockaHandlers as c, defineSocka as d, type InferSockaPushHandlers as e, type InferSockaPushPayload as f, type ReservedSockaProcedureName as g, SockaError as h, SOCKA_WIRE_VERSION as i, SockaWireError as j, type SockaClientRequestFrame as k, type SockaServerErrorFrame as l, type SockaServerEventFrame as m, type SockaServerResponseFrame as n, type SockaWireFrame as o, decodeSockaWire as p, encodeClientRequest as q, encodeServerResponse as r, encodeServerError as s, encodeServerEvent as t, encodeSockaWire as u, parseWirePayload as v, type SockaWireFormat as w, defaultReportError as x, reportSockaError as y, type SockaReportError as z };
|
|
@@ -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":";AAKO,SAAS,mBAAA,CACf,iBAAA,GAA4B,SAAA,CAAU,UAAA,EASrC;AACD,EAAA,MAAM,OAAwC,EAAC;AAC/C,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAA2B;AAEjD,EAAA,IAAI,UAAA,GAAqB,iBAAA;AAEzB,EAAA,MAAM,WAAA,GAAc,CAAC,IAAA,EAAc,QAAA,KAAuB;AACzD,IAAA,IAAI,GAAA,GAAM,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAC5B,IAAA,IAAI,CAAC,GAAA,EAAK;AACT,MAAA,GAAA,uBAAU,GAAA,EAAI;AACd,MAAA,SAAA,CAAU,GAAA,CAAI,MAAM,GAAG,CAAA;AAAA,IACxB;AACA,IAAA,GAAA,CAAI,IAAI,QAAQ,CAAA;AAAA,EACjB,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS;AAAA,IACd,UAAA,EAAY,MAAA;AAAA,IACZ,IAAI,UAAA,GAAa;AAChB,MAAA,OAAO,UAAA;AAAA,IACR,CAAA;AAAA,IACA,gBAAA,CACC,IAAA,EACA,QAAA,EACA,QAAA,EACO;AACP,MAAA,MAAM,EAAA,GACL,OAAO,QAAA,KAAa,UAAA,GACjB,WACA,CAAC,EAAA,KAAc,QAAA,CAAS,WAAA,CAAY,EAAE,CAAA;AAC1C,MAAA,WAAA,CAAY,MAAM,EAAc,CAAA;AAAA,IACjC,CAAA;AAAA,IACA,mBAAA,CACC,IAAA,EACA,QAAA,EACA,QAAA,EACO;AACP,MAAA,MAAM,GAAA,GAAM,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAC9B,MAAA,IAAI,CAAC,GAAA,EAAK;AACV,MAAA,MAAM,EAAA,GACL,OAAO,QAAA,KAAa,UAAA,GACjB,WACA,CAAC,EAAA,KAAc,QAAA,CAAS,WAAA,CAAY,EAAE,CAAA;AAC1C,MAAA,GAAA,CAAI,OAAO,EAAc,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,KAAK,IAAA,EAA2D;AAC/D,MAAA,IAAI,WAAA,CAAY,MAAA,CAAO,IAAI,CAAA,EAAG;AAC7B,QAAA,MAAM,CAAA,GAAI,IAAA;AACV,QAAA,MAAM,IAAA,GAAO,IAAI,WAAA,CAAY,CAAA,CAAE,UAAU,CAAA;AACzC,QAAA,IAAI,UAAA,CAAW,IAAI,CAAA,CAAE,GAAA;AAAA,UACpB,IAAI,UAAA,CAAW,CAAA,CAAE,QAAQ,CAAA,CAAE,UAAA,EAAY,EAAE,UAAU;AAAA,SACpD;AACA,QAAA,IAAA,CAAK,KAAK,IAAI,CAAA;AACd,QAAA;AAAA,MACD;AACA,MAAA,IAAA,CAAK,KAAK,IAAI,CAAA;AAAA,IACf,CAAA;AAAA,IACA,KAAA,CAAM,OAAgB,OAAA,EAAwB;AAAA,IAE9C,CAAA;AAAA,IACA,cAAc,KAAA,EAAuB;AACpC,MAAA,MAAM,GAAA,GAAM,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AACpC,MAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,MAAA,KAAA,MAAW,KAAK,GAAA,EAAK;AACpB,QAAA,CAAA,CAAE,KAAK,CAAA;AAAA,MACR;AACA,MAAA,OAAO,CAAC,KAAA,CAAM,gBAAA;AAAA,IACf;AAAA,GACD;AAEA,EAAA,MAAM,eAAA,GAAkB,CAAC,IAAA,KAAqC;AAC7D,IAAA,MAAM,KAAK,IAAI,YAAA,CAAa,SAAA,EAAW,EAAE,MAAM,CAAA;AAC/C,IAAA,MAAA,CAAO,cAAc,EAAE,CAAA;AAAA,EACxB,CAAA;AAEA,EAAA,MAAM,eAAe,MAAY;AAChC,IAAA,UAAA,GAAa,SAAA,CAAU,IAAA;AACvB,IAAA,MAAA,CAAO,aAAA,CAAc,IAAI,KAAA,CAAM,MAAM,CAAC,CAAA;AAAA,EACvC,CAAA;AAEA,EAAA,MAAM,gBAAgB,MAAY;AACjC,IAAA,UAAA,GAAa,SAAA,CAAU,MAAA;AACvB,IAAA,MAAA,CAAO,aAAA,CAAc,IAAI,UAAA,CAAW,OAAA,EAAS,EAAE,IAAA,EAAM,GAAA,EAAM,CAAC,CAAA;AAAA,EAC7D,CAAA;AAEA,EAAA,MAAM,gBAAgB,MAAY;AACjC,IAAA,UAAA,GAAa,SAAA,CAAU,MAAA;AACvB,IAAA,MAAA,CAAO,aAAA,CAAc,IAAI,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,KAAwB;AAC9C,IAAA,UAAA,GAAa,KAAA;AAAA,EACd,CAAA;AAEA,EAAA,OAAO;AAAA,IACN,MAAA;AAAA,IACA,IAAA;AAAA,IACA,eAAA;AAAA,IACA,YAAA;AAAA,IACA,aAAA;AAAA,IACA,aAAA;AAAA,IACA;AAAA,GACD;AACD","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
|
-
**Roadmap** — [Deferred and
|
|
27
|
+
**Roadmap** — [Deferred ideas and future work](../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`** — by default **`createData`** receives **`SockaStrictWebSocketInit`**; 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.
|
package/docs/client.md
CHANGED
|
@@ -16,6 +16,10 @@ Outside React, construct **`SockaSession`** from **`@firtoz/socka/client`** with
|
|
|
16
16
|
|
|
17
17
|
Full list: **[Reference — Client configuration](./reference.md#client-configuration)**.
|
|
18
18
|
|
|
19
|
+
### Fire-and-forget
|
|
20
|
+
|
|
21
|
+
Calls that **omit** **`output`** resolve **`await session.send.*`** as soon as the request is sent; they do **not** wait for **`serverResponse`**. If the server returns **`serverError`**, handle it with **`reportError`** (see **[Reference — Errors](./reference.md#errors-and-observability)**) — there is no rejected promise for that call. Prefer **`output: z.void()`** when you still want **`await`** to track success or failure through the returned promise.
|
|
22
|
+
|
|
19
23
|
**Call names** — For literal `calls` objects, **`defineSocka`** rejects names that would make **`session.send`** Promise-like or clash with object shape (e.g. **`then`**, **`toString`**). If you use a wide **`Record<string, SockaProcedureDef>`**, TypeScript cannot apply that check; **`SockaSession`** still validates at construction (see **`RESERVED_SOCKA_PROCEDURE_NAMES`** in **`@firtoz/socka/core`**).
|
|
20
24
|
|
|
21
25
|
```ts
|
|
@@ -28,8 +32,14 @@ const rows = await session.send.list();
|
|
|
28
32
|
|
|
29
33
|
Use **`SockaWebSocketClient`** directly if you need **`onResponse` / `onServerError` / `onEvent`** frame hooks without **`SockaSession`**’s typed **`send`** / **`subscribe`**; most apps use **`SockaSession`**.
|
|
30
34
|
|
|
35
|
+
**Connection status** — **`SockaWebSocketClient`** and **`SockaSession`** expose **`status`** (`"idle" | "connecting" | "open" | "reconnecting" | "closed"`) and **`onStatusChange`** for UI (e.g. “Reconnecting…”). Same fields power the React hooks below.
|
|
36
|
+
|
|
31
37
|
## React
|
|
32
38
|
|
|
39
|
+
### `useSockaSession` — typed `send`
|
|
40
|
+
|
|
41
|
+
Use **`useSockaSession(contract, options, deps)`** when you want **`send.*`** RPC methods directly (same shape as **`session.send`**). **`ready`** flips to **`true`** after the socket opens; **`deps`** remount the connection when identity (e.g. room id) changes. Also returns **`status`**, **`reconnecting`**, and **`reconnectAttempt`** (see **`useSocka`**).
|
|
42
|
+
|
|
33
43
|
```ts
|
|
34
44
|
import { useSockaSession } from "@firtoz/socka/react";
|
|
35
45
|
import { myContract } from "./contract";
|
|
@@ -44,6 +54,41 @@ function App() {
|
|
|
44
54
|
useSockaSession(myContract, { url: "wss://...", wireFormat: "msgpack" }, []);
|
|
45
55
|
```
|
|
46
56
|
|
|
57
|
+
### `useSocka` — hold a `SockaSession` ref
|
|
58
|
+
|
|
59
|
+
Use **`useSocka(options, deps)`** when you need the full **`SockaSession`** (e.g. **`session.subscribe`**, **`session.client`**, **`waitForPush`**, or passing the session into non-React helpers). It returns **`{ ready, sessionRef, status, reconnecting, reconnectAttempt }`** — read **`sessionRef.current`** in effects or callbacks (it is **`null`** until the effect runs). Use **`reconnecting`** or **`status === "reconnecting"`** for banners; **`reconnectAttempt`** counts backoff attempts.
|
|
60
|
+
|
|
61
|
+
| | **`useSockaSession`** | **`useSocka`** |
|
|
62
|
+
|--|------------------------|----------------|
|
|
63
|
+
| **Returns** | **`{ ready, send, status, reconnecting, reconnectAttempt }`** | **`{ ready, sessionRef, status, reconnecting, reconnectAttempt }`** |
|
|
64
|
+
| **Best for** | Most React UIs that only call RPCs | Subscriptions, low-level client access, imperative APIs |
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useEffect } from "react";
|
|
68
|
+
import { useSocka } from "@firtoz/socka/react";
|
|
69
|
+
import { myContract } from "./contract";
|
|
70
|
+
|
|
71
|
+
function App() {
|
|
72
|
+
const { ready, sessionRef } = useSocka({ contract: myContract, url: "ws://..." }, []);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const s = sessionRef.current;
|
|
76
|
+
if (!ready || !s) return;
|
|
77
|
+
const onNotify = (p: { msg: string }) => console.log(p.msg);
|
|
78
|
+
s.subscribe.on("notify", onNotify);
|
|
79
|
+
return () => s.subscribe.off("notify", onNotify);
|
|
80
|
+
}, [ready, sessionRef]);
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
*(Example assumes your contract defines a **`notify`** push; use the real push names from **`myContract`**.)*
|
|
87
|
+
|
|
88
|
+
### `useSockaPresence` — snapshot + join/leave deltas
|
|
89
|
+
|
|
90
|
+
Call **`useSockaPresence(sessionRef, ready, options, deps)`** after **`useSocka`** / **`useSockaSession`**: it runs your **`snapshot`** RPC once, subscribes to **`joinPush`** / **`leavePush`**, and returns **`{ users, selfUserId, loading }`**. Pass the same **`deps`** as the connection when room identity changes.
|
|
91
|
+
|
|
47
92
|
### One WebSocket for the whole tree
|
|
48
93
|
|
|
49
94
|
If many components need **`send`**, avoid calling **`useSockaSession`** in each one (each call owns a connection). Mount a provider once and read the session from context:
|
|
@@ -65,7 +110,7 @@ function Layout({ roomId }: { roomId: string }) {
|
|
|
65
110
|
}
|
|
66
111
|
|
|
67
112
|
function Child() {
|
|
68
|
-
const { ready, send } = useSockaSessionContext(myContract);
|
|
113
|
+
const { ready, send, status, reconnecting } = useSockaSessionContext(myContract);
|
|
69
114
|
// ...
|
|
70
115
|
}
|
|
71
116
|
```
|
|
@@ -78,8 +123,8 @@ Use **`autoConnect: false`** on **`SockaWebSocketClient`** / **`SockaSession`**
|
|
|
78
123
|
|
|
79
124
|
## Client lifecycle
|
|
80
125
|
|
|
81
|
-
Treat each **`SockaSession`** / **`SockaWebSocketClient`** as bound to **one** underlying **`WebSocket`**. When the socket closes, pending calls reject
|
|
126
|
+
Treat each **`SockaSession`** / **`SockaWebSocketClient`** as bound to **one** underlying **`WebSocket`**. When the socket closes, pending calls reject on that instance unless you opt into client-side reconnect (see **[Reconnection](./reconnection.md)**). For a deliberate room/url change, construct a **new** client (in React, remount **`useSockaSession`** / **`SockaSessionProvider`** when **`url`** or identity **`deps`** change). Use **`ready`** / **`waitForOpen()`** before assuming the connection is usable; use **`onClose`** / **`onError`** (or **`reportError`**) for telemetry.
|
|
82
127
|
|
|
83
128
|
## Pushes
|
|
84
129
|
|
|
85
|
-
Server push and client subscriptions are covered in [Pushes](./
|
|
130
|
+
Server push and client subscriptions are covered in **[Pushes](./pushes.md)**.
|