@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.
Files changed (67) hide show
  1. package/README.md +195 -42
  2. package/dist/SockaWebSocketSession-B1w7RAid.d.ts +209 -0
  3. package/dist/bun/index.d.ts +30 -5
  4. package/dist/bun/index.js +28 -5
  5. package/dist/bun/index.js.map +1 -1
  6. package/dist/{chunk-MZCQHJXY.js → chunk-IFIGKR3W.js} +45 -8
  7. package/dist/chunk-IFIGKR3W.js.map +1 -0
  8. package/dist/{chunk-45D4T232.js → chunk-LVVCHLNW.js} +74 -9
  9. package/dist/chunk-LVVCHLNW.js.map +1 -0
  10. package/dist/{chunk-AM7PB26G.js → chunk-P3JEEOJL.js} +192 -10
  11. package/dist/chunk-P3JEEOJL.js.map +1 -0
  12. package/dist/chunk-QGURL3DJ.js +8 -0
  13. package/dist/chunk-QGURL3DJ.js.map +1 -0
  14. package/dist/client/index.d.ts +59 -3
  15. package/dist/client/index.js +2 -2
  16. package/dist/core/index.d.ts +2 -21
  17. package/dist/core/index.js +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/do/index.d.ts +20 -2
  20. package/dist/do/index.js +36 -2
  21. package/dist/do/index.js.map +1 -1
  22. package/dist/hono/cloudflare-workers.d.ts +4 -4
  23. package/dist/hono/cloudflare-workers.js +4 -3
  24. package/dist/hono/cloudflare-workers.js.map +1 -1
  25. package/dist/hono/index.d.ts +22 -6
  26. package/dist/hono/index.js +5 -3
  27. package/dist/hono/index.js.map +1 -1
  28. package/dist/react/index.d.ts +43 -4
  29. package/dist/react/index.js +103 -9
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/server/index.d.ts +18 -5
  32. package/dist/server/index.js +24 -4
  33. package/dist/server/index.js.map +1 -1
  34. package/dist/{socka-report-error-DzFI2Tr7.d.ts → socka-report-error-CXwpAUgl.d.ts} +80 -8
  35. package/dist/test/index.d.ts +11 -0
  36. package/dist/test/index.js +84 -0
  37. package/dist/test/index.js.map +1 -0
  38. package/docs/README.md +16 -7
  39. package/docs/auth.md +27 -0
  40. package/docs/backpressure.md +16 -0
  41. package/docs/client.md +48 -3
  42. package/docs/comparison.md +2 -2
  43. package/docs/durable-objects.md +3 -3
  44. package/docs/getting-started.md +143 -84
  45. package/docs/history.md +26 -0
  46. package/docs/internals.md +56 -0
  47. package/docs/lifecycle.md +3 -3
  48. package/docs/multi-room.md +10 -8
  49. package/docs/peers.md +11 -7
  50. package/docs/presence.md +43 -0
  51. package/docs/{events.md → pushes.md} +1 -1
  52. package/docs/recipes.md +77 -0
  53. package/docs/reconnection.md +44 -0
  54. package/docs/reference.md +27 -32
  55. package/docs/server.md +19 -3
  56. package/docs/testing.md +20 -0
  57. package/docs/wire-format.md +29 -0
  58. package/examples/minimal-socka.ts +56 -3
  59. package/package.json +14 -10
  60. package/roadmap.md +2 -2
  61. package/skills/socka/core-rpc/SKILL.md +2 -2
  62. package/skills/socka/do-session/SKILL.md +2 -2
  63. package/skills/socka/standard-schema/SKILL.md +1 -1
  64. package/dist/SockaWebSocketSession-Bru8yFcK.d.ts +0 -107
  65. package/dist/chunk-45D4T232.js.map +0 -1
  66. package/dist/chunk-AM7PB26G.js.map +0 -1
  67. package/dist/chunk-MZCQHJXY.js.map +0 -1
@@ -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"]}
@@ -1,6 +1,6 @@
1
- import { S as SockaWebSocketSession, a as SockaWebSocketSessionConfig, b as SockaWebSocketInit } from '../SockaWebSocketSession-Bru8yFcK.js';
2
- export { d as SockaEmitCapable, e as SockaPushSession, c as broadcastSockaEventToPeers, r as runSockaSessionOnAttached } from '../SockaWebSocketSession-Bru8yFcK.js';
3
- import { S as SockaContract, a as SockaContractConfig, b as SockaWireFormat } from '../socka-report-error-DzFI2Tr7.js';
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: SockaWebSocketSessionConfig<TContract, TData>, init?: SockaWebSocketInit): AttachedSockaWebSocket<TContract, TData>;
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
- export { type AttachedSockaWebSocket, SockaWebSocketInit, SockaWebSocketSession, SockaWebSocketSessionConfig, attachSockaWebSocket, dispatchSockaInboundMessage };
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 };
@@ -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-45D4T232.js';
4
- export { SockaWebSocketSession, broadcastSockaEventToPeers, runSockaSessionOnAttached } from '../chunk-45D4T232.js';
5
- import { reportSockaError } from '../chunk-MZCQHJXY.js';
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
- export { attachSockaWebSocket };
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
@@ -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 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: an optional input schema and a required output schema.
15
- * Both must be Standard Schema v1 compliant (Zod v4, Valibot, ArkType, etc.).
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: StandardSchemaV1;
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<StandardSchemaV1.InferOutput<P["output"]>> : () => Promise<StandardSchemaV1.InferOutput<P["output"]>>;
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
- * Each handler returns the output that will be validated before sending.
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): SockaServerErrorFrame;
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 SockaWireFormat as b, type SockaProcedureDef as c, defineSocka as d, type InferSockaHandlers as e, type InferSockaPushHandlers as f, type InferSockaPushPayload as g, type ReservedSockaProcedureName 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, defaultReportError as w, reportSockaError as x, type SockaReportError as y };
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":"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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) | Quickest Bun path, other runtimes, install, wire-up, tic-tac-toe demos |
8
- | [Peers](./peers.md) | Which peers to install per import path and why |
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, reconnect |
14
- | [Pushes](./events.md) | `emitPush` / `broadcastPush`, `session.subscribe`, ordering notes |
15
- | [Reference](./reference.md) | Wire encoding (JSON/msgpack), frame kinds, server/client config tables, types, errors, imports |
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 post–v1 ideas](../roadmap.md).
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 and should not be retried on the same instance. For reconnect or room changes, 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 backoff, toasts, or logging.
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](./events.md).
130
+ Server push and client subscriptions are covered in **[Pushes](./pushes.md)**.