@shadow-js/realtime 1.0.0 → 1.0.2
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/CHANGELOG.md +26 -0
- package/README.md +35 -0
- package/dist/dev/adapter.d.ts +0 -2
- package/dist/dev/client/core.d.ts +0 -2
- package/dist/dev/client/core.js +105 -108
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/presence.js +1 -2
- package/dist/dev/client/sse.js +34 -26
- package/dist/dev/client/sse.js.map +1 -1
- package/dist/dev/client/types.d.ts +6 -2
- package/dist/dev/client/util.js +3 -4
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/client/ws.js +1 -2
- package/dist/dev/cloudflare/adapter.d.ts +0 -2
- package/dist/dev/cloudflare/adapter.js +1 -2
- package/dist/dev/cloudflare/core.js +1 -2
- package/dist/dev/cloudflare/link.js +70 -10
- package/dist/dev/cloudflare/link.js.map +1 -1
- package/dist/dev/cloudflare/object.d.ts +0 -2
- package/dist/dev/cloudflare/object.js +1 -2
- package/dist/dev/cloudflare/types.d.ts +0 -2
- package/dist/dev/cloudflare/wire.js +1 -2
- package/dist/dev/errors.d.ts +0 -2
- package/dist/dev/errors.js +1 -2
- package/dist/dev/hooks.d.ts +0 -2
- package/dist/dev/hooks.js +1 -2
- package/dist/dev/memory.d.ts +11 -2
- package/dist/dev/memory.js +39 -12
- package/dist/dev/memory.js.map +1 -1
- package/dist/dev/protocol.d.ts +0 -2
- package/dist/dev/protocol.js +14 -14
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/redis/client.d.ts +0 -2
- package/dist/dev/redis/client.js +1 -2
- package/dist/dev/redis/scripts.js +1 -2
- package/dist/dev/redis/topics.d.ts +6 -3
- package/dist/dev/redis/topics.js +5 -3
- package/dist/dev/redis/topics.js.map +1 -1
- package/dist/dev/redis.d.ts +0 -2
- package/dist/dev/redis.js +1 -2
- package/dist/dev/server/http.d.ts +0 -2
- package/dist/dev/server/http.js +38 -26
- package/dist/dev/server/http.js.map +1 -1
- package/dist/dev/server/hub.d.ts +0 -2
- package/dist/dev/server/hub.js +67 -41
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +2 -4
- package/dist/dev/server/index.js +1 -2
- package/dist/dev/server/index.js.map +1 -1
- package/dist/dev/server/limits.d.ts +4 -3
- package/dist/dev/server/limits.js +1 -2
- package/dist/dev/server/limits.js.map +1 -1
- package/dist/dev/server/rules.d.ts +0 -2
- package/dist/dev/server/rules.js +1 -2
- package/dist/dev/server/rules.js.map +1 -1
- package/dist/dev/testing.d.ts +0 -2
- package/dist/dev/testing.js +1 -2
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/sse.js +1 -4
- package/dist/prod/client/sse.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/cloudflare/link.js +1 -1
- package/dist/prod/cloudflare/link.js.map +1 -1
- package/dist/prod/memory.js +1 -1
- package/dist/prod/memory.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/redis/topics.js +1 -1
- package/dist/prod/redis/topics.js.map +1 -1
- package/dist/prod/server/http.js +1 -1
- package/dist/prod/server/http.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/dist/prod/server/index.js.map +1 -1
- package/dist/prod/server/limits.js +1 -1
- package/dist/prod/server/limits.js.map +1 -1
- package/dist/prod/server/rules.js +1 -1
- package/dist/prod/server/rules.js.map +1 -1
- package/package.json +6 -5
package/dist/prod/server/http.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{RealtimeError as e}from"../errors.js";import{
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{TokenBucket as t}from"./limits.js";const n=`$realtime`,r=`$realtime:gone`;function i(e,t){return new Response(t,{status:e,headers:{"content-type":`application/json`,"cache-control":`no-store`}})}function a(e,t,n){return i(e,JSON.stringify({error:{code:t,message:n}}))}async function o(e,t){let n=new TextDecoder,r=``,i=0;for await(let a of e.body??[]){if(i+=a.byteLength,i>t)return;r+=n.decode(a,{stream:!0})}return r+n.decode()}function s(e,t){let n=e.headers.get(`origin`);if(n===null)return!0;if(t!==void 0)return t.includes(n);try{return n===new URL(e.url).origin}catch{return!1}}async function c(e,t){let n=await e.authenticate?.(t);return n instanceof Response?{refusal:n}:{identity:n??{}}}function l(e,t={}){return{async upgrade(e){let n=await c(t,e);return`refusal`in n?n.refusal:void 0},async open(i,a){let o=await c(t,a);if(i.signal.aborted||i.data[r]===!0)return;if(`refusal`in o){i.close(1008,``);return}let s={send:e=>i.send(e),close:(e,t)=>i.close(e,t)};i.data[n]=e.connect(s,o.identity)},message(e,t){let r=e.data[n];r!==void 0&&(typeof t==`string`?r.receive(t):e.close(1003,``))},close(e){e.data[r]=!0,e.data[n]?.closed()},error(t,r){e.report(r,{where:`websocket`}),t.data[n]?.closed()}}}async function u(e,t){try{await e.send({event:`rt`,data:JSON.stringify({t:`bye`,code:1008,why:t})})}catch{}e.close()}function d(e){let t=e.searchParams.get(`s`);if(t===null)return[];try{let e=JSON.parse(t);return!Array.isArray(e)||e.length>64?void 0:e.map(e=>typeof e==`object`&&e?e:{})}catch{return}}function f(t,n={}){return async(r,i)=>{if(!s(r.request,n.allowedOrigins)){t.report(new e(`forbidden`,void 0),{where:`sse.origin`}),await u(i,``);return}let a=await c(n,r),o=`refusal`in a?void 0:d(new URL(r.request.url));if(`refusal`in a||o===void 0){await u(i,``);return}let l=0,f=t.connect({send(e){return!i.closed&&(l+=e.length,i.send({event:`rt`,data:e}).then(()=>{l-=e.length},()=>{l=2**53-1,f.closed()}),!0)},close:()=>i.close(),buffered:()=>l},a.identity);o.forEach((e,t)=>{f.receive(JSON.stringify({...e,t:`sub`,n:t+1}))}),i.signal.aborted||await new Promise(e=>{i.signal.addEventListener(`abort`,()=>e(),{once:!0})}),f.closed(),await f.done}}function p(n,r={}){let l=/* @__PURE__ */ new Map,u=e=>{let r=l.get(e);if(r===void 0){if(l.size>=1e4){let e=l.keys().next().value;e!==void 0&&l.delete(e)}r=new t(n.limits.publishesPerSecond,n.limits.publishBurst,Date.now()),l.set(e,r)}return r};return async t=>{let{request:l}=t;if(!s(l,r.allowedOrigins))return a(403,`forbidden`,void 0);let d=await c(r,t);if(`refusal`in d)return d.refusal;let f=await o(l,n.limits.maxMessageBytes*8);if(f===void 0)return a(413,`too_large`,void 0);let p;try{p=JSON.parse(f)}catch{return a(400,`bad_frame`,void 0)}let m=p?.frames,h=p?.cid;if(!Array.isArray(m)||m.length===0||m.length>64)return a(400,`bad_frame`,void 0);let g=r.key?.(t,d.identity)??d.identity.user??`anonymous`,_=[],v=n.connect({send(e){return e.startsWith(`{"t":"ready"`)||_.push(e),!0},close:()=>void 0},d.identity,{stateless:!0,publishBucket:u(g),...typeof h==`string`?{actingFor:h}:{}});try{for(let t of m){let r=t?.t;if(r===`pres`){let r=t;try{if(typeof r.c!=`string`||typeof h!=`string`)throw new e(`bad_frame`,void 0);if(!u(g).take(Date.now()))throw new e(`rate_limited`);await n.setMemberState(d.identity,r.c,h,r.s??null)}catch(t){let n=t instanceof e?t.code:`unavailable`;_.push(JSON.stringify({t:`err`,code:n}))}}else r!==`pub`&&r!==`req`?_.push(JSON.stringify({t:`err`,code:`bad_frame`,msg:void 0})):v.receive(JSON.stringify(t))}await v.settled()}finally{v.closed()}return i(200,`{"frames":[${_.join(`,`)}]}`)}}export{p as postRoute,f as sseRoute,l as websocketRoute};
|
|
2
2
|
//# sourceMappingURL=http.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":["KEY","refuse","originAllowed","origin","request","allowed","identify","result","options","event","websocketRoute","ws","transport","frame","code","reason","hub","connection","message","error","refuseStream","stream","declared","text","url","value","item","sseRoute","RealtimeError","entries","pendingBytes","at","entry","resolve","postRoute","buckets","bucketFor","bucket","key","oldest","TokenBucket","byteLength","body","frames","cid","answers","type"],"sources":["../../../src/server/http.ts"],"sourcesContent":["import { RealtimeError } from \"../errors\";\nimport { byteLength, type Json } from \"../protocol\";\nimport type { HubConnection, HubCore, Transport } from \"./hub\";\nimport { TokenBucket } from \"./limits\";\nimport type { Identity } from \"./rules\";\n\n/** What a hub needs from the framework's request event. @since 1.0.0 */\nexport interface RequestLike {\n readonly request: Request;\n}\n\n/** How a connection is identified: an identity, or a response that refuses it. @since 1.0.0 */\nexport type AuthenticateResult = Identity | undefined | Response;\n\n/** Options shared by the transport routes. @since 1.0.0 */\nexport interface HubRouteOptions<E extends RequestLike> {\n /**\n * Reads the identity from the request (the session cookie, a bearer token). Return a `Response` to refuse (a 401). Return\n * nothing for an anonymous connection. Default: everyone is anonymous, and channel rules decide.\n */\n readonly authenticate?: (event: E) => AuthenticateResult | Promise<AuthenticateResult>;\n /**\n * Origins allowed to open a stream or post (full origins such as `https://app.example.com`). Default: the request's own\n * origin only. A request with no `Origin` header (same-origin GET, a non-browser client) is allowed.\n */\n readonly allowedOrigins?: readonly string[];\n}\n\n/** The WebSocket connection the framework hands to handlers (the part this package uses). @since 1.0.0 */\nexport interface WebSocketLike {\n readonly data: Record<string, unknown>;\n readonly signal: AbortSignal;\n send(message: string | Uint8Array): boolean;\n close(code?: number, reason?: string): void;\n}\n\n/** The handlers of a WebSocket route, shaped like Dusk's `WS` export. @since 1.0.0 */\nexport interface WebSocketRoute<E extends RequestLike> {\n upgrade(event: E): Promise<Response | undefined>;\n open(ws: WebSocketLike, event: E): Promise<void>;\n message(ws: WebSocketLike, message: string | Uint8Array, event: E): void;\n close(ws: WebSocketLike): void;\n error(ws: WebSocketLike, error: unknown, event: E): void;\n}\n\n/** The stream a server-sent events route writes to (the part this package uses). @since 1.0.0 */\nexport interface SseLike {\n send(message: { readonly event: string; readonly data: string }): Promise<void>;\n close(): void;\n readonly signal: AbortSignal;\n readonly closed: boolean;\n}\n\nconst KEY = \"$realtime\";\nconst MAX_FRAMES_PER_POST = 64;\nconst MAX_SUBSCRIPTIONS_IN_QUERY = 64;\nconst MAX_BUCKETS = 10_000;\n\nfunction refuse(status: number, code: RealtimeError[\"code\"], message: string): Response {\n return new Response(JSON.stringify({ error: { code, message } }), {\n status,\n headers: { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" },\n });\n}\n\n/** Whether the request's `Origin` header is acceptable. A missing header is allowed (see `HubRouteOptions.allowedOrigins`). */\nfunction originAllowed(request: Request, allowed: readonly string[] | undefined): boolean {\n const origin = request.headers.get(\"origin\");\n if (origin === null) return true;\n if (allowed !== undefined) return allowed.includes(origin);\n try {\n return origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\nasync function identify<E extends RequestLike>(\n options: HubRouteOptions<E>,\n event: E,\n): Promise<{ readonly identity: Identity } | { readonly refusal: Response }> {\n const result = await options.authenticate?.(event);\n if (result instanceof Response) return { refusal: result };\n return { identity: result ?? {} };\n}\n\n/**\n * The WebSocket route of a hub: `export const WS = hub.websocket({ authenticate })` in a Dusk stream file. The upgrade is decided\n * by `authenticate` (a `Response` refuses it) and Dusk's own checks (origin, caps, size); each text frame goes to the hub.\n * Binary frames close the connection with 1003.\n *\n * @param hub The hub.\n * @param options How to identify the connection.\n * @returns Handlers shaped like Dusk's `WS` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const WS = hub.websocket();\n * @since 1.0.0\n */\nexport function websocketRoute<E extends RequestLike>(\n hub: HubCore,\n options: Pick<HubRouteOptions<E>, \"authenticate\"> = {},\n): WebSocketRoute<E> {\n return {\n async upgrade(event) {\n const result = await identify(options, event);\n return \"refusal\" in result ? result.refusal : undefined;\n },\n async open(ws, event) {\n const result = await identify(options, event);\n if (\"refusal\" in result) {\n ws.close(1008, \"unauthorized\");\n return;\n }\n const transport: Transport = {\n send: (frame) => ws.send(frame),\n close: (code, reason) => ws.close(code, reason),\n };\n ws.data[KEY] = hub.connect(transport, result.identity);\n },\n message(ws, message) {\n const connection = ws.data[KEY] as HubConnection | undefined;\n if (connection === undefined) return;\n if (typeof message !== \"string\") {\n ws.close(1003, \"binary frames are not supported\");\n return;\n }\n connection.receive(message);\n },\n close(ws) {\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n error(ws, error) {\n hub.report(error, { where: \"websocket\" });\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n };\n}\n\n/**\n * Refuses a stream that has already started (the status 200 is sent before the handler runs): a `bye` with the policy code the\n * client treats as fatal, so it gives up instead of reconnecting for ever, then the end of the stream.\n */\nasync function refuseStream(stream: SseLike, why: string): Promise<void> {\n try {\n await stream.send({ event: \"rt\", data: JSON.stringify({ t: \"bye\", code: 1008, why }) });\n } catch {\n // A client that already left needs no answer.\n }\n stream.close();\n}\n\n/** Reads the subscriptions a stream request declares: `?s=[{\"c\":\"chat:lobby\",\"after\":12}]`. */\nfunction declared(url: URL): readonly Record<string, unknown>[] | undefined {\n const text = url.searchParams.get(\"s\");\n if (text === null) return [];\n try {\n const value: unknown = JSON.parse(text);\n if (!Array.isArray(value) || value.length > MAX_SUBSCRIPTIONS_IN_QUERY) return undefined;\n return value.map((item) =>\n item !== null && typeof item === \"object\" ? (item as Record<string, unknown>) : {},\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * The server-sent events route of a hub, for hosts that cannot hold a WebSocket: `export const SSE = hub.sse()`. The channels\n * are declared in the request (`?s=[{\"c\":\"chat:lobby\",\"after\":12,\"echo\":false,\"presence\":{\"typing\":false}}]`); to change them the\n * client opens a new stream. Frames go out as events named `rt`; everything the client sends goes to {@link postRoute}.\n *\n * @param hub The hub.\n * @param options How to identify the connection and which origins may connect.\n * @returns A handler shaped like Dusk's `SSE` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const SSE = hub.sse();\n * @since 1.0.0\n */\nexport function sseRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> = {},\n): (event: E, stream: SseLike) => Promise<void> {\n return async (event, stream) => {\n if (!originAllowed(event.request, options.allowedOrigins)) {\n hub.report(new RealtimeError(\"forbidden\", \"origin refused\"), { where: \"sse.origin\" });\n await refuseStream(stream, \"origin not allowed\");\n return;\n }\n const result = await identify(options, event);\n if (\"refusal\" in result) {\n await refuseStream(stream, \"unauthorized\");\n return;\n }\n const entries = declared(new URL(event.request.url));\n if (entries === undefined) {\n await refuseStream(stream, \"bad subscription list\");\n return;\n }\n let pendingBytes = 0;\n const transport: Transport = {\n send(frame) {\n if (stream.closed) return false;\n pendingBytes += frame.length;\n stream.send({ event: \"rt\", data: frame }).then(\n () => {\n pendingBytes -= frame.length;\n },\n () => {\n pendingBytes = Number.MAX_SAFE_INTEGER;\n connection.closed();\n },\n );\n return true;\n },\n close: () => stream.close(),\n buffered: () => pendingBytes,\n };\n const connection = hub.connect(transport, result.identity);\n entries.forEach((entry, at) => {\n connection.receive(JSON.stringify({ t: \"sub\", n: at + 1, ...entry }));\n });\n if (!stream.signal.aborted)\n await new Promise<void>((resolve) => {\n stream.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n connection.closed();\n await connection.done;\n };\n}\n\n/**\n * The HTTP route that carries what an SSE client sends: `export const POST = hub.post()`. The body is\n * `{\"cid\": \"<connection id>\", \"frames\": [ ... ]}` (at most 64 frames of the types `pub`, `req`, `pres`); the answer is\n * `{\"frames\": [ ... ]}` with the acks, replies and errors. It keeps nothing between requests, so any server can answer it.\n *\n * The request rate of one caller is limited by `key` (default: the user, else all anonymous callers together) with the hub's\n * publish limits; put Dusk's rate limiting in front of the route for per-address limits.\n *\n * @param hub The hub.\n * @param options Identity, origins and the rate-limit key.\n * @returns A handler for a Dusk API route.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const POST = hub.post();\n * @since 1.0.0\n */\nexport function postRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string } = {},\n): (event: E) => Promise<Response> {\n const buckets = new Map<string, TokenBucket>();\n const bucketFor = (key: string): TokenBucket => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n if (buckets.size >= MAX_BUCKETS) {\n const oldest = buckets.keys().next().value;\n if (oldest !== undefined) buckets.delete(oldest);\n }\n bucket = new TokenBucket(hub.limits.publishesPerSecond, hub.limits.publishBurst, Date.now());\n buckets.set(key, bucket);\n }\n return bucket;\n };\n return async (event) => {\n const { request } = event;\n if (!originAllowed(request, options.allowedOrigins))\n return refuse(403, \"forbidden\", \"origin not allowed\");\n const result = await identify(options, event);\n if (\"refusal\" in result) return result.refusal;\n const text = await request.text();\n if (byteLength(text) > hub.limits.maxMessageBytes * 8)\n return refuse(413, \"too_large\", \"the request is too large\");\n let body: unknown;\n try {\n body = JSON.parse(text);\n } catch {\n return refuse(400, \"bad_frame\", \"the body is not JSON\");\n }\n const frames = (body as { frames?: unknown } | null)?.frames;\n const cid = (body as { cid?: unknown } | null)?.cid;\n if (!Array.isArray(frames) || frames.length === 0 || frames.length > MAX_FRAMES_PER_POST)\n return refuse(400, \"bad_frame\", `frames must hold 1..${MAX_FRAMES_PER_POST} frames`);\n const key = options.key?.(event, result.identity) ?? result.identity.user ?? \"anonymous\";\n const answers: string[] = [];\n const transport: Transport = {\n send(frame) {\n if (!frame.startsWith('{\"t\":\"ready\"')) answers.push(frame);\n return true;\n },\n close: () => undefined,\n };\n const connection = hub.connect(transport, result.identity, {\n stateless: true,\n publishBucket: bucketFor(key),\n ...(typeof cid === \"string\" ? { actingFor: cid } : {}),\n });\n try {\n for (const frame of frames) {\n const type = (frame as { t?: unknown } | null)?.t;\n if (type === \"pres\") {\n const item = frame as { c?: unknown; s?: unknown };\n try {\n if (typeof item.c !== \"string\" || typeof cid !== \"string\")\n throw new RealtimeError(\"bad_frame\", \"pres needs c and the connection id\");\n await hub.setMemberState(result.identity, item.c, cid, (item.s ?? null) as Json);\n } catch (error) {\n const code = error instanceof RealtimeError ? error.code : \"unavailable\";\n answers.push(JSON.stringify({ t: \"err\", code }));\n }\n continue;\n }\n if (type !== \"pub\" && type !== \"req\") {\n answers.push(\n JSON.stringify({ t: \"err\", code: \"bad_frame\", msg: \"only pub, req and pres\" }),\n );\n continue;\n }\n connection.receive(JSON.stringify(frame));\n }\n await connection.settled();\n } finally {\n connection.closed();\n }\n return new Response(`{\"frames\":[${answers.join(\",\")}]}`, {\n status: 200,\n headers: { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" },\n });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.js","names":["KEY","GONE","jsonResponse","body","refuse","status","readBounded","decoder","text","bytes","chunk","request","max","originAllowed","origin","allowed","identify","result","options","event","websocketRoute","ws","transport","frame","code","reason","hub","connection","message","error","refuseStream","stream","declared","url","value","item","sseRoute","RealtimeError","entries","pendingBytes","entry","at","resolve","postRoute","buckets","bucketFor","bucket","key","oldest","TokenBucket","frames","cid","answers","type"],"sources":["../../../src/server/http.ts"],"sourcesContent":["import { RealtimeError } from \"../errors\";\nimport type { Json } from \"../protocol\";\nimport type { HubConnection, HubCore, Transport } from \"./hub\";\nimport { TokenBucket } from \"./limits\";\nimport type { Identity } from \"./rules\";\n\n/** What a hub needs from the framework's request event. @since 1.0.0 */\nexport interface RequestLike {\n readonly request: Request;\n}\n\n/** How a connection is identified: an identity, or a response that refuses it. @since 1.0.0 */\nexport type AuthenticateResult = Identity | undefined | Response;\n\n/** Options shared by the transport routes. @since 1.0.0 */\nexport interface HubRouteOptions<E extends RequestLike> {\n /**\n * Reads the identity from the request (the session cookie, a bearer token). Return a `Response` to refuse (a 401). Return\n * nothing for an anonymous connection. Default: everyone is anonymous, and channel rules decide.\n */\n readonly authenticate?: (event: E) => AuthenticateResult | Promise<AuthenticateResult>;\n /**\n * Origins allowed to open a stream or post (full origins such as `https://app.example.com`). Default: the request's own\n * origin only. A request with no `Origin` header (same-origin GET, a non-browser client) is allowed.\n */\n readonly allowedOrigins?: readonly string[];\n}\n\n/** The WebSocket connection the framework hands to handlers (the part this package uses). @since 1.0.0 */\nexport interface WebSocketLike {\n readonly data: Record<string, unknown>;\n readonly signal: AbortSignal;\n send(message: string | Uint8Array): boolean;\n close(code?: number, reason?: string): void;\n}\n\n/** The handlers of a WebSocket route, shaped like Dusk's `WS` export. @since 1.0.0 */\nexport interface WebSocketRoute<E extends RequestLike> {\n upgrade(event: E): Promise<Response | undefined>;\n open(ws: WebSocketLike, event: E): Promise<void>;\n message(ws: WebSocketLike, message: string | Uint8Array, event: E): void;\n close(ws: WebSocketLike): void;\n error(ws: WebSocketLike, error: unknown, event: E): void;\n}\n\n/** The stream a server-sent events route writes to (the part this package uses). @since 1.0.0 */\nexport interface SseLike {\n send(message: { readonly event: string; readonly data: string }): Promise<void>;\n close(): void;\n readonly signal: AbortSignal;\n readonly closed: boolean;\n}\n\nconst KEY = \"$realtime\";\n/** Marks a socket that closed before its connection existed. */\nconst GONE = \"$realtime:gone\";\nconst MAX_FRAMES_PER_POST = 64;\nconst MAX_SUBSCRIPTIONS_IN_QUERY = 64;\nconst MAX_BUCKETS = 10_000;\n\n/** A JSON answer that no cache may keep. */\nfunction jsonResponse(status: number, body: string): Response {\n return new Response(body, {\n status,\n headers: { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" },\n });\n}\n\nfunction refuse(\n status: number,\n code: RealtimeError[\"code\"],\n message: string | undefined,\n): Response {\n return jsonResponse(status, JSON.stringify({ error: { code, message } }));\n}\n\n/**\n * Reads a request body as text, stopping at `max` bytes: a body that never ends must not be held in memory before it is\n * refused. Undefined when the body is larger than `max`.\n */\nasync function readBounded(request: Request, max: number): Promise<string | undefined> {\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n // Leaving the loop early cancels the stream, so the rest of an oversized body is never pulled.\n for await (const chunk of (request.body ?? []) as AsyncIterable<Uint8Array>) {\n bytes += chunk.byteLength;\n if (bytes > max) return undefined;\n text += decoder.decode(chunk, { stream: true });\n }\n return text + decoder.decode();\n}\n\n/** Whether the request's `Origin` header is acceptable. A missing header is allowed (see `HubRouteOptions.allowedOrigins`). */\nfunction originAllowed(request: Request, allowed: readonly string[] | undefined): boolean {\n const origin = request.headers.get(\"origin\");\n if (origin === null) return true;\n if (allowed !== undefined) return allowed.includes(origin);\n try {\n return origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\nasync function identify<E extends RequestLike>(\n options: HubRouteOptions<E>,\n event: E,\n): Promise<{ readonly identity: Identity } | { readonly refusal: Response }> {\n const result = await options.authenticate?.(event);\n if (result instanceof Response) return { refusal: result };\n return { identity: result ?? {} };\n}\n\n/**\n * The WebSocket route of a hub: `export const WS = hub.websocket({ authenticate })` in a Dusk stream file. The upgrade is decided\n * by `authenticate` (a `Response` refuses it) and Dusk's own checks (origin, caps, size); each text frame goes to the hub.\n * Binary frames close the connection with 1003.\n *\n * @param hub The hub.\n * @param options How to identify the connection.\n * @returns Handlers shaped like Dusk's `WS` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const WS = hub.websocket();\n * @since 1.0.0\n */\nexport function websocketRoute<E extends RequestLike>(\n hub: HubCore,\n options: Pick<HubRouteOptions<E>, \"authenticate\"> = {},\n): WebSocketRoute<E> {\n return {\n async upgrade(event) {\n const result = await identify(options, event);\n return \"refusal\" in result ? result.refusal : undefined;\n },\n async open(ws, event) {\n const result = await identify(options, event);\n // `authenticate` can take a while: a peer that left in the meantime already had its `close`, which found no connection to\n // release, so connecting now would leave one in the hub for good.\n if (ws.signal.aborted || ws.data[GONE] === true) return;\n if (\"refusal\" in result) {\n ws.close(1008, __DEV__ ? \"unauthorized\" : \"\");\n return;\n }\n const transport: Transport = {\n send: (frame) => ws.send(frame),\n close: (code, reason) => ws.close(code, reason),\n };\n ws.data[KEY] = hub.connect(transport, result.identity);\n },\n message(ws, message) {\n const connection = ws.data[KEY] as HubConnection | undefined;\n if (connection === undefined) return;\n if (typeof message !== \"string\") {\n ws.close(1003, __DEV__ ? \"binary frames are not supported\" : \"\");\n return;\n }\n connection.receive(message);\n },\n close(ws) {\n ws.data[GONE] = true;\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n error(ws, error) {\n hub.report(error, { where: \"websocket\" });\n (ws.data[KEY] as HubConnection | undefined)?.closed();\n },\n };\n}\n\n/**\n * Refuses a stream that has already started (the status 200 is sent before the handler runs): a `bye` with the policy code the\n * client treats as fatal, so it gives up instead of reconnecting for ever, then the end of the stream.\n */\nasync function refuseStream(stream: SseLike, why: string): Promise<void> {\n try {\n await stream.send({ event: \"rt\", data: JSON.stringify({ t: \"bye\", code: 1008, why }) });\n } catch {\n // A client that already left needs no answer.\n }\n stream.close();\n}\n\n/** Reads the subscriptions a stream request declares: `?s=[{\"c\":\"chat:lobby\",\"after\":12}]`. */\nfunction declared(url: URL): readonly Record<string, unknown>[] | undefined {\n const text = url.searchParams.get(\"s\");\n if (text === null) return [];\n try {\n const value: unknown = JSON.parse(text);\n if (!Array.isArray(value) || value.length > MAX_SUBSCRIPTIONS_IN_QUERY) return undefined;\n return value.map((item) =>\n item !== null && typeof item === \"object\" ? (item as Record<string, unknown>) : {},\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * The server-sent events route of a hub, for hosts that cannot hold a WebSocket: `export const SSE = hub.sse()`. The channels\n * are declared in the request (`?s=[{\"c\":\"chat:lobby\",\"after\":12,\"echo\":false,\"presence\":{\"typing\":false}}]`); to change them the\n * client opens a new stream. Frames go out as events named `rt`; everything the client sends goes to {@link postRoute}.\n *\n * @param hub The hub.\n * @param options How to identify the connection and which origins may connect.\n * @returns A handler shaped like Dusk's `SSE` export.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const SSE = hub.sse();\n * @since 1.0.0\n */\nexport function sseRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> = {},\n): (event: E, stream: SseLike) => Promise<void> {\n return async (event, stream) => {\n if (!originAllowed(event.request, options.allowedOrigins)) {\n hub.report(new RealtimeError(\"forbidden\", __DEV__ ? \"origin refused\" : undefined), {\n where: \"sse.origin\",\n });\n await refuseStream(stream, __DEV__ ? \"origin not allowed\" : \"\");\n return;\n }\n const result = await identify(options, event);\n // The list is read only for an identified connection; either refusal ends the stream the same way.\n const entries = \"refusal\" in result ? undefined : declared(new URL(event.request.url));\n if (\"refusal\" in result || entries === undefined) {\n await refuseStream(\n stream,\n __DEV__ ? (\"refusal\" in result ? \"unauthorized\" : \"bad subscription list\") : \"\",\n );\n return;\n }\n let pendingBytes = 0;\n const transport: Transport = {\n send(frame) {\n if (stream.closed) return false;\n pendingBytes += frame.length;\n stream.send({ event: \"rt\", data: frame }).then(\n () => {\n pendingBytes -= frame.length;\n },\n () => {\n pendingBytes = Number.MAX_SAFE_INTEGER;\n connection.closed();\n },\n );\n return true;\n },\n close: () => stream.close(),\n buffered: () => pendingBytes,\n };\n const connection = hub.connect(transport, result.identity);\n entries.forEach((entry, at) => {\n // The type and the number come last: an entry that names its own `t` must not turn a declaration into a publish.\n connection.receive(JSON.stringify({ ...entry, t: \"sub\", n: at + 1 }));\n });\n if (!stream.signal.aborted)\n await new Promise<void>((resolve) => {\n stream.signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n connection.closed();\n await connection.done;\n };\n}\n\n/**\n * The HTTP route that carries what an SSE client sends: `export const POST = hub.post()`. The body is\n * `{\"cid\": \"<connection id>\", \"frames\": [ ... ]}` (at most 64 frames of the types `pub`, `req`, `pres`); the answer is\n * `{\"frames\": [ ... ]}` with the acks, replies and errors. It keeps nothing between requests, so any server can answer it.\n *\n * The request rate of one caller is limited by `key` (default: the user, else all anonymous callers together) with the hub's\n * publish limits; put Dusk's rate limiting in front of the route for per-address limits.\n *\n * @param hub The hub.\n * @param options Identity, origins and the rate-limit key.\n * @returns A handler for a Dusk API route.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({ channels: { \"chat:{room}\": { subscribe: true, publish: true } } });\n * export const POST = hub.post();\n * @since 1.0.0\n */\nexport function postRoute<E extends RequestLike>(\n hub: HubCore,\n options: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string } = {},\n): (event: E) => Promise<Response> {\n const buckets = new Map<string, TokenBucket>();\n const bucketFor = (key: string): TokenBucket => {\n let bucket = buckets.get(key);\n if (bucket === undefined) {\n if (buckets.size >= MAX_BUCKETS) {\n const oldest = buckets.keys().next().value;\n if (oldest !== undefined) buckets.delete(oldest);\n }\n bucket = new TokenBucket(hub.limits.publishesPerSecond, hub.limits.publishBurst, Date.now());\n buckets.set(key, bucket);\n }\n return bucket;\n };\n return async (event) => {\n const { request } = event;\n if (!originAllowed(request, options.allowedOrigins))\n return refuse(403, \"forbidden\", __DEV__ ? \"origin not allowed\" : undefined);\n const result = await identify(options, event);\n if (\"refusal\" in result) return result.refusal;\n const text = await readBounded(request, hub.limits.maxMessageBytes * 8);\n if (text === undefined)\n return refuse(413, \"too_large\", __DEV__ ? \"the request is too large\" : undefined);\n let body: unknown;\n try {\n body = JSON.parse(text);\n } catch {\n return refuse(400, \"bad_frame\", __DEV__ ? \"the body is not JSON\" : undefined);\n }\n const frames = (body as { frames?: unknown } | null)?.frames;\n const cid = (body as { cid?: unknown } | null)?.cid;\n if (!Array.isArray(frames) || frames.length === 0 || frames.length > MAX_FRAMES_PER_POST)\n return refuse(\n 400,\n \"bad_frame\",\n __DEV__ ? `frames must hold 1..${MAX_FRAMES_PER_POST} frames` : undefined,\n );\n const key = options.key?.(event, result.identity) ?? result.identity.user ?? \"anonymous\";\n const answers: string[] = [];\n const transport: Transport = {\n send(frame) {\n if (!frame.startsWith('{\"t\":\"ready\"')) answers.push(frame);\n return true;\n },\n close: () => undefined,\n };\n const connection = hub.connect(transport, result.identity, {\n stateless: true,\n publishBucket: bucketFor(key),\n ...(typeof cid === \"string\" ? { actingFor: cid } : {}),\n });\n try {\n for (const frame of frames) {\n const type = (frame as { t?: unknown } | null)?.t;\n if (type === \"pres\") {\n const item = frame as { c?: unknown; s?: unknown };\n try {\n if (typeof item.c !== \"string\" || typeof cid !== \"string\")\n throw new RealtimeError(\n \"bad_frame\",\n __DEV__ ? \"pres needs c and the connection id\" : undefined,\n );\n // Each change costs the adapter two calls: it spends from the same bucket as a publish, as it does on a socket.\n if (!bucketFor(key).take(Date.now())) throw new RealtimeError(\"rate_limited\");\n await hub.setMemberState(result.identity, item.c, cid, (item.s ?? null) as Json);\n } catch (error) {\n const code = error instanceof RealtimeError ? error.code : \"unavailable\";\n answers.push(JSON.stringify({ t: \"err\", code }));\n }\n continue;\n }\n if (type !== \"pub\" && type !== \"req\") {\n answers.push(\n JSON.stringify({\n t: \"err\",\n code: \"bad_frame\",\n msg: __DEV__ ? \"only pub, req and pres\" : undefined,\n }),\n );\n continue;\n }\n connection.receive(JSON.stringify(frame));\n }\n await connection.settled();\n } finally {\n connection.closed();\n }\n return jsonResponse(200, `{\"frames\":[${answers.join(\",\")}]}`);\n };\n}\n"],"mappings":"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"}
|
package/dist/prod/server/hub.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeClientFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{TokenBucket as o,WindowCounter as s,resolveLimits as c}from"./limits.js";import{memoryAdapter as l}from"../memory.js";import{compileRule as u,matchRule as d}from"./rules.js";function f(){let e=/* @__PURE__ */ new Uint8Array(16);crypto.getRandomValues(e);let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function p(e){return JSON.stringify(e)}function m(e,t,n){return`{"t":"msg","c":${JSON.stringify(e)},"s":${t.seq},"e":${JSON.stringify(t.event)},"d":${t.data}`+(t.publisher===void 0?``:`,"p":${JSON.stringify(t.publisher)}`)+(t.user===void 0?``:`,"u":${JSON.stringify(t.user)}`)+(n?`,"r":1}`:`}`)}function h(h={}){let g=c(h.limits),_=h.now??Date.now,v=h.adapter===void 0,y=h.adapter??l({now:_}),b=[],x=/* @__PURE__ */ new Map,S=/* @__PURE__ */ new Map,C,w=!1,T=!1,E={published:0,delivered:0,refused:0,slowConsumers:0},D=(e,t)=>{if(h.onError!==void 0)try{h.onError(e,t)}catch{}else console.error(`[realtime] ${t.where}`,e)};function O(e){for(let t of b){let n=d(t,e);if(n!==void 0)return{rule:t.rule,params:n}}}async function k(t,n,r,i,a){let o=t[n];if(o===void 0||o===!1)return!1;if(o===!0)return!0;try{return await o({channel:r,params:i,identity:a,op:n})===!0}catch(t){throw D(t,{where:`rule.${n}`,channel:r}),new e(`unavailable`,`the channel rule could not be evaluated`)}}function A(e,t){if(!e.closed){try{if((e.transport.buffered?.()??0)>g.maxBufferedBytes){E.slowConsumers+=1,N(e,1013,`slow consumer`);return}if(e.transport.send(t))return}catch(t){D(t,{where:`transport.send`,connection:e.id})}Z(e)}}function j(e,t){A(e,p(t))}function M(e,t,n,r,i){E.refused+=1,j(e,{t:`err`,...t===void 0?{}:{n:t},...r===void 0?{}:{c:r},code:n,...i===void 0?{}:{msg:i}})}function N(e,t,n){if(!e.closed){try{e.transport.send(p({t:`bye`,code:t,why:n}))}catch{}try{e.transport.close(t,n)}catch{}Z(e)}}function P(e){let t=S.get(e);return(t===void 0||t.closing)&&(t={name:e,subs:/* @__PURE__ */ new Set,bucket:new o(g.channelPublishesPerSecond,g.channelPublishesPerSecond,_()),adapterSub:void 0,presenceSub:void 0,closing:!1,frame:void 0},S.set(e,t)),t}async function F(e){e.adapterSub===void 0&&(e.adapterSub=y.subscribe(e.name,t=>L(e,t)),e.adapterSub.catch(()=>{e.adapterSub=void 0})),await e.adapterSub}async function I(e){e.presenceSub===void 0&&(e.presenceSub=y.presenceSubscribe(e.name,t=>B(e,t)),e.presenceSub.catch(()=>{e.presenceSub=void 0})),await e.presenceSub}function L(e,t){for(let n of e.subs)n.active&&(n.pumping?n.buffer.length>=1e4?H(n,`gap`):n.buffer.push(t):t.seq===n.lastSeq+1?R(n,t,!1):t.seq>n.lastSeq+1?(n.buffer.push(t),z(n,void 0)):t.at>n.lastAt&&H(n,`reset`))}function R(e,t,n){if(e.lastSeq=t.seq,e.lastAt=Math.max(e.lastAt,t.at),!e.echo&&t.publisher===e.conn.id)return;if(E.delivered+=1,n){A(e.conn,m(e.channel.name,t,!0));return}let r=e.channel.frame,i=r!==void 0&&r.seq===t.seq?r.text:m(e.channel.name,t,!1);(r===void 0||r.seq!==t.seq)&&(e.channel.frame={seq:t.seq,text:i}),A(e.conn,i)}async function z(e,t){if(!(e.pumping&&t===void 0)){e.pumping=!0;try{for(;e.active;){e.buffer=e.buffer.filter(t=>t.seq>e.lastSeq);let n=e.buffer[0];if(n!==void 0&&n.seq===e.lastSeq+1){R(e,n,!1),e.buffer.shift();continue}let r=n===void 0?t??0:n.seq-1;if(e.lastSeq>=r)break;let i=await y.history(e.channel.name,e.lastSeq,Math.min(500,r-e.lastSeq));if(!e.active)return;let a=i.messages[0];if(a===void 0||a.seq!==e.lastSeq+1){if(!e.gapOk){H(e,`gap`);return}e.lastSeq=(a?.seq??r+1)-1}else for(let t of i.messages){if(t.seq>r)break;R(e,t,!0)}}}catch(t){D(t,{where:`pump`,channel:e.channel.name,connection:e.conn.id}),e.active&&H(e,`unavailable`)}finally{e.pumping=!1}e.active&&e.buffer.length>0&&z(e,void 0)}}function B(e,t){for(let n of e.subs)n.active&&(n.joined||n.watch)&&(n.presenceQueue===void 0?V(n,t):n.presenceQueue.push(t))}function V(e,t){j(e.conn,{t:`pres`,c:t.channel,op:t.op,m:t.member})}function H(e,t){e.active&&(e.active=!1,e.conn.subs.delete(e.channel.name),e.channel.subs.delete(e),t!==void 0&&!e.conn.closed&&j(e.conn,{t:`unsubbed`,c:e.channel.name,...t===``?{}:{why:t}}),W(e))}let U=/* @__PURE__ */ new Set;function W(e){let t=G(e);return U.add(t),t.finally(()=>U.delete(t)),t}async function G(e){let t=e.channel;try{await e.joining?.catch(()=>void 0),e.joined&&(e.joined=!1,await y.presenceRemove(t.name,e.conn.id))}catch(n){D(n,{where:`presenceRemove`,channel:t.name,connection:e.conn.id})}if(t.subs.size>0||t.closing)return;t.closing=!0,S.get(t.name)===t&&S.delete(t.name);let n=[t.adapterSub,t.presenceSub].filter(e=>e!==void 0);await Promise.all(n.map(async e=>{try{await(await e)()}catch(e){D(e,{where:`unsubscribe`,channel:t.name})}}))}function K(e){return e.identity.user===void 0?`unauthorized`:`forbidden`}async function q(n,r){let{c:i,n:a}=r;if(n.subs.has(i))return M(n,a,`conflict`,i,`already subscribed`);if(n.subs.size>=g.maxChannelsPerConnection)return M(n,a,`limit`,i,`too many channels on this connection`);let o=O(i);if(o===void 0||!await k(o.rule,`subscribe`,i,o.params,n.identity))return M(n,a,K(n),i);let s=r.hasPresence||r.watch===!0;if(s&&!await k(o.rule,`presence`,i,o.params,n.identity))return M(n,a,K(n),i);if(r.hasPresence&&t(JSON.stringify(r.presence??null))>g.maxStateBytes)return M(n,a,`too_large`,i,`the presence state is too large`);if(n.closed)return;let c=P(i);if(c.subs.size>=g.maxSubscribersPerChannel)return M(n,a,`limit`,i,`the channel is full`);let l={conn:n,channel:c,rule:o.rule,params:o.params,echo:r.echo!==!1,watch:r.watch===!0,joined:!1,version:0,lastSeq:0,lastAt:_(),pumping:!0,gapOk:!1,buffer:[],presenceQueue:s?[]:void 0,joining:void 0,active:!0};n.subs.set(i,l),c.subs.add(l);try{await F(c),s&&(await I(c),Q());let e=await y.history(i,2**53-1,0);if(!l.active)return;let t=e.head,o=r.after,u=!1,d=!1;if(o===void 0?l.lastSeq=t:o>t?(d=!0,l.lastSeq=t):(l.lastSeq=o,u=e.oldest===0?t>o:e.oldest>o+1,l.gapOk=u),j(n,{t:`subbed`,n:a,c:i,head:t,...u?{gap:!0}:{},...d?{reset:!0}:{}}),r.hasPresence&&(l.joining=J(l,r.presence??null),await l.joining),s&&await ee(l),!l.active)return;await z(l,t),l.gapOk=!1}catch(t){l.active&&(H(l),D(t,{where:`subscribe`,channel:i,connection:n.id}),M(n,a,t instanceof e?t.code:`unavailable`,i))}}async function J(t,n){if((await y.presenceList(t.channel.name)).length>=g.maxPresenceMembers)throw new e(`limit`,`the presence set is full`);t.version=1,t.joined=!0,Q();let r={id:t.conn.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:n,v:1};await y.presenceSet(t.channel.name,r,g.presenceTtlMs)}async function ee(e){let t=await y.presenceList(e.channel.name);if(!e.active)return;j(e.conn,{t:`pres`,c:e.channel.name,op:`sync`,m:t});let n=e.presenceQueue??[];e.presenceQueue=void 0;let r=new Map(t.map(e=>[e.id,e.v]));for(let t of n){let n=r.get(t.member.id);(t.op===`leave`?n===void 0:n!==void 0&&n>=t.member.v)||V(e,t)}}async function te(e,n){let r=e.subs.get(n.c);if(r===void 0||!r.joined)return M(e,void 0,`not_subscribed`,n.c);if(t(JSON.stringify(n.s))>g.maxStateBytes)return M(e,void 0,`too_large`,n.c,`the presence state is too large`);if(!e.publishBucket.take(_()))return Y(e,void 0,n.c);r.version+=1;let i={id:e.id,...e.identity.user===void 0?{}:{u:e.identity.user},s:n.s,v:r.version};try{await y.presenceSet(n.c,i,g.presenceTtlMs)}catch(t){D(t,{where:`presenceSet`,channel:n.c,connection:e.id}),M(e,void 0,`unavailable`,n.c)}}function Y(e,t,n){M(e,t,`rate_limited`,n),e.limited.hit(_())&&N(e,1008,`rate limit exceeded`)}async function ne(e,t){let{c:n,n:r}=t,i=O(n);if(i===void 0||!await k(i.rule,`publish`,n,i.params,e.identity))return M(e,r,K(e),n);if(t.d.length>g.maxMessageBytes)return M(e,r,`too_large`,n,`the message is too large`);if(!e.publishBucket.take(_())||S.get(n)?.bucket.take(_())===!1)return Y(e,r,n);try{let a=await y.publish(n,{event:t.e,data:t.d,publisher:e.id,...e.identity.user===void 0?{}:{user:e.identity.user}},{history:i.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...t.k===void 0?{}:{key:t.k}});E.published+=+!a.duplicate,r!==void 0&&j(e,{t:`ack`,n:r,s:a.seq,...a.duplicate?{dup:!0}:{}})}catch(t){D(t,{where:`publish`,channel:n,connection:e.id}),M(e,r,`unavailable`,n)}}async function re(n,r){let{c:i,n:a}=r,o=n.subs.get(i);if(o===void 0&&n.stateless){let e=O(i);e!==void 0&&await k(e.rule,`subscribe`,i,e.params,n.identity)&&(o=e)}if(o===void 0)return M(n,a,K(n)===`unauthorized`&&n.stateless?`unauthorized`:`not_subscribed`,i);let s=o,c=s.rule.handlers?.[r.e];if(c===void 0)return M(n,a,`no_handler`,i);if(n.pending.size>=g.maxPendingRequests)return M(n,a,`limit`,i,`too many requests in flight`);if(!n.publishBucket.take(_()))return Y(n,a,i);let l=new AbortController;n.pending.add(l);let u=Math.min(r.ms??g.requestTimeoutMs,g.requestTimeoutMs),d=setTimeout(()=>l.abort(new e(`timeout`)),u);(async()=>{try{let e=Promise.resolve(c({channel:i,params:s.params,identity:n.identity,event:r.e,data:r.d,signal:l.signal})),o=new Promise((e,t)=>{l.signal.addEventListener(`abort`,()=>t(l.signal.reason),{once:!0})}),u=await Promise.race([e,o])??null,d=JSON.stringify(u);if(d===void 0||t(d)>g.maxMessageBytes)return M(n,a,`too_large`,i,`the reply is too large`);A(n,`{"t":"rep","n":${a},"d":${d}}`)}catch(t){t instanceof e?M(n,a,t.code,i,t.code===`timeout`||t.code===`closed`?void 0:t.message):(D(t,{where:`request`,channel:i,connection:n.id}),M(n,a,`handler_error`,i))}finally{clearTimeout(d),n.pending.delete(l),X(n)}})()}async function ie(t,r){let i=n(r,g.maxMessageBytes);if(!i.ok){M(t,i.n,i.code,void 0,i.why),t.invalid.hit(_())&&N(t,1008,`too many invalid frames`);return}let a=i.frame;try{switch(a.t){case`hello`:return;case`ping`:j(t,{t:`pong`,...a.n===void 0?{}:{n:a.n}});return;case`sub`:return await q(t,a);case`unsub`:{let e=t.subs.get(a.c);if(e===void 0)return M(t,a.n,`not_subscribed`,a.c);H(e);return}case`pub`:return await ne(t,a);case`req`:return await re(t,a);case`pres`:return await te(t,a)}}catch(n){let r=`n`in a?a.n:void 0,i=`c`in a?a.c:void 0;n instanceof e?M(t,r,n.code,i):(D(n,{where:`frame`,connection:t.id,...i===void 0?{}:{channel:i}}),M(t,r,`unavailable`,i))}}async function ae(t){if(!t.running){t.running=!0;try{for(let n=t.queue.shift();n!==void 0&&!t.closed;n=t.queue.shift())try{await ie(t,n)}catch(n){n instanceof e?M(t,void 0,n.code):(D(n,{where:`frame`,connection:t.id}),M(t,void 0,`unavailable`))}}finally{t.running=!1,X(t)}}}function X(e){if(!(e.running||e.queue.length>0||e.pending.size>0))for(let t of e.waiters.splice(0))t()}async function Z(t){if(t.closed)return t.done;t.closed=!0,x.get(t.id)===t&&x.delete(t.id);for(let n of t.pending)n.abort(new e(`closed`));t.pending.clear(),t.queue.length=0;for(let e of t.waiters.splice(0))e();let n=[...t.subs.values()];for(let e of n)e.active=!1,e.channel.subs.delete(e);t.subs.clear(),await Promise.allSettled(n.map(e=>W(e))),t.release?.()}function Q(){if(C!==void 0||w)return;let e=setInterval(()=>void oe(),g.presenceHeartbeatMs);e.unref?.(),C=e}async function oe(){if(!T){T=!0;try{await se()}finally{T=!1}}}async function se(){let e=[],t=/* @__PURE__ */ new Set;for(let n of x.values())for(let r of n.subs.values())r.joined&&e.push({channel:r.channel.name,id:n.id}),(r.joined||r.watch)&&t.add(r.channel.name);if(e.length===0&&t.size===0)clearInterval(C),C=void 0;else try{let n=await y.presenceTouch(e,g.presenceTtlMs);for(let e of n){let t=x.get(e.id)?.subs.get(e.channel);t!==void 0&&t.joined&&await y.presenceSet(e.channel,{id:e.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:null,v:t.version},g.presenceTtlMs)}for(let e of t)await y.presenceReap(e)}catch(e){D(e,{where:`presenceHeartbeat`})}}for(let[e,t]of Object.entries(h.channels??{}))b.push(u(e,t));let $={limits:g,adapter:y,report:D,match:O,allowed:k,channel(e,t){return b.push(u(e,t)),$},connect(t,n={},r={}){if(w)throw new e(`closed`,`the hub is closed`);let i=()=>void 0,a=new Promise(e=>{i=e}),c=r.actingFor===void 0?void 0:x.get(r.actingFor),l=c!==void 0&&n.user!==void 0&&c.identity.user===n.user,u={id:l?r.actingFor:f(),identity:n,transport:t,subs:/* @__PURE__ */ new Map,queue:[],publishBucket:r.publishBucket??new o(g.publishesPerSecond,g.publishBurst,_()),stateless:r.stateless===!0,waiters:[],invalid:new s(10,6e4),limited:new s(50,1e4),pending:/* @__PURE__ */ new Set,running:!1,closed:!1,release:i,done:a};return l||x.set(u.id,u),j(u,{t:`ready`,v:1,id:u.id,hb:g.heartbeatMs,max:g.maxMessageBytes}),{id:u.id,identity:n,done:a,receive(e){u.closed||(u.queue.length>=g.maxQueuedFrames?N(u,1013,`too many frames waiting`):(u.queue.push(e),ae(u)))},closed(){Z(u)},settled(){return new Promise(e=>{u.waiters.push(e),X(u)})}}},async publish(n,o,s,c={}){if(!r(n))throw new e(`bad_channel`,`"${n}" is not a valid channel name`);if(!i(o))throw new e(`bad_frame`,`"${o}" is not a valid event name`);if(c.key!==void 0&&!a(c.key))throw new e(`bad_frame`,`the idempotency key is not valid`);let l;try{l=JSON.stringify(s===void 0?null:s)}catch(e){throw TypeError(`the data is not JSON`,{cause:e})}if(l===void 0)throw TypeError(`the data is not JSON`);if(t(l)>g.maxMessageBytes)throw new e(`too_large`,`the message is too large`);let u=O(n),d=await y.publish(n,{event:o,data:l},{history:u?.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...c.key===void 0?{}:{key:c.key}});return E.published+=+!d.duplicate,d},async setMemberState(n,r,i,a){if(n.user===void 0)throw new e(`unauthorized`,`presence changes over HTTP need a signed-in user`);let o=O(r);if(o===void 0||!await k(o.rule,`presence`,r,o.params,n))throw new e(`forbidden`);if(t(JSON.stringify(a))>g.maxStateBytes)throw new e(`too_large`,`the presence state is too large`);let s=(await y.presenceList(r)).find(e=>e.id===i);if(s===void 0||s.u!==n.user)throw new e(`forbidden`,`that connection is not yours`);await y.presenceSet(r,{id:s.id,u:s.u,s:a,v:s.v+1},g.presenceTtlMs)},revoke(e){if(e.user===void 0&&e.connection===void 0&&e.channel===void 0)throw TypeError(`revoke needs a user, a connection or a channel.`);let t=0;for(let n of[...x.values()])if((e.connection===void 0||n.id===e.connection)&&(e.user===void 0||n.identity.user===e.user))for(let r of[...n.subs.values()])(e.channel===void 0||r.channel.name===e.channel)&&(H(r,`revoked`),t+=1);return t},stats(){let e=0;for(let t of x.values())e+=t.subs.size;return{connections:x.size,channels:S.size,subscriptions:e,...E}},async close(){if(w)return;w=!0,C!==void 0&&clearInterval(C),C=void 0;let e=[...x.values()];for(let t of e)N(t,1001,`going away`);await Promise.all(e.map(e=>e.done)),await Promise.allSettled([...U]),v&&await y.close()}};return $}export{h as createHubCore};
|
|
1
|
+
import{RealtimeError as e}from"../errors.js";import{byteLength as t,decodeClientFrame as n,isChannelName as r,isEventName as i,isIdempotencyKey as a}from"../protocol.js";import{TokenBucket as o,WindowCounter as s,resolveLimits as c}from"./limits.js";import{memoryAdapter as l}from"../memory.js";import{compileRule as u,matchRule as d}from"./rules.js";function f(){let e=crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));return btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function p(e){return JSON.stringify(e)}function m(e,t,n){return`{"t":"msg","c":${JSON.stringify(e)},"s":${t.seq},"e":${JSON.stringify(t.event)},"d":${t.data}`+(t.publisher===void 0?``:`,"p":${JSON.stringify(t.publisher)}`)+(t.user===void 0?``:`,"u":${JSON.stringify(t.user)}`)+(n?`,"r":1}`:`}`)}function h(h={}){let g=c(h.limits),_=h.now??Date.now,v=h.adapter===void 0,y=h.adapter??l({now:_,onError:(e,t)=>D(e,{where:`memory.${t.where}`,channel:t.channel})}),b=[],x=/* @__PURE__ */ new Map,S=/* @__PURE__ */ new Map,C,w=!1,T=!1,E={published:0,delivered:0,refused:0,slowConsumers:0},D=(e,t)=>{if(h.onError!==void 0)try{h.onError(e,t)}catch{}else console.error(`[realtime] ${t.where}`,e)};function O(e){for(let t of b){let n=d(t,e);if(n!==void 0)return{rule:t.rule,params:n}}}async function k(t,n,r,i,a){let o=t[n];if(o===void 0||o===!1)return!1;if(o===!0)return!0;try{return await o({channel:r,params:i,identity:a,op:n})===!0}catch(t){throw D(t,{where:`rule.${n}`,channel:r}),new e(`unavailable`,void 0)}}function A(e,t){if(!e.closed){try{if((e.transport.buffered?.()??0)>g.maxBufferedBytes){E.slowConsumers+=1,P(e,1013,``);return}if(e.transport.send(t))return}catch(t){D(t,{where:`transport.send`,connection:e.id}),j(e,`transport.close`,()=>e.transport.close(1011,``))}Z(e)}}function j(e,t,n){try{n()}catch(n){D(n,{where:t,connection:e.id})}}function M(e,t){A(e,p(t))}function N(e,t,n,r,i){E.refused+=1,M(e,{t:`err`,...t===void 0?{}:{n:t},...r===void 0?{}:{c:r},code:n,...i===void 0||i===``?{}:{msg:i}})}function P(e,t,n){e.closed||(j(e,`transport.send`,()=>e.transport.send(p({t:`bye`,code:t,why:n}))),j(e,`transport.close`,()=>e.transport.close(t,n)),Z(e))}function F(e){let t=S.get(e);return(t===void 0||t.closing)&&(t={name:e,subs:/* @__PURE__ */ new Set,bucket:new o(g.channelPublishesPerSecond,g.channelPublishesPerSecond,_()),adapterSub:void 0,presenceSub:void 0,closing:!1,frame:void 0},S.set(e,t)),t}async function I(e,t,n){if(e[t]===void 0){let r=n();e[t]=r,r.catch(()=>{e[t]=void 0})}await e[t]}function L(e,t){for(let n of e.subs)n.active&&(n.pumping?n.buffer.length>=1e4?H(n,`gap`):n.buffer.push(t):t.seq===n.lastSeq+1?R(n,t,!1):t.seq>n.lastSeq+1?(n.buffer.push(t),z(n,void 0)):t.at>n.lastAt&&H(n,`reset`))}function R(e,t,n){if(e.lastSeq=t.seq,e.lastAt=Math.max(e.lastAt,t.at),!e.echo&&t.publisher===e.conn.id)return;if(E.delivered+=1,n){A(e.conn,m(e.channel.name,t,!0));return}let r=e.channel.frame,i=r!==void 0&&r.seq===t.seq?r.text:m(e.channel.name,t,!1);(r===void 0||r.seq!==t.seq)&&(e.channel.frame={seq:t.seq,text:i}),A(e.conn,i)}async function z(e,t){if(!(e.pumping&&t===void 0)){e.pumping=!0;try{for(;e.active;){e.buffer=e.buffer.filter(t=>t.seq>e.lastSeq);let n=e.buffer[0];if(n!==void 0&&n.seq===e.lastSeq+1){R(e,n,!1),e.buffer.shift();continue}let r=n===void 0?t??0:n.seq-1;if(e.lastSeq>=r)break;let i=await y.history(e.channel.name,e.lastSeq,Math.min(500,r-e.lastSeq));if(!e.active)return;let a=i.messages[0];if(a===void 0||a.seq!==e.lastSeq+1){if(!e.gapOk){H(e,`gap`);return}e.lastSeq=(a?.seq??r+1)-1}else for(let t of i.messages){if(t.seq>r)break;R(e,t,!0)}}}catch(t){D(t,{where:`pump`,channel:e.channel.name,connection:e.conn.id}),e.active&&H(e,`unavailable`)}finally{e.pumping=!1}e.active&&e.buffer.length>0&&z(e,void 0)}}function B(e,t){for(let n of e.subs)n.joined&&t.op!==`leave`&&t.member.id===n.conn.id&&t.member.v>n.version&&(n.version=t.member.v,n.state=t.member.s),n.active&&(n.joined||n.watch)&&(n.presenceQueue===void 0?V(n,t):n.presenceQueue.push(t))}function V(e,t){M(e.conn,{t:`pres`,c:t.channel,op:t.op,m:t.member})}function H(e,t){e.active&&(e.active=!1,e.conn.subs.delete(e.channel.name),e.channel.subs.delete(e),t!==void 0&&!e.conn.closed&&M(e.conn,{t:`unsubbed`,c:e.channel.name,...t===``?{}:{why:t}}),W(e))}let U=/* @__PURE__ */ new Set;function W(e){let t=G(e);return U.add(t),t.finally(()=>U.delete(t)),t}async function G(e){let t=e.channel;try{await e.joining?.catch(()=>void 0),e.joined&&(e.joined=!1,await y.presenceRemove(t.name,e.conn.id))}catch(n){D(n,{where:`presenceRemove`,channel:t.name,connection:e.conn.id})}if(t.subs.size>0||t.closing)return;t.closing=!0,S.get(t.name)===t&&S.delete(t.name);let n=[t.adapterSub,t.presenceSub].filter(e=>e!==void 0);await Promise.all(n.map(async e=>{try{await(await e)()}catch(e){D(e,{where:`unsubscribe`,channel:t.name})}}))}function K(e){return e.identity.user===void 0?`unauthorized`:`forbidden`}async function q(n,r){let{c:i,n:a}=r;if(n.subs.has(i))return N(n,a,`conflict`,i,void 0);if(n.subs.size>=g.maxChannelsPerConnection)return N(n,a,`limit`,i,void 0);let o=O(i);if(o===void 0||!await k(o.rule,`subscribe`,i,o.params,n.identity))return N(n,a,K(n),i);let s=r.hasPresence||r.watch===!0;if(s&&!await k(o.rule,`presence`,i,o.params,n.identity))return N(n,a,K(n),i);if(r.hasPresence&&t(JSON.stringify(r.presence??null))>g.maxStateBytes)return N(n,a,`too_large`,i,void 0);if(n.closed)return;let c=F(i);if(c.subs.size>=g.maxSubscribersPerChannel)return N(n,a,`limit`,i,void 0);let l={conn:n,channel:c,rule:o.rule,params:o.params,echo:r.echo!==!1,watch:r.watch===!0,joined:!1,version:0,state:null,lastSeq:0,lastAt:_(),pumping:!0,gapOk:!1,buffer:[],presenceQueue:s?[]:void 0,joining:void 0,active:!0};n.subs.set(i,l),c.subs.add(l);try{await I(c,`adapterSub`,()=>y.subscribe(c.name,e=>L(c,e))),s&&(await I(c,`presenceSub`,()=>y.presenceSubscribe(c.name,e=>B(c,e))),Q());let e=await y.history(i,2**53-1,0);if(!l.active)return;let t=e.head,o=r.after,u=!1,d=!1;if(o===void 0?l.lastSeq=t:o>t?(d=!0,l.lastSeq=t):(l.lastSeq=o,u=e.oldest===0?t>o:e.oldest>o+1,l.gapOk=u),M(n,{t:`subbed`,n:a,c:i,head:t,...u?{gap:!0}:{},...d?{reset:!0}:{}}),r.hasPresence&&(l.joining=ee(l,r.presence??null),await l.joining),s&&await te(l),!l.active)return;await z(l,t),l.gapOk=!1}catch(t){l.active&&(H(l),D(t,{where:`subscribe`,channel:i,connection:n.id}),N(n,a,t instanceof e?t.code:`unavailable`,i))}}async function ee(t,n){if((await y.presenceList(t.channel.name)).length>=g.maxPresenceMembers)throw new e(`limit`,void 0);t.version=1,t.state=n,t.joined=!0,Q();let r={id:t.conn.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:n,v:1};await y.presenceSet(t.channel.name,r,g.presenceTtlMs)}async function te(e){let t=await y.presenceList(e.channel.name);if(!e.active)return;M(e.conn,{t:`pres`,c:e.channel.name,op:`sync`,m:t});let n=e.presenceQueue??[];e.presenceQueue=void 0;let r=new Map(t.map(e=>[e.id,e.v]));for(let t of n){let n=r.get(t.member.id);(t.op===`leave`?n===void 0:n!==void 0&&n>=t.member.v)||V(e,t)}}async function ne(e,n){let r=e.subs.get(n.c);if(r===void 0||!r.joined)return N(e,void 0,`not_subscribed`,n.c);if(t(JSON.stringify(n.s))>g.maxStateBytes)return N(e,void 0,`too_large`,n.c,void 0);if(!e.publishBucket.take(_()))return J(e,void 0,n.c);r.version+=1,r.state=n.s;let i={id:e.id,...e.identity.user===void 0?{}:{u:e.identity.user},s:n.s,v:r.version};try{await y.presenceSet(n.c,i,g.presenceTtlMs)}catch(t){D(t,{where:`presenceSet`,channel:n.c,connection:e.id}),N(e,void 0,`unavailable`,n.c)}}function J(e,t,n){N(e,t,`rate_limited`,n),e.limited.hit(_())&&P(e,1008,``)}async function re(e,t){let{c:n,n:r}=t,i=O(n);if(i===void 0||!await k(i.rule,`publish`,n,i.params,e.identity))return N(e,r,K(e),n);if(t.d.length>g.maxMessageBytes)return N(e,r,`too_large`,n,void 0);if(!e.publishBucket.take(_())||S.get(n)?.bucket.take(_())===!1)return J(e,r,n);try{let a=await y.publish(n,{event:t.e,data:t.d,publisher:e.id,...e.identity.user===void 0?{}:{user:e.identity.user}},{history:i.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...t.k===void 0?{}:{key:t.k}});E.published+=+!a.duplicate,r!==void 0&&M(e,{t:`ack`,n:r,s:a.seq,...a.duplicate?{dup:!0}:{}})}catch(t){D(t,{where:`publish`,channel:n,connection:e.id}),N(e,r,`unavailable`,n)}}async function ie(n,r){let{c:i,n:a}=r,o=n.subs.get(i);if(o===void 0&&n.stateless){let e=O(i);e!==void 0&&await k(e.rule,`subscribe`,i,e.params,n.identity)&&(o=e)}if(o===void 0)return N(n,a,K(n)===`unauthorized`&&n.stateless?`unauthorized`:`not_subscribed`,i);let s=o,c=s.rule.handlers?.[r.e];if(c===void 0)return N(n,a,`no_handler`,i);if(n.pending.size>=g.maxPendingRequests)return N(n,a,`limit`,i,void 0);if(!n.publishBucket.take(_()))return J(n,a,i);let l=new AbortController;n.pending.add(l);let u=Math.min(r.ms??g.requestTimeoutMs,g.requestTimeoutMs),d=setTimeout(()=>l.abort(new e(`timeout`)),u);(async()=>{try{let e=Promise.resolve(c({channel:i,params:s.params,identity:n.identity,event:r.e,data:r.d,signal:l.signal})),o=new Promise((e,t)=>{l.signal.addEventListener(`abort`,()=>t(l.signal.reason),{once:!0})}),u=await Promise.race([e,o])??null,d=JSON.stringify(u);if(d===void 0||t(d)>g.maxMessageBytes)return N(n,a,`too_large`,i,void 0);A(n,`{"t":"rep","n":${a},"d":${d}}`)}catch(t){t instanceof e?N(n,a,t.code,i,t.code===`timeout`||t.code===`closed`?void 0:t.message):(D(t,{where:`request`,channel:i,connection:n.id}),N(n,a,`handler_error`,i))}finally{clearTimeout(d),n.pending.delete(l),X(n)}})()}function Y(e,t,n,r){N(e,t,n,void 0,r),e.invalid.hit(_())&&P(e,1008,``)}async function ae(t,r){let i=n(r,g.maxMessageBytes);if(!i.ok){Y(t,i.n,i.code,i.why);return}let a=i.frame,o=`n`in a?a.n:void 0;if((a.t===`sub`||a.t===`unsub`)&&!t.controlBucket.take(_())&&a.t===`sub`)return J(t,o,a.c);try{switch(a.t){case`hello`:return;case`ping`:M(t,{t:`pong`,...a.n===void 0?{}:{n:a.n}});return;case`sub`:return await q(t,a);case`unsub`:{let e=t.subs.get(a.c);if(e===void 0)return N(t,a.n,`not_subscribed`,a.c);H(e);return}case`pub`:return await re(t,a);case`req`:return await ie(t,a);case`pres`:return await ne(t,a)}}catch(n){let r=`c`in a?a.c:void 0;n instanceof e?N(t,o,n.code,r):(D(n,{where:`frame`,connection:t.id,...r===void 0?{}:{channel:r}}),N(t,o,`unavailable`,r))}}async function oe(t){if(!t.running){t.running=!0;try{for(let n=t.queue.shift();n!==void 0&&!t.closed;n=t.queue.shift())try{await ae(t,n)}catch(n){n instanceof e?N(t,void 0,n.code):(D(n,{where:`frame`,connection:t.id}),N(t,void 0,`unavailable`))}}finally{t.running=!1,X(t)}}}function X(e){if(!(e.running||e.queue.length>0||e.pending.size>0))for(let t of e.waiters.splice(0))t()}async function Z(t){if(t.closed)return t.done;t.closed=!0,x.get(t.id)===t&&x.delete(t.id);for(let n of t.pending)n.abort(new e(`closed`));t.pending.clear(),t.queue.length=0;for(let e of t.waiters.splice(0))e();let n=[...t.subs.values()];for(let e of n)e.active=!1,e.channel.subs.delete(e);t.subs.clear(),await Promise.allSettled(n.map(e=>W(e))),t.release?.()}function Q(){if(C!==void 0||w)return;let e=setInterval(()=>void se(),g.presenceHeartbeatMs);e.unref?.(),C=e}async function se(){if(!T){T=!0;try{await ce()}finally{T=!1}}}async function ce(){let e=[],t=/* @__PURE__ */ new Set;for(let n of x.values())for(let r of n.subs.values())r.joined&&e.push({channel:r.channel.name,id:n.id}),(r.joined||r.watch)&&t.add(r.channel.name);if(e.length===0&&t.size===0)clearInterval(C),C=void 0;else try{let n=await y.presenceTouch(e,g.presenceTtlMs);for(let e of n){let t=x.get(e.id)?.subs.get(e.channel);t!==void 0&&t.joined&&await y.presenceSet(e.channel,{id:e.id,...t.conn.identity.user===void 0?{}:{u:t.conn.identity.user},s:t.state,v:t.version},g.presenceTtlMs)}for(let e of t)await y.presenceReap(e)}catch(e){D(e,{where:`presenceHeartbeat`})}}for(let[e,t]of Object.entries(h.channels??{}))b.push(u(e,t));let $={limits:g,adapter:y,report:D,match:O,allowed:k,channel(e,t){return b.push(u(e,t)),$},connect(n,r={},i={}){if(w)throw new e(`closed`,void 0);let a=()=>void 0,c=new Promise(e=>{a=e}),l=i.actingFor===void 0?void 0:x.get(i.actingFor),u=l!==void 0&&r.user!==void 0&&l.identity.user===r.user,d={id:u?i.actingFor:f(),identity:r,transport:n,subs:/* @__PURE__ */ new Map,queue:[],publishBucket:i.publishBucket??new o(g.publishesPerSecond,g.publishBurst,_()),controlBucket:new o(g.publishesPerSecond,Math.max(g.publishBurst,g.maxChannelsPerConnection),_()),stateless:i.stateless===!0,waiters:[],invalid:new s(10,6e4),limited:new s(50,1e4),pending:/* @__PURE__ */ new Set,running:!1,closed:!1,release:a,done:c};return u||x.set(d.id,d),M(d,{t:`ready`,v:1,id:d.id,hb:g.heartbeatMs,max:g.maxMessageBytes}),{id:d.id,identity:r,done:c,receive(e){d.closed||(t(e)>g.maxMessageBytes?Y(d,void 0,`too_large`):d.queue.length>=g.maxQueuedFrames?P(d,1013,``):(d.queue.push(e),oe(d)))},closed(){Z(d)},settled(){return new Promise(e=>{d.waiters.push(e),X(d)})}}},async publish(n,o,s,c={}){if(!r(n))throw new e(`bad_channel`,void 0);if(!i(o)||c.key!==void 0&&!a(c.key))throw new e(`bad_frame`,void 0);let l;try{l=JSON.stringify(s===void 0?null:s)}catch(e){throw TypeError(`REALTIME_DATA`,{cause:e})}if(l===void 0)throw TypeError(`REALTIME_DATA`);if(t(l)>g.maxMessageBytes)throw new e(`too_large`,void 0);let u=O(n),d=await y.publish(n,{event:o,data:l},{history:u?.rule.history??g.history,historyTtlMs:g.historyTtlMs,idempotencyTtlMs:g.idempotencyTtlMs,...c.key===void 0?{}:{key:c.key}});return E.published+=+!d.duplicate,d},async setMemberState(n,r,i,a){if(n.user===void 0)throw new e(`unauthorized`,void 0);let o=O(r);if(o===void 0||!await k(o.rule,`presence`,r,o.params,n))throw new e(`forbidden`);if(t(JSON.stringify(a))>g.maxStateBytes)throw new e(`too_large`,void 0);let s=(await y.presenceList(r)).find(e=>e.id===i);if(s===void 0||s.u!==n.user)throw new e(`forbidden`,void 0);let c=x.get(i)?.subs.get(r),l=Math.max(s.v,c?.version??0)+1;c?.joined===!0&&(c.version=l,c.state=a),await y.presenceSet(r,{id:s.id,u:s.u,s:a,v:l},g.presenceTtlMs)},revoke(e){if(e.user===void 0&&e.connection===void 0&&e.channel===void 0)throw TypeError(`REALTIME_REVOKE`);let t=0;for(let n of[...x.values()])if((e.connection===void 0||n.id===e.connection)&&(e.user===void 0||n.identity.user===e.user))for(let r of[...n.subs.values()])(e.channel===void 0||r.channel.name===e.channel)&&(H(r,`revoked`),t+=1);return t},stats(){let e=0;for(let t of x.values())e+=t.subs.size;return{connections:x.size,channels:S.size,subscriptions:e,...E}},async close(){if(w)return;w=!0,C!==void 0&&clearInterval(C),C=void 0;let e=[...x.values()];for(let t of e)P(t,1001,``);await Promise.all(e.map(e=>e.done)),await Promise.allSettled([...U]),v&&await y.close()}};return $}export{h as createHubCore};
|
|
2
2
|
//# sourceMappingURL=hub.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub.js","names":["randomId","bytes","text","byte","encodeFrame","frame","messageFrame","channel","message","replayed","createHubCore","limits","resolveLimits","options","now","ownsAdapter","adapter","memoryAdapter","rules","conns","chans","heartbeat","closing","beating","counters","report","error","context","match","compiled","params","matchRule","allowed","permission","rule","op","RealtimeError","send","conn","drop","closeConnection","sendFrame","fail","n","reason","code","channelState","chan","name","TokenBucket","ensureAdapterSub","onMessage","ensurePresenceSub","onPresence","event","sub","removeSub","deliver","pump","cached","upTo","next","target","page","first","sendPresence","why","releaseSub","releasing","running","release","stops","item","stop","identityDenied","subscribe","found","wantsPresence","byteLength","startHeartbeat","probe","head","after","gap","reset","joinPresence","syncPresence","member","state","members","queued","known","version","setPresence","limit","publishFrame","result","request","handler","controller","timeout","timer","run","aborted","reject","reply","settle","handle","decoded","decodeClientFrame","wake","subs","beat","beatOnce","refs","watched","missing","ref","pattern","compileRule","core","done","resolve","owner","connectOptions","acting","identity","WindowCounter","isChannelName","isEventName","publishOptions","isIdempotencyKey","data","connectionId","count","subscriptions","all"],"sources":["../../../src/server/hub.ts"],"sourcesContent":["import type { Adapter, PublishResult, Unsubscribe } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport { memoryAdapter } from \"../memory\";\nimport {\n byteLength,\n type ClientFrame,\n decodeClientFrame,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n PROTOCOL_VERSION,\n type PresenceEvent,\n type StoredMessage,\n} from \"../protocol\";\nimport { type HubLimits, resolveLimits, TokenBucket, WindowCounter } from \"./limits\";\nimport {\n type ChannelRule,\n type CompiledRule,\n compileRule,\n type Identity,\n matchRule,\n type RuleContext,\n} from \"./rules\";\n\n/**\n * What a hub needs from a connection's transport. A WebSocket, an SSE stream and an in-memory test client all fit.\n * @since 1.0.0\n */\nexport interface Transport {\n /** Sends one frame (text). False when the transport is closed. */\n send(frame: string): boolean;\n /** Closes the transport with a WebSocket close code and a reason of at most 123 bytes. */\n close(code: number, reason: string): void;\n /** Bytes accepted by `send` that the peer has not yet taken. When present, a connection over the limit is closed. */\n buffered?(): number;\n}\n\n/** Where an error reported to `onError` came from. @since 1.0.0 */\nexport interface ErrorContext {\n readonly where: string;\n readonly channel?: string;\n readonly connection?: string;\n}\n\n/** Options of {@link createHub}. @since 1.0.0 */\nexport interface HubOptions {\n /** Storage and fan-out. Default: a fresh `memoryAdapter()`, closed with the hub. */\n readonly adapter?: Adapter;\n /** Channel rules by pattern, in order. The first matching pattern wins. */\n readonly channels?: Readonly<Record<string, ChannelRule>>;\n /** Limits (see {@link HubLimits}). */\n readonly limits?: Partial<HubLimits>;\n /** Receives failures the hub handled (an adapter outage, a rule predicate that threw). Default: `console.error`. */\n readonly onError?: (error: unknown, context: ErrorContext) => void;\n /** The clock, in epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n}\n\n/** Counters of a running hub. @since 1.0.0 */\nexport interface HubStats {\n readonly connections: number;\n readonly channels: number;\n readonly subscriptions: number;\n readonly published: number;\n readonly delivered: number;\n readonly refused: number;\n readonly slowConsumers: number;\n}\n\n/** One connection, as the transport glue drives it. @since 1.0.0 */\nexport interface HubConnection {\n readonly id: string;\n readonly identity: Identity;\n /** Hands the hub one text frame from the peer. Frames are handled in order. */\n receive(text: string): void;\n /** Tells the hub the transport closed. Safe to call twice. */\n closed(): void;\n /** Resolves once every frame received so far was handled and every request answered. */\n settled(): Promise<void>;\n /** Resolves when the hub finished releasing what the connection held. */\n readonly done: Promise<void>;\n}\n\n/** How a connection differs from a long-lived one. @since 1.0.0 */\nexport interface ConnectOptions {\n /**\n * A connection that exists for one HTTP request: a request is answered for any identity the channel's `subscribe` rule\n * accepts (there is no subscription to look up), and nothing is kept after it closes.\n */\n readonly stateless?: boolean;\n /** Replaces the connection's own publish bucket, so a caller can share one across requests. */\n readonly publishBucket?: TokenBucket;\n /**\n * The id of a long-lived connection of the same user this request acts for (SSE clients post under their stream's id, so\n * `echo:false` and `publisher` work). Honored only when that connection is open on this server and has the same user.\n */\n readonly actingFor?: string;\n}\n\n/** Who to remove subscriptions of. At least one field. @since 1.0.0 */\nexport interface RevokeTarget {\n readonly user?: string;\n readonly connection?: string;\n readonly channel?: string;\n}\n\n/** The server object. @since 1.0.0 */\nexport interface HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: ChannelRule): HubCore;\n /** Starts handling a connection. Sends `ready`. */\n connect(transport: Transport, identity?: Identity, options?: ConnectOptions): HubConnection;\n /** Publishes from the server (no rule applies). */\n publish(\n channel: string,\n event: string,\n data: unknown,\n options?: { readonly key?: string },\n ): Promise<PublishResult>;\n /** Removes matching subscriptions; each subscriber gets `unsubbed why:\"revoked\"`. Returns how many were removed. */\n revoke(target: RevokeTarget): number;\n /** Counters. */\n stats(): HubStats;\n /** Closes every connection with 1001, stops timers, removes presence, settles requests. */\n close(): Promise<void>;\n /** @internal Changes the state of a presence member from outside its connection (SSE clients). */\n setMemberState(\n identity: Identity,\n channel: string,\n connectionId: string,\n state: Json,\n ): Promise<void>;\n /** @internal The resolved limits. */\n readonly limits: HubLimits;\n /** @internal The adapter. */\n readonly adapter: Adapter;\n /** @internal Reports an error. */\n report(error: unknown, context: ErrorContext): void;\n /** @internal Matches a channel against the rules. */\n match(\n channel: string,\n ): { readonly rule: ChannelRule; readonly params: Record<string, string> } | undefined;\n /** @internal Evaluates a rule permission for an identity. */\n allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean>;\n}\n\ninterface SubState {\n readonly conn: ConnState;\n readonly channel: ChanState;\n readonly rule: ChannelRule;\n readonly params: Record<string, string>;\n readonly echo: boolean;\n readonly watch: boolean;\n joined: boolean;\n version: number;\n lastSeq: number;\n /** The `at` of the last message delivered: a message with an old seq but a newer `at` means the numbering restarted. */\n lastAt: number;\n /** While true, live messages wait in `buffer` and `pump` delivers in order. */\n pumping: boolean;\n gapOk: boolean;\n buffer: StoredMessage[];\n presenceQueue: PresenceEvent[] | undefined;\n /** The join in progress, so that releasing the subscription waits for it and never leaves a member behind. */\n joining: Promise<void> | undefined;\n active: boolean;\n}\n\ninterface ChanState {\n readonly name: string;\n readonly subs: Set<SubState>;\n readonly bucket: TokenBucket;\n adapterSub: Promise<Unsubscribe> | undefined;\n presenceSub: Promise<Unsubscribe> | undefined;\n closing: boolean;\n /** Cache of the encoded frame of the message being fanned out. */\n frame: { readonly seq: number; readonly text: string } | undefined;\n}\n\ninterface ConnState {\n readonly id: string;\n readonly identity: Identity;\n readonly transport: Transport;\n readonly subs: Map<string, SubState>;\n readonly queue: string[];\n readonly publishBucket: TokenBucket;\n readonly invalid: WindowCounter;\n readonly limited: WindowCounter;\n readonly pending: Set<AbortController>;\n readonly stateless: boolean;\n readonly waiters: (() => void)[];\n running: boolean;\n closed: boolean;\n release: (() => void) | undefined;\n readonly done: Promise<void>;\n}\n\nconst REPLAY_PAGE = 500;\nconst MAX_PUMP_BUFFER = 10_000;\nconst ID_BYTES = 16;\n\n/** A random id: 128 bits, base64url. */\nfunction randomId(): string {\n const bytes = new Uint8Array(ID_BYTES);\n crypto.getRandomValues(bytes);\n let text = \"\";\n for (const byte of bytes) text += String.fromCharCode(byte);\n return btoa(text).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction encodeFrame(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\nfunction messageFrame(channel: string, message: StoredMessage, replayed: boolean): string {\n return (\n `{\"t\":\"msg\",\"c\":${JSON.stringify(channel)},\"s\":${message.seq},\"e\":${JSON.stringify(message.event)},\"d\":${message.data}` +\n (message.publisher === undefined ? \"\" : `,\"p\":${JSON.stringify(message.publisher)}`) +\n (message.user === undefined ? \"\" : `,\"u\":${JSON.stringify(message.user)}`) +\n (replayed ? ',\"r\":1}' : \"}\")\n );\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to\n * one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: { \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },\n * });\n * const sent: string[] = [];\n * const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });\n * connection.receive('{\"t\":\"sub\",\"n\":1,\"c\":\"chat:lobby\"}');\n * @since 1.0.0\n */\nexport function createHubCore(options: HubOptions = {}): HubCore {\n const limits = resolveLimits(options.limits);\n const now = options.now ?? Date.now;\n const ownsAdapter = options.adapter === undefined;\n const adapter = options.adapter ?? memoryAdapter({ now });\n const rules: CompiledRule[] = [];\n const conns = new Map<string, ConnState>();\n const chans = new Map<string, ChanState>();\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let closing = false;\n let beating = false;\n const counters = { published: 0, delivered: 0, refused: 0, slowConsumers: 0 };\n\n const report = (error: unknown, context: ErrorContext): void => {\n if (options.onError !== undefined) {\n try {\n options.onError(error, context);\n } catch {\n // A failing reporter must not break the hub.\n }\n } else {\n console.error(`[realtime] ${context.where}`, error);\n }\n };\n\n function match(\n channel: string,\n ): { rule: ChannelRule; params: Record<string, string> } | undefined {\n for (const compiled of rules) {\n const params = matchRule(compiled, channel);\n if (params !== undefined) return { rule: compiled.rule, params };\n }\n return undefined;\n }\n\n async function allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean> {\n const permission = rule[op];\n if (permission === undefined || permission === false) return false;\n if (permission === true) return true;\n try {\n return (await permission({ channel, params, identity, op })) === true;\n } catch (error) {\n report(error, { where: `rule.${op}`, channel });\n throw new RealtimeError(\"unavailable\", \"the channel rule could not be evaluated\");\n }\n }\n\n function send(conn: ConnState, text: string): void {\n if (conn.closed) return;\n try {\n const buffered = conn.transport.buffered?.() ?? 0;\n if (buffered > limits.maxBufferedBytes) {\n counters.slowConsumers += 1;\n drop(conn, 1013, \"slow consumer\");\n return;\n }\n if (conn.transport.send(text)) return;\n } catch (error) {\n // A transport that throws is a broken socket: this connection goes, the others are not affected (guarantee G13).\n report(error, { where: \"transport.send\", connection: conn.id });\n }\n void closeConnection(conn);\n }\n\n function sendFrame(conn: ConnState, frame: Record<string, unknown>): void {\n send(conn, encodeFrame(frame));\n }\n\n function fail(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n channel?: string,\n message?: string,\n ): void {\n counters.refused += 1;\n sendFrame(conn, {\n t: \"err\",\n ...(n === undefined ? {} : { n }),\n ...(channel === undefined ? {} : { c: channel }),\n code,\n ...(message === undefined ? {} : { msg: message }),\n });\n }\n\n /** Sends a final frame, closes the transport and releases the connection. */\n function drop(conn: ConnState, code: number, reason: string): void {\n if (conn.closed) return;\n try {\n conn.transport.send(encodeFrame({ t: \"bye\", code, why: reason }));\n } catch {\n // The transport may already be gone.\n }\n try {\n conn.transport.close(code, reason);\n } catch {\n // See above.\n }\n void closeConnection(conn);\n }\n\n // --- channels ------------------------------------------------------------------------------------------------------\n\n function channelState(name: string): ChanState {\n let chan = chans.get(name);\n if (chan === undefined || chan.closing) {\n chan = {\n name,\n subs: new Set(),\n bucket: new TokenBucket(\n limits.channelPublishesPerSecond,\n limits.channelPublishesPerSecond,\n now(),\n ),\n adapterSub: undefined,\n presenceSub: undefined,\n closing: false,\n frame: undefined,\n };\n chans.set(name, chan);\n }\n return chan;\n }\n\n async function ensureAdapterSub(chan: ChanState): Promise<void> {\n if (chan.adapterSub === undefined) {\n chan.adapterSub = adapter.subscribe(chan.name, (message) => onMessage(chan, message));\n chan.adapterSub.catch(() => {\n chan.adapterSub = undefined;\n });\n }\n await chan.adapterSub;\n }\n\n async function ensurePresenceSub(chan: ChanState): Promise<void> {\n if (chan.presenceSub === undefined) {\n chan.presenceSub = adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event));\n chan.presenceSub.catch(() => {\n chan.presenceSub = undefined;\n });\n }\n await chan.presenceSub;\n }\n\n function onMessage(chan: ChanState, message: StoredMessage): void {\n for (const sub of chan.subs) {\n if (!sub.active) continue;\n if (sub.pumping) {\n // A consumer so far behind that it cannot catch up is resubscribed from its last seq instead of holding memory.\n if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, \"gap\");\n else sub.buffer.push(message);\n continue;\n }\n if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);\n else if (message.seq > sub.lastSeq + 1) {\n sub.buffer.push(message);\n void pump(sub, undefined);\n } else if (message.at > sub.lastAt) removeSub(sub, \"reset\");\n // Any other seq at or below the cursor was already delivered: dropped (guarantee G2).\n }\n }\n\n function deliver(sub: SubState, message: StoredMessage, replayed: boolean): void {\n sub.lastSeq = message.seq;\n sub.lastAt = Math.max(sub.lastAt, message.at);\n if (!sub.echo && message.publisher === sub.conn.id) return;\n counters.delivered += 1;\n if (replayed) {\n send(sub.conn, messageFrame(sub.channel.name, message, true));\n return;\n }\n const cached = sub.channel.frame;\n const text =\n cached !== undefined && cached.seq === message.seq\n ? cached.text\n : messageFrame(sub.channel.name, message, false);\n if (cached === undefined || cached.seq !== message.seq)\n sub.channel.frame = { seq: message.seq, text };\n send(sub.conn, text);\n }\n\n /**\n * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the\n * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one\n * is removed with `unsubbed why:\"gap\"`.\n */\n async function pump(sub: SubState, upTo: number | undefined): Promise<void> {\n if (sub.pumping && upTo === undefined) return;\n sub.pumping = true;\n try {\n while (sub.active) {\n sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);\n const next = sub.buffer[0];\n if (next !== undefined && next.seq === sub.lastSeq + 1) {\n deliver(sub, next, false);\n sub.buffer.shift();\n continue;\n }\n const target = next === undefined ? (upTo ?? 0) : next.seq - 1;\n if (sub.lastSeq >= target) break;\n const page = await adapter.history(\n sub.channel.name,\n sub.lastSeq,\n Math.min(REPLAY_PAGE, target - sub.lastSeq),\n );\n if (!sub.active) return;\n const first = page.messages[0];\n if (first === undefined || first.seq !== sub.lastSeq + 1) {\n if (!sub.gapOk) {\n removeSub(sub, \"gap\");\n return;\n }\n // The gap was reported at subscribe time: continue from what is retained.\n sub.lastSeq = (first?.seq ?? target + 1) - 1;\n continue;\n }\n for (const message of page.messages) {\n if (message.seq > target) break;\n deliver(sub, message, true);\n }\n }\n } catch (error) {\n report(error, { where: \"pump\", channel: sub.channel.name, connection: sub.conn.id });\n if (sub.active) removeSub(sub, \"unavailable\");\n } finally {\n sub.pumping = false;\n }\n // A message that arrived while the last history page was awaited is in the buffer; one more pass delivers it.\n if (sub.active && sub.buffer.length > 0) void pump(sub, undefined);\n }\n\n function onPresence(chan: ChanState, event: PresenceEvent): void {\n for (const sub of chan.subs) {\n if (!sub.active || (!sub.joined && !sub.watch)) continue;\n if (sub.presenceQueue !== undefined) sub.presenceQueue.push(event);\n else sendPresence(sub, event);\n }\n }\n\n function sendPresence(sub: SubState, event: PresenceEvent): void {\n sendFrame(sub.conn, { t: \"pres\", c: event.channel, op: event.op, m: event.member });\n }\n\n /** Removes a subscription from its connection and channel; tells the peer when `why` is given. */\n function removeSub(sub: SubState, why?: string): void {\n if (!sub.active) return;\n sub.active = false;\n sub.conn.subs.delete(sub.channel.name);\n sub.channel.subs.delete(sub);\n if (why !== undefined && !sub.conn.closed)\n sendFrame(sub.conn, { t: \"unsubbed\", c: sub.channel.name, ...(why === \"\" ? {} : { why }) });\n void releaseSub(sub);\n }\n\n /** The releases still running, so that `close` finishes them before it closes the adapter. */\n const releasing = new Set<Promise<void>>();\n\n function releaseSub(sub: SubState): Promise<void> {\n const running = release(sub);\n releasing.add(running);\n void running.finally(() => releasing.delete(running));\n return running;\n }\n\n async function release(sub: SubState): Promise<void> {\n const chan = sub.channel;\n try {\n // A join that is still being written must finish first, or its member would outlive the connection.\n await sub.joining?.catch(() => undefined);\n if (sub.joined) {\n sub.joined = false;\n await adapter.presenceRemove(chan.name, sub.conn.id);\n }\n } catch (error) {\n report(error, { where: \"presenceRemove\", channel: chan.name, connection: sub.conn.id });\n }\n if (chan.subs.size > 0 || chan.closing) return;\n chan.closing = true;\n if (chans.get(chan.name) === chan) chans.delete(chan.name);\n const stops = [chan.adapterSub, chan.presenceSub].filter(\n (item): item is Promise<Unsubscribe> => item !== undefined,\n );\n await Promise.all(\n stops.map(async (stop) => {\n try {\n await (await stop)();\n } catch (error) {\n report(error, { where: \"unsubscribe\", channel: chan.name });\n }\n }),\n );\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n function identityDenied(conn: ConnState): RealtimeError[\"code\"] {\n return conn.identity.user === undefined ? \"unauthorized\" : \"forbidden\";\n }\n\n async function subscribe(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"sub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n if (conn.subs.has(name)) return fail(conn, n, \"conflict\", name, \"already subscribed\");\n if (conn.subs.size >= limits.maxChannelsPerConnection)\n return fail(conn, n, \"limit\", name, \"too many channels on this connection\");\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n const wantsPresence = frame.hasPresence || frame.watch === true;\n if (\n wantsPresence &&\n !(await allowed(found.rule, \"presence\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (\n frame.hasPresence &&\n byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes\n )\n return fail(conn, n, \"too_large\", name, \"the presence state is too large\");\n if (conn.closed) return;\n const chan = channelState(name);\n if (chan.subs.size >= limits.maxSubscribersPerChannel)\n return fail(conn, n, \"limit\", name, \"the channel is full\");\n const sub: SubState = {\n conn,\n channel: chan,\n rule: found.rule,\n params: found.params,\n echo: frame.echo !== false,\n watch: frame.watch === true,\n joined: false,\n version: 0,\n lastSeq: 0,\n lastAt: now(),\n pumping: true,\n gapOk: false,\n buffer: [],\n presenceQueue: wantsPresence ? [] : undefined,\n joining: undefined,\n active: true,\n };\n conn.subs.set(name, sub);\n chan.subs.add(sub);\n try {\n await ensureAdapterSub(chan);\n if (wantsPresence) {\n await ensurePresenceSub(chan);\n startHeartbeat();\n }\n const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);\n if (!sub.active) return;\n const head = probe.head;\n const after = frame.after;\n let gap = false;\n let reset = false;\n if (after === undefined) {\n sub.lastSeq = head;\n } else if (after > head) {\n reset = true;\n sub.lastSeq = head;\n } else {\n sub.lastSeq = after;\n gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;\n sub.gapOk = gap;\n }\n sendFrame(conn, {\n t: \"subbed\",\n n,\n c: name,\n head,\n ...(gap ? { gap: true } : {}),\n ...(reset ? { reset: true } : {}),\n });\n if (frame.hasPresence) {\n sub.joining = joinPresence(sub, frame.presence ?? null);\n await sub.joining;\n }\n if (wantsPresence) await syncPresence(sub);\n if (!sub.active) return;\n await pump(sub, head);\n sub.gapOk = false;\n } catch (error) {\n if (sub.active) {\n removeSub(sub);\n report(error, { where: \"subscribe\", channel: name, connection: conn.id });\n fail(conn, n, error instanceof RealtimeError ? error.code : \"unavailable\", name);\n }\n }\n }\n\n async function joinPresence(sub: SubState, state: Json): Promise<void> {\n const existing = await adapter.presenceList(sub.channel.name);\n if (existing.length >= limits.maxPresenceMembers)\n throw new RealtimeError(\"limit\", \"the presence set is full\");\n sub.version = 1;\n sub.joined = true;\n startHeartbeat();\n const member: Member = {\n id: sub.conn.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: state,\n v: 1,\n };\n await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);\n }\n\n async function syncPresence(sub: SubState): Promise<void> {\n const members = await adapter.presenceList(sub.channel.name);\n if (!sub.active) return;\n sendFrame(sub.conn, { t: \"pres\", c: sub.channel.name, op: \"sync\", m: members });\n const queued = sub.presenceQueue ?? [];\n sub.presenceQueue = undefined;\n const known = new Map(members.map((member) => [member.id, member.v]));\n for (const event of queued) {\n // What the snapshot already shows is not announced again.\n const version = known.get(event.member.id);\n if (\n event.op === \"leave\"\n ? version === undefined\n : version !== undefined && version >= event.member.v\n )\n continue;\n sendPresence(sub, event);\n }\n }\n\n async function setPresence(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pres\" }>,\n ): Promise<void> {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined || !sub.joined) return fail(conn, undefined, \"not_subscribed\", frame.c);\n if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes)\n return fail(conn, undefined, \"too_large\", frame.c, \"the presence state is too large\");\n if (!conn.publishBucket.take(now())) return limit(conn, undefined, frame.c);\n sub.version += 1;\n const member: Member = {\n id: conn.id,\n ...(conn.identity.user === undefined ? {} : { u: conn.identity.user }),\n s: frame.s,\n v: sub.version,\n };\n try {\n await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);\n } catch (error) {\n report(error, { where: \"presenceSet\", channel: frame.c, connection: conn.id });\n fail(conn, undefined, \"unavailable\", frame.c);\n }\n }\n\n function limit(conn: ConnState, n: number | undefined, channel: string): void {\n fail(conn, n, \"rate_limited\", channel);\n if (conn.limited.hit(now())) drop(conn, 1008, \"rate limit exceeded\");\n }\n\n async function publishFrame(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"publish\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (frame.d.length > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the message is too large\");\n // A channel with no subscriber on this server has no bucket: only the connection's own rate applies to it.\n if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false)\n return limit(conn, n, name);\n try {\n const result = await adapter.publish(\n name,\n {\n event: frame.e,\n data: frame.d,\n publisher: conn.id,\n ...(conn.identity.user === undefined ? {} : { user: conn.identity.user }),\n },\n {\n history: found.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(frame.k === undefined ? {} : { key: frame.k }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n if (n !== undefined)\n sendFrame(conn, { t: \"ack\", n, s: result.seq, ...(result.duplicate ? { dup: true } : {}) });\n } catch (error) {\n report(error, { where: \"publish\", channel: name, connection: conn.id });\n fail(conn, n, \"unavailable\", name);\n }\n }\n\n async function request(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"req\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n let target: { rule: ChannelRule; params: Record<string, string> } | undefined =\n conn.subs.get(name);\n if (target === undefined && conn.stateless) {\n const found = match(name);\n if (\n found !== undefined &&\n (await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n target = found;\n }\n if (target === undefined)\n return fail(\n conn,\n n,\n identityDenied(conn) === \"unauthorized\" && conn.stateless\n ? \"unauthorized\"\n : \"not_subscribed\",\n name,\n );\n const sub = target;\n const handler = sub.rule.handlers?.[frame.e];\n if (handler === undefined) return fail(conn, n, \"no_handler\", name);\n if (conn.pending.size >= limits.maxPendingRequests)\n return fail(conn, n, \"limit\", name, \"too many requests in flight\");\n if (!conn.publishBucket.take(now())) return limit(conn, n, name);\n const controller = new AbortController();\n conn.pending.add(controller);\n const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);\n const timer = setTimeout(() => controller.abort(new RealtimeError(\"timeout\")), timeout);\n void (async () => {\n try {\n const run = Promise.resolve(\n handler({\n channel: name,\n params: sub.params,\n identity: conn.identity,\n event: frame.e,\n data: frame.d,\n signal: controller.signal,\n }),\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n controller.signal.addEventListener(\"abort\", () => reject(controller.signal.reason), {\n once: true,\n });\n });\n const reply = (await Promise.race([run, aborted])) ?? null;\n const text = JSON.stringify(reply);\n if (text === undefined || byteLength(text) > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, \"the reply is too large\");\n send(conn, `{\"t\":\"rep\",\"n\":${n},\"d\":${text}}`);\n } catch (error) {\n if (error instanceof RealtimeError)\n fail(\n conn,\n n,\n error.code,\n name,\n error.code === \"timeout\" || error.code === \"closed\" ? undefined : error.message,\n );\n else {\n report(error, { where: \"request\", channel: name, connection: conn.id });\n fail(conn, n, \"handler_error\", name);\n }\n } finally {\n clearTimeout(timer);\n conn.pending.delete(controller);\n settle(conn);\n }\n })();\n }\n\n async function handle(conn: ConnState, text: string): Promise<void> {\n const decoded = decodeClientFrame(text, limits.maxMessageBytes);\n if (!decoded.ok) {\n fail(conn, decoded.n, decoded.code, undefined, decoded.why);\n if (conn.invalid.hit(now())) drop(conn, 1008, \"too many invalid frames\");\n return;\n }\n const frame = decoded.frame;\n try {\n switch (frame.t) {\n case \"hello\":\n return;\n case \"ping\":\n sendFrame(conn, { t: \"pong\", ...(frame.n === undefined ? {} : { n: frame.n }) });\n return;\n case \"sub\":\n return await subscribe(conn, frame);\n case \"unsub\": {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined) return fail(conn, frame.n, \"not_subscribed\", frame.c);\n removeSub(sub);\n return;\n }\n case \"pub\":\n return await publishFrame(conn, frame);\n case \"req\":\n return await request(conn, frame);\n case \"pres\":\n return await setPresence(conn, frame);\n }\n } catch (error) {\n const n = \"n\" in frame ? frame.n : undefined;\n const channel = \"c\" in frame ? frame.c : undefined;\n if (error instanceof RealtimeError) fail(conn, n, error.code, channel);\n else {\n report(error, {\n where: \"frame\",\n connection: conn.id,\n ...(channel === undefined ? {} : { channel }),\n });\n fail(conn, n, \"unavailable\", channel);\n }\n }\n }\n\n async function run(conn: ConnState): Promise<void> {\n if (conn.running) return;\n conn.running = true;\n try {\n for (let text = conn.queue.shift(); text !== undefined; text = conn.queue.shift()) {\n if (conn.closed) break;\n try {\n await handle(conn, text);\n } catch (error) {\n if (error instanceof RealtimeError) fail(conn, undefined, error.code);\n else {\n report(error, { where: \"frame\", connection: conn.id });\n fail(conn, undefined, \"unavailable\");\n }\n }\n }\n } finally {\n conn.running = false;\n settle(conn);\n }\n }\n\n /** Wakes the callers of `settled` when the connection has nothing left to do. */\n function settle(conn: ConnState): void {\n if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;\n for (const wake of conn.waiters.splice(0)) wake();\n }\n\n async function closeConnection(conn: ConnState): Promise<void> {\n if (conn.closed) return conn.done;\n conn.closed = true;\n if (conns.get(conn.id) === conn) conns.delete(conn.id);\n for (const controller of conn.pending) controller.abort(new RealtimeError(\"closed\"));\n conn.pending.clear();\n conn.queue.length = 0;\n for (const wake of conn.waiters.splice(0)) wake();\n const subs = [...conn.subs.values()];\n for (const sub of subs) {\n sub.active = false;\n sub.channel.subs.delete(sub);\n }\n conn.subs.clear();\n await Promise.allSettled(subs.map((sub) => releaseSub(sub)));\n conn.release?.();\n }\n\n // --- presence heartbeat --------------------------------------------------------------------------------------------\n\n function startHeartbeat(): void {\n if (heartbeat !== undefined || closing) return;\n const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);\n // Node's timer can be unref'd so an idle hub does not keep the process alive; other runtimes return a number.\n (timer as unknown as { unref?: () => void }).unref?.();\n heartbeat = timer;\n }\n\n async function beat(): Promise<void> {\n if (beating) return;\n beating = true;\n try {\n await beatOnce();\n } finally {\n beating = false;\n }\n }\n\n async function beatOnce(): Promise<void> {\n const refs: { channel: string; id: string }[] = [];\n const watched = new Set<string>();\n for (const conn of conns.values())\n for (const sub of conn.subs.values()) {\n if (sub.joined) refs.push({ channel: sub.channel.name, id: conn.id });\n if (sub.joined || sub.watch) watched.add(sub.channel.name);\n }\n if (refs.length === 0 && watched.size === 0) {\n clearInterval(heartbeat);\n heartbeat = undefined;\n return;\n }\n try {\n const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);\n for (const ref of missing) {\n const sub = conns.get(ref.id)?.subs.get(ref.channel);\n if (sub === undefined || !sub.joined) continue;\n await adapter.presenceSet(\n ref.channel,\n {\n id: ref.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: null,\n v: sub.version,\n },\n limits.presenceTtlMs,\n );\n }\n for (const channel of watched) await adapter.presenceReap(channel);\n } catch (error) {\n report(error, { where: \"presenceHeartbeat\" });\n }\n }\n\n // --- the hub -------------------------------------------------------------------------------------------------------\n\n for (const [pattern, rule] of Object.entries(options.channels ?? {}))\n rules.push(compileRule(pattern, rule));\n\n const core: HubCore = {\n limits,\n adapter,\n report,\n match,\n allowed,\n\n channel(pattern, rule) {\n rules.push(compileRule(pattern, rule));\n return core;\n },\n\n connect(transport, identity = {}, connectOptions = {}) {\n if (closing) throw new RealtimeError(\"closed\", \"the hub is closed\");\n let release: () => void = () => undefined;\n const done = new Promise<void>((resolve) => {\n release = resolve;\n });\n const owner =\n connectOptions.actingFor === undefined ? undefined : conns.get(connectOptions.actingFor);\n const acting =\n owner !== undefined && identity.user !== undefined && owner.identity.user === identity.user;\n const conn: ConnState = {\n id: acting ? (connectOptions.actingFor as string) : randomId(),\n identity,\n transport,\n subs: new Map(),\n queue: [],\n publishBucket:\n connectOptions.publishBucket ??\n new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),\n stateless: connectOptions.stateless === true,\n waiters: [],\n invalid: new WindowCounter(10, 60_000),\n limited: new WindowCounter(50, 10_000),\n pending: new Set(),\n running: false,\n closed: false,\n release,\n done,\n };\n // An acting connection shares its owner's id: it must not replace the owner in the table.\n if (!acting) conns.set(conn.id, conn);\n sendFrame(conn, {\n t: \"ready\",\n v: PROTOCOL_VERSION,\n id: conn.id,\n hb: limits.heartbeatMs,\n max: limits.maxMessageBytes,\n });\n return {\n id: conn.id,\n identity,\n done,\n receive(text) {\n if (conn.closed) return;\n if (conn.queue.length >= limits.maxQueuedFrames) {\n drop(conn, 1013, \"too many frames waiting\");\n return;\n }\n conn.queue.push(text);\n void run(conn);\n },\n closed() {\n void closeConnection(conn);\n },\n settled() {\n return new Promise<void>((resolve) => {\n conn.waiters.push(resolve);\n settle(conn);\n });\n },\n };\n },\n\n async publish(channel, event, data, publishOptions = {}) {\n if (!isChannelName(channel))\n throw new RealtimeError(\"bad_channel\", `\"${channel}\" is not a valid channel name`);\n if (!isEventName(event))\n throw new RealtimeError(\"bad_frame\", `\"${event}\" is not a valid event name`);\n if (publishOptions.key !== undefined && !isIdempotencyKey(publishOptions.key))\n throw new RealtimeError(\"bad_frame\", \"the idempotency key is not valid\");\n let text: string | undefined;\n try {\n text = JSON.stringify(data === undefined ? null : data);\n } catch (error) {\n throw new TypeError(\"the data is not JSON\", { cause: error });\n }\n if (text === undefined) throw new TypeError(\"the data is not JSON\");\n if (byteLength(text) > limits.maxMessageBytes)\n throw new RealtimeError(\"too_large\", \"the message is too large\");\n const found = match(channel);\n const result = await adapter.publish(\n channel,\n { event, data: text },\n {\n history: found?.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(publishOptions.key === undefined ? {} : { key: publishOptions.key }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n return result;\n },\n\n async setMemberState(identity, channel, connectionId, state) {\n if (identity.user === undefined)\n throw new RealtimeError(\"unauthorized\", \"presence changes over HTTP need a signed-in user\");\n const found = match(channel);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"presence\", channel, found.params, identity))\n )\n throw new RealtimeError(\"forbidden\");\n if (byteLength(JSON.stringify(state)) > limits.maxStateBytes)\n throw new RealtimeError(\"too_large\", \"the presence state is too large\");\n const members = await adapter.presenceList(channel);\n const member = members.find((item) => item.id === connectionId);\n if (member === undefined || member.u !== identity.user)\n throw new RealtimeError(\"forbidden\", \"that connection is not yours\");\n await adapter.presenceSet(\n channel,\n { id: member.id, u: member.u, s: state, v: member.v + 1 },\n limits.presenceTtlMs,\n );\n },\n\n revoke(target) {\n if (\n target.user === undefined &&\n target.connection === undefined &&\n target.channel === undefined\n )\n throw new TypeError(\"revoke needs a user, a connection or a channel.\");\n let count = 0;\n for (const conn of [...conns.values()]) {\n if (target.connection !== undefined && conn.id !== target.connection) continue;\n if (target.user !== undefined && conn.identity.user !== target.user) continue;\n for (const sub of [...conn.subs.values()]) {\n if (target.channel !== undefined && sub.channel.name !== target.channel) continue;\n removeSub(sub, \"revoked\");\n count += 1;\n }\n }\n return count;\n },\n\n stats() {\n let subscriptions = 0;\n for (const conn of conns.values()) subscriptions += conn.subs.size;\n return {\n connections: conns.size,\n channels: chans.size,\n subscriptions,\n ...counters,\n };\n },\n\n async close() {\n if (closing) return;\n closing = true;\n if (heartbeat !== undefined) clearInterval(heartbeat);\n heartbeat = undefined;\n const all = [...conns.values()];\n for (const conn of all) drop(conn, 1001, \"going away\");\n await Promise.all(all.map((conn) => conn.done));\n await Promise.allSettled([...releasing]);\n if (ownsAdapter) await adapter.close();\n },\n };\n return core;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hub.js","names":["randomId","bytes","encodeFrame","frame","messageFrame","channel","message","replayed","createHubCore","limits","resolveLimits","options","now","ownsAdapter","adapter","memoryAdapter","report","error","context","rules","conns","chans","heartbeat","closing","beating","counters","match","compiled","params","matchRule","allowed","permission","rule","op","RealtimeError","send","conn","drop","text","onTransport","closeConnection","call","sendFrame","fail","n","reason","code","channelState","chan","name","TokenBucket","ensureFeed","key","opened","open","onMessage","sub","removeSub","deliver","pump","cached","upTo","next","target","page","first","onPresence","event","sendPresence","why","releaseSub","releasing","running","release","stops","item","stop","identityDenied","subscribe","found","wantsPresence","byteLength","startHeartbeat","probe","head","after","gap","reset","joinPresence","syncPresence","state","member","members","queued","known","version","setPresence","limit","publishFrame","result","request","handler","controller","timeout","timer","run","aborted","reject","reply","settle","refuseFrame","handle","decoded","decodeClientFrame","wake","subs","beat","beatOnce","refs","watched","missing","ref","pattern","compileRule","core","done","resolve","owner","connectOptions","acting","identity","WindowCounter","isChannelName","isEventName","publishOptions","isIdempotencyKey","data","connectionId","local","count","subscriptions","all"],"sources":["../../../src/server/hub.ts"],"sourcesContent":["import type { Adapter, PublishResult, Unsubscribe } from \"../adapter\";\nimport { RealtimeError } from \"../errors\";\nimport { memoryAdapter } from \"../memory\";\nimport {\n byteLength,\n type ClientFrame,\n decodeClientFrame,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n PROTOCOL_VERSION,\n type PresenceEvent,\n type StoredMessage,\n} from \"../protocol\";\nimport { type HubLimits, resolveLimits, TokenBucket, WindowCounter } from \"./limits\";\nimport {\n type ChannelRule,\n type CompiledRule,\n compileRule,\n type Identity,\n matchRule,\n type RuleContext,\n} from \"./rules\";\n\n/**\n * What a hub needs from a connection's transport. A WebSocket, an SSE stream and an in-memory test client all fit.\n * @since 1.0.0\n */\nexport interface Transport {\n /** Sends one frame (text). False when the transport is closed. */\n send(frame: string): boolean;\n /** Closes the transport with a WebSocket close code and a reason of at most 123 bytes. */\n close(code: number, reason: string): void;\n /** Bytes accepted by `send` that the peer has not yet taken. When present, a connection over the limit is closed. */\n buffered?(): number;\n}\n\n/** Where an error reported to `onError` came from. @since 1.0.0 */\nexport interface ErrorContext {\n readonly where: string;\n readonly channel?: string;\n readonly connection?: string;\n}\n\n/** Options of {@link createHub}. @since 1.0.0 */\nexport interface HubOptions {\n /** Storage and fan-out. Default: a fresh `memoryAdapter()`, closed with the hub. */\n readonly adapter?: Adapter;\n /** Channel rules by pattern, in order. The first matching pattern wins. */\n readonly channels?: Readonly<Record<string, ChannelRule>>;\n /** Limits (see {@link HubLimits}). */\n readonly limits?: Partial<HubLimits>;\n /** Receives failures the hub handled (an adapter outage, a rule predicate that threw). Default: `console.error`. */\n readonly onError?: (error: unknown, context: ErrorContext) => void;\n /** The clock, in epoch milliseconds. Default `Date.now`. */\n readonly now?: () => number;\n}\n\n/** Counters of a running hub. @since 1.0.0 */\nexport interface HubStats {\n readonly connections: number;\n readonly channels: number;\n readonly subscriptions: number;\n readonly published: number;\n readonly delivered: number;\n readonly refused: number;\n readonly slowConsumers: number;\n}\n\n/** One connection, as the transport glue drives it. @since 1.0.0 */\nexport interface HubConnection {\n readonly id: string;\n readonly identity: Identity;\n /** Hands the hub one text frame from the peer. Frames are handled in order. */\n receive(text: string): void;\n /** Tells the hub the transport closed. Safe to call twice. */\n closed(): void;\n /** Resolves once every frame received so far was handled and every request answered. */\n settled(): Promise<void>;\n /** Resolves when the hub finished releasing what the connection held. */\n readonly done: Promise<void>;\n}\n\n/** How a connection differs from a long-lived one. @since 1.0.0 */\nexport interface ConnectOptions {\n /**\n * A connection that exists for one HTTP request: a request is answered for any identity the channel's `subscribe` rule\n * accepts (there is no subscription to look up), and nothing is kept after it closes.\n */\n readonly stateless?: boolean;\n /** Replaces the connection's own publish bucket, so a caller can share one across requests. */\n readonly publishBucket?: TokenBucket;\n /**\n * The id of a long-lived connection of the same user this request acts for (SSE clients post under their stream's id, so\n * `echo:false` and `publisher` work). Honored only when that connection is open on this server and has the same user.\n */\n readonly actingFor?: string;\n}\n\n/** Who to remove subscriptions of. At least one field. @since 1.0.0 */\nexport interface RevokeTarget {\n readonly user?: string;\n readonly connection?: string;\n readonly channel?: string;\n}\n\n/** The server object. @since 1.0.0 */\nexport interface HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: ChannelRule): HubCore;\n /** Starts handling a connection. Sends `ready`. */\n connect(transport: Transport, identity?: Identity, options?: ConnectOptions): HubConnection;\n /** Publishes from the server (no rule applies). */\n publish(\n channel: string,\n event: string,\n data: unknown,\n options?: { readonly key?: string },\n ): Promise<PublishResult>;\n /** Removes matching subscriptions; each subscriber gets `unsubbed why:\"revoked\"`. Returns how many were removed. */\n revoke(target: RevokeTarget): number;\n /** Counters. */\n stats(): HubStats;\n /** Closes every connection with 1001, stops timers, removes presence, settles requests. */\n close(): Promise<void>;\n /** @internal Changes the state of a presence member from outside its connection (SSE clients). */\n setMemberState(\n identity: Identity,\n channel: string,\n connectionId: string,\n state: Json,\n ): Promise<void>;\n /** @internal The resolved limits. */\n readonly limits: HubLimits;\n /** @internal The adapter. */\n readonly adapter: Adapter;\n /** @internal Reports an error. */\n report(error: unknown, context: ErrorContext): void;\n /** @internal Matches a channel against the rules. */\n match(\n channel: string,\n ): { readonly rule: ChannelRule; readonly params: Record<string, string> } | undefined;\n /** @internal Evaluates a rule permission for an identity. */\n allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean>;\n}\n\ninterface SubState {\n readonly conn: ConnState;\n readonly channel: ChanState;\n readonly rule: ChannelRule;\n readonly params: Record<string, string>;\n readonly echo: boolean;\n readonly watch: boolean;\n joined: boolean;\n version: number;\n /** The state this connection last published to the presence set, to write it back if the entry expired. */\n state: Json;\n lastSeq: number;\n /** The `at` of the last message delivered: a message with an old seq but a newer `at` means the numbering restarted. */\n lastAt: number;\n /** While true, live messages wait in `buffer` and `pump` delivers in order. */\n pumping: boolean;\n gapOk: boolean;\n buffer: StoredMessage[];\n presenceQueue: PresenceEvent[] | undefined;\n /** The join in progress, so that releasing the subscription waits for it and never leaves a member behind. */\n joining: Promise<void> | undefined;\n active: boolean;\n}\n\ninterface ChanState {\n readonly name: string;\n readonly subs: Set<SubState>;\n readonly bucket: TokenBucket;\n adapterSub: Promise<Unsubscribe> | undefined;\n presenceSub: Promise<Unsubscribe> | undefined;\n closing: boolean;\n /** Cache of the encoded frame of the message being fanned out. */\n frame: { readonly seq: number; readonly text: string } | undefined;\n}\n\ninterface ConnState {\n readonly id: string;\n readonly identity: Identity;\n readonly transport: Transport;\n readonly subs: Map<string, SubState>;\n readonly queue: string[];\n readonly publishBucket: TokenBucket;\n /** Spent by `sub` and `unsub` frames: subscription churn costs the adapter (and Redis) work like a publish does. */\n readonly controlBucket: TokenBucket;\n readonly invalid: WindowCounter;\n readonly limited: WindowCounter;\n readonly pending: Set<AbortController>;\n readonly stateless: boolean;\n readonly waiters: (() => void)[];\n running: boolean;\n closed: boolean;\n release: (() => void) | undefined;\n readonly done: Promise<void>;\n}\n\nconst REPLAY_PAGE = 500;\nconst MAX_PUMP_BUFFER = 10_000;\nconst ID_BYTES = 16;\n\n/** A random id: 128 bits, base64url. */\nfunction randomId(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(ID_BYTES));\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction encodeFrame(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\nfunction messageFrame(channel: string, message: StoredMessage, replayed: boolean): string {\n return (\n `{\"t\":\"msg\",\"c\":${JSON.stringify(channel)},\"s\":${message.seq},\"e\":${JSON.stringify(message.event)},\"d\":${message.data}` +\n (message.publisher === undefined ? \"\" : `,\"p\":${JSON.stringify(message.publisher)}`) +\n (message.user === undefined ? \"\" : `,\"u\":${JSON.stringify(message.user)}`) +\n (replayed ? ',\"r\":1}' : \"}\")\n );\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules, enforces limits and talks to\n * one adapter. Wire it to a transport with `hub.websocket()`, `hub.sse()` and `hub.post()` (Dusk routes) or with `hub.connect`.\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: { \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined } },\n * });\n * const sent: string[] = [];\n * const connection = hub.connect({ send: (frame) => (sent.push(frame), true), close: () => {} });\n * connection.receive('{\"t\":\"sub\",\"n\":1,\"c\":\"chat:lobby\"}');\n * @since 1.0.0\n */\nexport function createHubCore(options: HubOptions = {}): HubCore {\n const limits = resolveLimits(options.limits);\n const now = options.now ?? Date.now;\n const ownsAdapter = options.adapter === undefined;\n const adapter =\n options.adapter ??\n memoryAdapter({\n now,\n onError: (error, context) =>\n report(error, { where: `memory.${context.where}`, channel: context.channel }),\n });\n const rules: CompiledRule[] = [];\n const conns = new Map<string, ConnState>();\n const chans = new Map<string, ChanState>();\n let heartbeat: ReturnType<typeof setInterval> | undefined;\n let closing = false;\n let beating = false;\n const counters = { published: 0, delivered: 0, refused: 0, slowConsumers: 0 };\n\n const report = (error: unknown, context: ErrorContext): void => {\n if (options.onError !== undefined) {\n try {\n options.onError(error, context);\n } catch {\n // A failing reporter must not break the hub.\n }\n } else {\n console.error(`[realtime] ${context.where}`, error);\n }\n };\n\n function match(\n channel: string,\n ): { rule: ChannelRule; params: Record<string, string> } | undefined {\n for (const compiled of rules) {\n const params = matchRule(compiled, channel);\n if (params !== undefined) return { rule: compiled.rule, params };\n }\n return undefined;\n }\n\n async function allowed(\n rule: ChannelRule,\n op: RuleContext[\"op\"],\n channel: string,\n params: Record<string, string>,\n identity: Identity,\n ): Promise<boolean> {\n const permission = rule[op];\n if (permission === undefined || permission === false) return false;\n if (permission === true) return true;\n try {\n return (await permission({ channel, params, identity, op })) === true;\n } catch (error) {\n report(error, { where: `rule.${op}`, channel });\n throw new RealtimeError(\n \"unavailable\",\n __DEV__ ? \"the channel rule could not be evaluated\" : undefined,\n );\n }\n }\n\n function send(conn: ConnState, text: string): void {\n if (conn.closed) return;\n try {\n const buffered = conn.transport.buffered?.() ?? 0;\n if (buffered > limits.maxBufferedBytes) {\n counters.slowConsumers += 1;\n drop(conn, 1013, __DEV__ ? \"slow consumer\" : \"\");\n return;\n }\n if (conn.transport.send(text)) return;\n } catch (error) {\n // A transport that throws is a broken socket: this connection goes, the others are not affected (guarantee G13). The\n // transport is closed too, or its peer would keep a socket the hub no longer answers on until its own heartbeat gave up.\n report(error, { where: \"transport.send\", connection: conn.id });\n onTransport(conn, \"transport.close\", () =>\n conn.transport.close(1011, __DEV__ ? \"write failed\" : \"\"),\n );\n }\n void closeConnection(conn);\n }\n\n /** Runs a call on the transport; a transport that is already gone throws, which is only reported. */\n function onTransport(conn: ConnState, where: string, call: () => unknown): void {\n try {\n call();\n } catch (error) {\n report(error, { where, connection: conn.id });\n }\n }\n\n function sendFrame(conn: ConnState, frame: Record<string, unknown>): void {\n send(conn, encodeFrame(frame));\n }\n\n function fail(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n channel?: string,\n message?: string,\n ): void {\n counters.refused += 1;\n sendFrame(conn, {\n t: \"err\",\n ...(n === undefined ? {} : { n }),\n ...(channel === undefined ? {} : { c: channel }),\n code,\n ...(message === undefined || message === \"\" ? {} : { msg: message }),\n });\n }\n\n /** Sends a final frame, closes the transport and releases the connection. */\n function drop(conn: ConnState, code: number, reason: string): void {\n if (conn.closed) return;\n // The connection is released whatever the transport does, so its failures are only reported.\n onTransport(conn, \"transport.send\", () =>\n conn.transport.send(encodeFrame({ t: \"bye\", code, why: reason })),\n );\n onTransport(conn, \"transport.close\", () => conn.transport.close(code, reason));\n void closeConnection(conn);\n }\n\n // --- channels ------------------------------------------------------------------------------------------------------\n\n function channelState(name: string): ChanState {\n let chan = chans.get(name);\n if (chan === undefined || chan.closing) {\n chan = {\n name,\n subs: new Set(),\n bucket: new TokenBucket(\n limits.channelPublishesPerSecond,\n limits.channelPublishesPerSecond,\n now(),\n ),\n adapterSub: undefined,\n presenceSub: undefined,\n closing: false,\n frame: undefined,\n };\n chans.set(name, chan);\n }\n return chan;\n }\n\n /** Opens one of the channel's two adapter feeds once; a failed attempt is forgotten so the next caller retries. */\n async function ensureFeed(\n chan: ChanState,\n key: \"adapterSub\" | \"presenceSub\",\n open: () => Promise<Unsubscribe>,\n ): Promise<void> {\n if (chan[key] === undefined) {\n const opened = open();\n chan[key] = opened;\n opened.catch(() => {\n chan[key] = undefined;\n });\n }\n await chan[key];\n }\n\n function onMessage(chan: ChanState, message: StoredMessage): void {\n for (const sub of chan.subs) {\n if (!sub.active) continue;\n if (sub.pumping) {\n // A consumer so far behind that it cannot catch up is resubscribed from its last seq instead of holding memory.\n if (sub.buffer.length >= MAX_PUMP_BUFFER) removeSub(sub, \"gap\");\n else sub.buffer.push(message);\n continue;\n }\n if (message.seq === sub.lastSeq + 1) deliver(sub, message, false);\n else if (message.seq > sub.lastSeq + 1) {\n sub.buffer.push(message);\n void pump(sub, undefined);\n } else if (message.at > sub.lastAt) removeSub(sub, \"reset\");\n // Any other seq at or below the cursor was already delivered: dropped (guarantee G2).\n }\n }\n\n function deliver(sub: SubState, message: StoredMessage, replayed: boolean): void {\n sub.lastSeq = message.seq;\n sub.lastAt = Math.max(sub.lastAt, message.at);\n if (!sub.echo && message.publisher === sub.conn.id) return;\n counters.delivered += 1;\n if (replayed) {\n send(sub.conn, messageFrame(sub.channel.name, message, true));\n return;\n }\n const cached = sub.channel.frame;\n const text =\n cached !== undefined && cached.seq === message.seq\n ? cached.text\n : messageFrame(sub.channel.name, message, false);\n if (cached === undefined || cached.seq !== message.seq)\n sub.channel.frame = { seq: message.seq, text };\n send(sub.conn, text);\n }\n\n /**\n * Delivers, in order and without a gap, the messages after `sub.lastSeq` up to `upTo` and whatever is buffered. Fetches the\n * missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one\n * is removed with `unsubbed why:\"gap\"`.\n */\n async function pump(sub: SubState, upTo: number | undefined): Promise<void> {\n if (sub.pumping && upTo === undefined) return;\n sub.pumping = true;\n try {\n while (sub.active) {\n sub.buffer = sub.buffer.filter((message) => message.seq > sub.lastSeq);\n const next = sub.buffer[0];\n if (next !== undefined && next.seq === sub.lastSeq + 1) {\n deliver(sub, next, false);\n sub.buffer.shift();\n continue;\n }\n const target = next === undefined ? (upTo ?? 0) : next.seq - 1;\n if (sub.lastSeq >= target) break;\n const page = await adapter.history(\n sub.channel.name,\n sub.lastSeq,\n Math.min(REPLAY_PAGE, target - sub.lastSeq),\n );\n if (!sub.active) return;\n const first = page.messages[0];\n if (first === undefined || first.seq !== sub.lastSeq + 1) {\n if (!sub.gapOk) {\n removeSub(sub, \"gap\");\n return;\n }\n // The gap was reported at subscribe time: continue from what is retained.\n sub.lastSeq = (first?.seq ?? target + 1) - 1;\n continue;\n }\n for (const message of page.messages) {\n if (message.seq > target) break;\n deliver(sub, message, true);\n }\n }\n } catch (error) {\n report(error, { where: \"pump\", channel: sub.channel.name, connection: sub.conn.id });\n if (sub.active) removeSub(sub, \"unavailable\");\n } finally {\n sub.pumping = false;\n }\n // A message that arrived while the last history page was awaited is in the buffer; one more pass delivers it.\n if (sub.active && sub.buffer.length > 0) void pump(sub, undefined);\n }\n\n function onPresence(chan: ChanState, event: PresenceEvent): void {\n for (const sub of chan.subs) {\n // A change to this connection's own member made elsewhere (an HTTP post to another server) is what a write-back after an\n // expiry has to carry, so the connection's copy follows it.\n if (\n sub.joined &&\n event.op !== \"leave\" &&\n event.member.id === sub.conn.id &&\n event.member.v > sub.version\n ) {\n sub.version = event.member.v;\n sub.state = event.member.s;\n }\n if (!sub.active || (!sub.joined && !sub.watch)) continue;\n if (sub.presenceQueue !== undefined) sub.presenceQueue.push(event);\n else sendPresence(sub, event);\n }\n }\n\n function sendPresence(sub: SubState, event: PresenceEvent): void {\n sendFrame(sub.conn, { t: \"pres\", c: event.channel, op: event.op, m: event.member });\n }\n\n /** Removes a subscription from its connection and channel; tells the peer when `why` is given. */\n function removeSub(sub: SubState, why?: string): void {\n if (!sub.active) return;\n sub.active = false;\n sub.conn.subs.delete(sub.channel.name);\n sub.channel.subs.delete(sub);\n if (why !== undefined && !sub.conn.closed)\n sendFrame(sub.conn, { t: \"unsubbed\", c: sub.channel.name, ...(why === \"\" ? {} : { why }) });\n void releaseSub(sub);\n }\n\n /** The releases still running, so that `close` finishes them before it closes the adapter. */\n const releasing = new Set<Promise<void>>();\n\n function releaseSub(sub: SubState): Promise<void> {\n const running = release(sub);\n releasing.add(running);\n void running.finally(() => releasing.delete(running));\n return running;\n }\n\n async function release(sub: SubState): Promise<void> {\n const chan = sub.channel;\n try {\n // A join that is still being written must finish first, or its member would outlive the connection.\n await sub.joining?.catch(() => undefined);\n if (sub.joined) {\n sub.joined = false;\n await adapter.presenceRemove(chan.name, sub.conn.id);\n }\n } catch (error) {\n report(error, { where: \"presenceRemove\", channel: chan.name, connection: sub.conn.id });\n }\n if (chan.subs.size > 0 || chan.closing) return;\n chan.closing = true;\n if (chans.get(chan.name) === chan) chans.delete(chan.name);\n const stops = [chan.adapterSub, chan.presenceSub].filter(\n (item): item is Promise<Unsubscribe> => item !== undefined,\n );\n await Promise.all(\n stops.map(async (stop) => {\n try {\n await (await stop)();\n } catch (error) {\n report(error, { where: \"unsubscribe\", channel: chan.name });\n }\n }),\n );\n }\n\n // --- frames --------------------------------------------------------------------------------------------------------\n\n function identityDenied(conn: ConnState): RealtimeError[\"code\"] {\n return conn.identity.user === undefined ? \"unauthorized\" : \"forbidden\";\n }\n\n async function subscribe(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"sub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n if (conn.subs.has(name))\n return fail(conn, n, \"conflict\", name, __DEV__ ? \"already subscribed\" : undefined);\n if (conn.subs.size >= limits.maxChannelsPerConnection)\n return fail(\n conn,\n n,\n \"limit\",\n name,\n __DEV__ ? \"too many channels on this connection\" : undefined,\n );\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n const wantsPresence = frame.hasPresence || frame.watch === true;\n if (\n wantsPresence &&\n !(await allowed(found.rule, \"presence\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (\n frame.hasPresence &&\n byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes\n )\n return fail(\n conn,\n n,\n \"too_large\",\n name,\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n if (conn.closed) return;\n const chan = channelState(name);\n if (chan.subs.size >= limits.maxSubscribersPerChannel)\n return fail(conn, n, \"limit\", name, __DEV__ ? \"the channel is full\" : undefined);\n const sub: SubState = {\n conn,\n channel: chan,\n rule: found.rule,\n params: found.params,\n echo: frame.echo !== false,\n watch: frame.watch === true,\n joined: false,\n version: 0,\n state: null,\n lastSeq: 0,\n lastAt: now(),\n pumping: true,\n gapOk: false,\n buffer: [],\n presenceQueue: wantsPresence ? [] : undefined,\n joining: undefined,\n active: true,\n };\n conn.subs.set(name, sub);\n chan.subs.add(sub);\n try {\n await ensureFeed(chan, \"adapterSub\", () =>\n adapter.subscribe(chan.name, (message) => onMessage(chan, message)),\n );\n if (wantsPresence) {\n await ensureFeed(chan, \"presenceSub\", () =>\n adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)),\n );\n startHeartbeat();\n }\n const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);\n if (!sub.active) return;\n const head = probe.head;\n const after = frame.after;\n let gap = false;\n let reset = false;\n if (after === undefined) {\n sub.lastSeq = head;\n } else if (after > head) {\n reset = true;\n sub.lastSeq = head;\n } else {\n sub.lastSeq = after;\n gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;\n sub.gapOk = gap;\n }\n sendFrame(conn, {\n t: \"subbed\",\n n,\n c: name,\n head,\n ...(gap ? { gap: true } : {}),\n ...(reset ? { reset: true } : {}),\n });\n if (frame.hasPresence) {\n sub.joining = joinPresence(sub, frame.presence ?? null);\n await sub.joining;\n }\n if (wantsPresence) await syncPresence(sub);\n if (!sub.active) return;\n await pump(sub, head);\n sub.gapOk = false;\n } catch (error) {\n if (sub.active) {\n removeSub(sub);\n report(error, { where: \"subscribe\", channel: name, connection: conn.id });\n fail(conn, n, error instanceof RealtimeError ? error.code : \"unavailable\", name);\n }\n }\n }\n\n async function joinPresence(sub: SubState, state: Json): Promise<void> {\n const existing = await adapter.presenceList(sub.channel.name);\n if (existing.length >= limits.maxPresenceMembers)\n throw new RealtimeError(\"limit\", __DEV__ ? \"the presence set is full\" : undefined);\n sub.version = 1;\n sub.state = state;\n sub.joined = true;\n startHeartbeat();\n const member: Member = {\n id: sub.conn.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: state,\n v: 1,\n };\n await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);\n }\n\n async function syncPresence(sub: SubState): Promise<void> {\n const members = await adapter.presenceList(sub.channel.name);\n if (!sub.active) return;\n sendFrame(sub.conn, { t: \"pres\", c: sub.channel.name, op: \"sync\", m: members });\n const queued = sub.presenceQueue ?? [];\n sub.presenceQueue = undefined;\n const known = new Map(members.map((member) => [member.id, member.v]));\n for (const event of queued) {\n // What the snapshot already shows is not announced again.\n const version = known.get(event.member.id);\n if (\n event.op === \"leave\"\n ? version === undefined\n : version !== undefined && version >= event.member.v\n )\n continue;\n sendPresence(sub, event);\n }\n }\n\n async function setPresence(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pres\" }>,\n ): Promise<void> {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined || !sub.joined) return fail(conn, undefined, \"not_subscribed\", frame.c);\n if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes)\n return fail(\n conn,\n undefined,\n \"too_large\",\n frame.c,\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n if (!conn.publishBucket.take(now())) return limit(conn, undefined, frame.c);\n sub.version += 1;\n sub.state = frame.s;\n const member: Member = {\n id: conn.id,\n ...(conn.identity.user === undefined ? {} : { u: conn.identity.user }),\n s: frame.s,\n v: sub.version,\n };\n try {\n await adapter.presenceSet(frame.c, member, limits.presenceTtlMs);\n } catch (error) {\n report(error, { where: \"presenceSet\", channel: frame.c, connection: conn.id });\n fail(conn, undefined, \"unavailable\", frame.c);\n }\n }\n\n function limit(conn: ConnState, n: number | undefined, channel: string): void {\n fail(conn, n, \"rate_limited\", channel);\n if (conn.limited.hit(now())) drop(conn, 1008, __DEV__ ? \"rate limit exceeded\" : \"\");\n }\n\n async function publishFrame(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"pub\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n const found = match(name);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"publish\", name, found.params, conn.identity))\n )\n return fail(conn, n, identityDenied(conn), name);\n if (frame.d.length > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, __DEV__ ? \"the message is too large\" : undefined);\n // A channel with no subscriber on this server has no bucket: only the connection's own rate applies to it.\n if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false)\n return limit(conn, n, name);\n try {\n const result = await adapter.publish(\n name,\n {\n event: frame.e,\n data: frame.d,\n publisher: conn.id,\n ...(conn.identity.user === undefined ? {} : { user: conn.identity.user }),\n },\n {\n history: found.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(frame.k === undefined ? {} : { key: frame.k }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n if (n !== undefined)\n sendFrame(conn, { t: \"ack\", n, s: result.seq, ...(result.duplicate ? { dup: true } : {}) });\n } catch (error) {\n report(error, { where: \"publish\", channel: name, connection: conn.id });\n fail(conn, n, \"unavailable\", name);\n }\n }\n\n async function request(\n conn: ConnState,\n frame: Extract<ClientFrame, { t: \"req\" }>,\n ): Promise<void> {\n const { c: name, n } = frame;\n let target: { rule: ChannelRule; params: Record<string, string> } | undefined =\n conn.subs.get(name);\n if (target === undefined && conn.stateless) {\n const found = match(name);\n if (\n found !== undefined &&\n (await allowed(found.rule, \"subscribe\", name, found.params, conn.identity))\n )\n target = found;\n }\n if (target === undefined)\n return fail(\n conn,\n n,\n identityDenied(conn) === \"unauthorized\" && conn.stateless\n ? \"unauthorized\"\n : \"not_subscribed\",\n name,\n );\n const sub = target;\n const handler = sub.rule.handlers?.[frame.e];\n if (handler === undefined) return fail(conn, n, \"no_handler\", name);\n if (conn.pending.size >= limits.maxPendingRequests)\n return fail(conn, n, \"limit\", name, __DEV__ ? \"too many requests in flight\" : undefined);\n if (!conn.publishBucket.take(now())) return limit(conn, n, name);\n const controller = new AbortController();\n conn.pending.add(controller);\n const timeout = Math.min(frame.ms ?? limits.requestTimeoutMs, limits.requestTimeoutMs);\n const timer = setTimeout(() => controller.abort(new RealtimeError(\"timeout\")), timeout);\n void (async () => {\n try {\n const run = Promise.resolve(\n handler({\n channel: name,\n params: sub.params,\n identity: conn.identity,\n event: frame.e,\n data: frame.d,\n signal: controller.signal,\n }),\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n controller.signal.addEventListener(\"abort\", () => reject(controller.signal.reason), {\n once: true,\n });\n });\n const reply = (await Promise.race([run, aborted])) ?? null;\n const text = JSON.stringify(reply);\n if (text === undefined || byteLength(text) > limits.maxMessageBytes)\n return fail(conn, n, \"too_large\", name, __DEV__ ? \"the reply is too large\" : undefined);\n send(conn, `{\"t\":\"rep\",\"n\":${n},\"d\":${text}}`);\n } catch (error) {\n if (error instanceof RealtimeError)\n fail(\n conn,\n n,\n error.code,\n name,\n error.code === \"timeout\" || error.code === \"closed\" ? undefined : error.message,\n );\n else {\n report(error, { where: \"request\", channel: name, connection: conn.id });\n fail(conn, n, \"handler_error\", name);\n }\n } finally {\n clearTimeout(timer);\n conn.pending.delete(controller);\n settle(conn);\n }\n })();\n }\n\n /** Answers a frame that cannot be handled; a peer that keeps sending them is dropped. */\n function refuseFrame(\n conn: ConnState,\n n: number | undefined,\n code: RealtimeError[\"code\"],\n why?: string,\n ): void {\n fail(conn, n, code, undefined, why);\n if (conn.invalid.hit(now())) drop(conn, 1008, __DEV__ ? \"too many invalid frames\" : \"\");\n }\n\n async function handle(conn: ConnState, text: string): Promise<void> {\n const decoded = decodeClientFrame(text, limits.maxMessageBytes);\n if (!decoded.ok) {\n refuseFrame(conn, decoded.n, decoded.code, decoded.why);\n return;\n }\n const frame = decoded.frame;\n const n = \"n\" in frame ? frame.n : undefined;\n // An `unsub` spends a token but is never refused: the client cannot tell a refused one (the answer names no frame), and would\n // believe it left a channel the hub still holds, so its next `sub` of that channel would be a conflict.\n if (\n (frame.t === \"sub\" || frame.t === \"unsub\") &&\n !conn.controlBucket.take(now()) &&\n frame.t === \"sub\"\n )\n return limit(conn, n, frame.c);\n try {\n switch (frame.t) {\n case \"hello\":\n return;\n case \"ping\":\n sendFrame(conn, { t: \"pong\", ...(frame.n === undefined ? {} : { n: frame.n }) });\n return;\n case \"sub\":\n return await subscribe(conn, frame);\n case \"unsub\": {\n const sub = conn.subs.get(frame.c);\n if (sub === undefined) return fail(conn, frame.n, \"not_subscribed\", frame.c);\n removeSub(sub);\n return;\n }\n case \"pub\":\n return await publishFrame(conn, frame);\n case \"req\":\n return await request(conn, frame);\n case \"pres\":\n return await setPresence(conn, frame);\n }\n } catch (error) {\n const channel = \"c\" in frame ? frame.c : undefined;\n if (error instanceof RealtimeError) fail(conn, n, error.code, channel);\n else {\n report(error, {\n where: \"frame\",\n connection: conn.id,\n ...(channel === undefined ? {} : { channel }),\n });\n fail(conn, n, \"unavailable\", channel);\n }\n }\n }\n\n async function run(conn: ConnState): Promise<void> {\n if (conn.running) return;\n conn.running = true;\n try {\n for (let text = conn.queue.shift(); text !== undefined; text = conn.queue.shift()) {\n if (conn.closed) break;\n try {\n await handle(conn, text);\n } catch (error) {\n if (error instanceof RealtimeError) fail(conn, undefined, error.code);\n else {\n report(error, { where: \"frame\", connection: conn.id });\n fail(conn, undefined, \"unavailable\");\n }\n }\n }\n } finally {\n conn.running = false;\n settle(conn);\n }\n }\n\n /** Wakes the callers of `settled` when the connection has nothing left to do. */\n function settle(conn: ConnState): void {\n if (conn.running || conn.queue.length > 0 || conn.pending.size > 0) return;\n for (const wake of conn.waiters.splice(0)) wake();\n }\n\n async function closeConnection(conn: ConnState): Promise<void> {\n if (conn.closed) return conn.done;\n conn.closed = true;\n if (conns.get(conn.id) === conn) conns.delete(conn.id);\n for (const controller of conn.pending) controller.abort(new RealtimeError(\"closed\"));\n conn.pending.clear();\n conn.queue.length = 0;\n for (const wake of conn.waiters.splice(0)) wake();\n const subs = [...conn.subs.values()];\n for (const sub of subs) {\n sub.active = false;\n sub.channel.subs.delete(sub);\n }\n conn.subs.clear();\n await Promise.allSettled(subs.map((sub) => releaseSub(sub)));\n conn.release?.();\n }\n\n // --- presence heartbeat --------------------------------------------------------------------------------------------\n\n function startHeartbeat(): void {\n if (heartbeat !== undefined || closing) return;\n const timer = setInterval(() => void beat(), limits.presenceHeartbeatMs);\n // Node's timer can be unref'd so an idle hub does not keep the process alive; other runtimes return a number.\n (timer as unknown as { unref?: () => void }).unref?.();\n heartbeat = timer;\n }\n\n async function beat(): Promise<void> {\n if (beating) return;\n beating = true;\n try {\n await beatOnce();\n } finally {\n beating = false;\n }\n }\n\n async function beatOnce(): Promise<void> {\n const refs: { channel: string; id: string }[] = [];\n const watched = new Set<string>();\n for (const conn of conns.values())\n for (const sub of conn.subs.values()) {\n if (sub.joined) refs.push({ channel: sub.channel.name, id: conn.id });\n if (sub.joined || sub.watch) watched.add(sub.channel.name);\n }\n if (refs.length === 0 && watched.size === 0) {\n clearInterval(heartbeat);\n heartbeat = undefined;\n return;\n }\n try {\n const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);\n for (const ref of missing) {\n const sub = conns.get(ref.id)?.subs.get(ref.channel);\n if (sub === undefined || !sub.joined) continue;\n await adapter.presenceSet(\n ref.channel,\n {\n id: ref.id,\n ...(sub.conn.identity.user === undefined ? {} : { u: sub.conn.identity.user }),\n s: sub.state,\n v: sub.version,\n },\n limits.presenceTtlMs,\n );\n }\n for (const channel of watched) await adapter.presenceReap(channel);\n } catch (error) {\n report(error, { where: \"presenceHeartbeat\" });\n }\n }\n\n // --- the hub -------------------------------------------------------------------------------------------------------\n\n for (const [pattern, rule] of Object.entries(options.channels ?? {}))\n rules.push(compileRule(pattern, rule));\n\n const core: HubCore = {\n limits,\n adapter,\n report,\n match,\n allowed,\n\n channel(pattern, rule) {\n rules.push(compileRule(pattern, rule));\n return core;\n },\n\n connect(transport, identity = {}, connectOptions = {}) {\n if (closing) throw new RealtimeError(\"closed\", __DEV__ ? \"the hub is closed\" : undefined);\n let release: () => void = () => undefined;\n const done = new Promise<void>((resolve) => {\n release = resolve;\n });\n const owner =\n connectOptions.actingFor === undefined ? undefined : conns.get(connectOptions.actingFor);\n const acting =\n owner !== undefined && identity.user !== undefined && owner.identity.user === identity.user;\n const conn: ConnState = {\n id: acting ? (connectOptions.actingFor as string) : randomId(),\n identity,\n transport,\n subs: new Map(),\n queue: [],\n publishBucket:\n connectOptions.publishBucket ??\n new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),\n // The refill is the publish rate; the burst always covers a connection declaring all its channels at once.\n controlBucket: new TokenBucket(\n limits.publishesPerSecond,\n Math.max(limits.publishBurst, limits.maxChannelsPerConnection),\n now(),\n ),\n stateless: connectOptions.stateless === true,\n waiters: [],\n invalid: new WindowCounter(10, 60_000),\n limited: new WindowCounter(50, 10_000),\n pending: new Set(),\n running: false,\n closed: false,\n release,\n done,\n };\n // An acting connection shares its owner's id: it must not replace the owner in the table.\n if (!acting) conns.set(conn.id, conn);\n sendFrame(conn, {\n t: \"ready\",\n v: PROTOCOL_VERSION,\n id: conn.id,\n hb: limits.heartbeatMs,\n max: limits.maxMessageBytes,\n });\n return {\n id: conn.id,\n identity,\n done,\n receive(text) {\n if (conn.closed) return;\n // Judged before it takes a place in the queue: a frame over the limit can never be handled, so it must not be able to\n // fill the queue (or be held until its turn) however many arrive.\n if (byteLength(text) > limits.maxMessageBytes) {\n refuseFrame(conn, undefined, \"too_large\");\n return;\n }\n if (conn.queue.length >= limits.maxQueuedFrames) {\n drop(conn, 1013, __DEV__ ? \"too many frames waiting\" : \"\");\n return;\n }\n conn.queue.push(text);\n void run(conn);\n },\n closed() {\n void closeConnection(conn);\n },\n settled() {\n return new Promise<void>((resolve) => {\n conn.waiters.push(resolve);\n settle(conn);\n });\n },\n };\n },\n\n async publish(channel, event, data, publishOptions = {}) {\n if (!isChannelName(channel))\n throw new RealtimeError(\n \"bad_channel\",\n __DEV__ ? `\"${channel}\" is not a valid channel name` : undefined,\n );\n if (!isEventName(event))\n throw new RealtimeError(\n \"bad_frame\",\n __DEV__ ? `\"${event}\" is not a valid event name` : undefined,\n );\n if (publishOptions.key !== undefined && !isIdempotencyKey(publishOptions.key))\n throw new RealtimeError(\n \"bad_frame\",\n __DEV__ ? \"the idempotency key is not valid\" : undefined,\n );\n let text: string | undefined;\n try {\n text = JSON.stringify(data === undefined ? null : data);\n } catch (error) {\n throw new TypeError(__DEV__ ? \"the data is not JSON\" : \"REALTIME_DATA\", {\n cause: error,\n });\n }\n if (text === undefined)\n throw new TypeError(__DEV__ ? \"the data is not JSON\" : \"REALTIME_DATA\");\n if (byteLength(text) > limits.maxMessageBytes)\n throw new RealtimeError(\"too_large\", __DEV__ ? \"the message is too large\" : undefined);\n const found = match(channel);\n const result = await adapter.publish(\n channel,\n { event, data: text },\n {\n history: found?.rule.history ?? limits.history,\n historyTtlMs: limits.historyTtlMs,\n idempotencyTtlMs: limits.idempotencyTtlMs,\n ...(publishOptions.key === undefined ? {} : { key: publishOptions.key }),\n },\n );\n counters.published += result.duplicate ? 0 : 1;\n return result;\n },\n\n async setMemberState(identity, channel, connectionId, state) {\n if (identity.user === undefined)\n throw new RealtimeError(\n \"unauthorized\",\n __DEV__ ? \"presence changes over HTTP need a signed-in user\" : undefined,\n );\n const found = match(channel);\n if (\n found === undefined ||\n !(await allowed(found.rule, \"presence\", channel, found.params, identity))\n )\n throw new RealtimeError(\"forbidden\");\n if (byteLength(JSON.stringify(state)) > limits.maxStateBytes)\n throw new RealtimeError(\n \"too_large\",\n __DEV__ ? \"the presence state is too large\" : undefined,\n );\n const members = await adapter.presenceList(channel);\n const member = members.find((item) => item.id === connectionId);\n if (member === undefined || member.u !== identity.user)\n throw new RealtimeError(\"forbidden\", __DEV__ ? \"that connection is not yours\" : undefined);\n // The connection may live on this server: its own copy of the member is what the heartbeat writes back after an expiry, so\n // it moves with the stored one (the same for a connection on another server, through the presence event below).\n const local = conns.get(connectionId)?.subs.get(channel);\n const version = Math.max(member.v, local?.version ?? 0) + 1;\n if (local?.joined === true) {\n local.version = version;\n local.state = state;\n }\n await adapter.presenceSet(\n channel,\n { id: member.id, u: member.u, s: state, v: version },\n limits.presenceTtlMs,\n );\n },\n\n revoke(target) {\n if (\n target.user === undefined &&\n target.connection === undefined &&\n target.channel === undefined\n )\n throw new TypeError(\n __DEV__ ? \"revoke needs a user, a connection or a channel.\" : \"REALTIME_REVOKE\",\n );\n let count = 0;\n for (const conn of [...conns.values()]) {\n if (target.connection !== undefined && conn.id !== target.connection) continue;\n if (target.user !== undefined && conn.identity.user !== target.user) continue;\n for (const sub of [...conn.subs.values()]) {\n if (target.channel !== undefined && sub.channel.name !== target.channel) continue;\n removeSub(sub, \"revoked\");\n count += 1;\n }\n }\n return count;\n },\n\n stats() {\n let subscriptions = 0;\n for (const conn of conns.values()) subscriptions += conn.subs.size;\n return {\n connections: conns.size,\n channels: chans.size,\n subscriptions,\n ...counters,\n };\n },\n\n async close() {\n if (closing) return;\n closing = true;\n if (heartbeat !== undefined) clearInterval(heartbeat);\n heartbeat = undefined;\n const all = [...conns.values()];\n for (const conn of all) drop(conn, 1001, __DEV__ ? \"going away\" : \"\");\n await Promise.all(all.map((conn) => conn.done));\n await Promise.allSettled([...releasing]);\n if (ownsAdapter) await adapter.close();\n },\n };\n return core;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createHub","core","createHubCore","options","hub","pattern","rule","websocketRoute","routeOptions","sseRoute","postRoute","DEFAULT_HUB_LIMITS","memoryAdapter"],"sources":["../../../src/server/index.ts"],"sourcesContent":["import {\n type HubRouteOptions,\n postRoute,\n type RequestLike,\n type SseLike,\n sseRoute,\n type WebSocketRoute,\n websocketRoute,\n} from \"./http\";\nimport { createHubCore, type HubCore, type HubOptions } from \"./hub\";\nimport type { Identity } from \"./rules\";\n\nexport type { Adapter } from \"../adapter\";\nexport { memoryAdapter } from \"../memory\";\nexport type {\n AuthenticateResult,\n HubRouteOptions,\n RequestLike,\n SseLike,\n WebSocketLike,\n WebSocketRoute,\n} from \"./http\";\nexport type {\n ConnectOptions,\n ErrorContext,\n HubConnection,\n HubOptions,\n HubStats,\n RevokeTarget,\n Transport,\n} from \"./hub\";\nexport type { HubLimits } from \"./limits\";\nexport { DEFAULT_HUB_LIMITS } from \"./limits\";\nexport type {\n ChannelRule,\n Identity,\n Permission,\n RequestContext,\n RequestHandler,\n RuleContext,\n} from \"./rules\";\n\n/**\n * The server object of `@shadow-js/realtime`: connections, channel rules, limits and an adapter, with the routes that\n * connect it to a transport.\n * @since 1.0.0\n */\nexport interface Hub extends HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub;\n /** The WebSocket route: `export const WS = hub.websocket()`. */\n websocket<E extends RequestLike = RequestLike>(\n options?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E>;\n /** The server-sent events route: `export const SSE = hub.sse()`. */\n sse<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E>,\n ): (event: E, stream: SseLike) => Promise<void>;\n /** The HTTP route for what SSE clients send: `export const POST = hub.post()`. */\n post<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string },\n ): (event: E) => Promise<Response>;\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules (default deny), enforces the\n * limits, and talks to one adapter (in memory by default; Redis or Cloudflare when several servers share channels).\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: {\n * \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },\n * },\n * });\n * await hub.publish(\"chat:lobby\", \"message\", { text: \"hello\" });\n * await hub.close();\n * @since 1.0.0\n */\nexport function createHub(options: HubOptions = {}): Hub {\n const core = createHubCore(options);\n const hub: Hub = Object.assign(core, {\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub {\n core.channel(pattern, rule);\n return hub;\n },\n websocket: <E extends RequestLike = RequestLike>(\n routeOptions?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E> => websocketRoute<E>(core, routeOptions),\n sse: <E extends RequestLike = RequestLike>(routeOptions?: HubRouteOptions<E>) =>\n sseRoute<E>(core, routeOptions),\n post: <E extends RequestLike = RequestLike>(\n routeOptions?: HubRouteOptions<E> & {\n readonly key?: (event: E, identity: Identity) => string;\n },\n ) => postRoute<E>(core, routeOptions),\n });\n return hub;\n}\n"],"mappings":"A,+MAmFA,SAAgBA,EAAU,EAAsB,CAAC,EAAQ,CACvD,IAAMC,EAAOC,EAAcC,CAAO,EAC5BC,EAAW,OAAO,OAAOH,EAAM,CACnC,QAAQ,EAAiB,EAA0C,CAEjE,OADAA,EAAK,QAAQI,EAASC,CAAI,EACnBF,CACT,EACA,UACE,GACsBG,EAAkBN,EAAMO,CAAY,EAC5D,IAA2C,GACzCC,EAAYR,EAAMO,CAAY,EAChC,KACE,GAGGE,EAAaT,EAAMO,CAAY,CACtC,CAAC,EACD,OAAOJ,CACT,C"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["createHub","core","createHubCore","options","hub","pattern","rule","websocketRoute","routeOptions","sseRoute","postRoute","DEFAULT_HUB_LIMITS","memoryAdapter"],"sources":["../../../src/server/index.ts"],"sourcesContent":["import {\n type HubRouteOptions,\n postRoute,\n type RequestLike,\n type SseLike,\n sseRoute,\n type WebSocketRoute,\n websocketRoute,\n} from \"./http\";\nimport { createHubCore, type HubCore, type HubOptions } from \"./hub\";\nimport type { Identity } from \"./rules\";\n\nexport type { Adapter } from \"../adapter\";\nexport { type MemoryAdapterOptions, type MemoryErrorContext, memoryAdapter } from \"../memory\";\nexport type {\n AuthenticateResult,\n HubRouteOptions,\n RequestLike,\n SseLike,\n WebSocketLike,\n WebSocketRoute,\n} from \"./http\";\nexport type {\n ConnectOptions,\n ErrorContext,\n HubConnection,\n HubOptions,\n HubStats,\n RevokeTarget,\n Transport,\n} from \"./hub\";\nexport type { HubLimits } from \"./limits\";\nexport { DEFAULT_HUB_LIMITS } from \"./limits\";\nexport type {\n ChannelRule,\n Identity,\n Permission,\n RequestContext,\n RequestHandler,\n RuleContext,\n} from \"./rules\";\n\n/**\n * The server object of `@shadow-js/realtime`: connections, channel rules, limits and an adapter, with the routes that\n * connect it to a transport.\n * @since 1.0.0\n */\nexport interface Hub extends HubCore {\n /** Adds a channel rule after the ones already registered. */\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub;\n /** The WebSocket route: `export const WS = hub.websocket()`. */\n websocket<E extends RequestLike = RequestLike>(\n options?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E>;\n /** The server-sent events route: `export const SSE = hub.sse()`. */\n sse<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E>,\n ): (event: E, stream: SseLike) => Promise<void>;\n /** The HTTP route for what SSE clients send: `export const POST = hub.post()`. */\n post<E extends RequestLike = RequestLike>(\n options?: HubRouteOptions<E> & { readonly key?: (event: E, identity: Identity) => string },\n ): (event: E) => Promise<Response>;\n}\n\n/**\n * Creates the hub: the server side of `@shadow-js/realtime`. It owns connections and channel rules (default deny), enforces the\n * limits, and talks to one adapter (in memory by default; Redis or Cloudflare when several servers share channels).\n *\n * @param options The adapter, the channel rules and the limits.\n * @returns The hub.\n * @throws RangeError for a limit that is not valid; TypeError for an invalid channel pattern.\n * @example\n * import { createHub } from \"@shadow-js/realtime/server\";\n *\n * const hub = createHub({\n * channels: {\n * \"chat:{room}\": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },\n * },\n * });\n * await hub.publish(\"chat:lobby\", \"message\", { text: \"hello\" });\n * await hub.close();\n * @since 1.0.0\n */\nexport function createHub(options: HubOptions = {}): Hub {\n const core = createHubCore(options);\n const hub: Hub = Object.assign(core, {\n channel(pattern: string, rule: import(\"./rules\").ChannelRule): Hub {\n core.channel(pattern, rule);\n return hub;\n },\n websocket: <E extends RequestLike = RequestLike>(\n routeOptions?: Pick<HubRouteOptions<E>, \"authenticate\">,\n ): WebSocketRoute<E> => websocketRoute<E>(core, routeOptions),\n sse: <E extends RequestLike = RequestLike>(routeOptions?: HubRouteOptions<E>) =>\n sseRoute<E>(core, routeOptions),\n post: <E extends RequestLike = RequestLike>(\n routeOptions?: HubRouteOptions<E> & {\n readonly key?: (event: E, identity: Identity) => string;\n },\n ) => postRoute<E>(core, routeOptions),\n });\n return hub;\n}\n"],"mappings":"A,+MAmFA,SAAgBA,EAAU,EAAsB,CAAC,EAAQ,CACvD,IAAMC,EAAOC,EAAcC,CAAO,EAC5BC,EAAW,OAAO,OAAOH,EAAM,CACnC,QAAQ,EAAiB,EAA0C,CAEjE,OADAA,EAAK,QAAQI,EAASC,CAAI,EACnBF,CACT,EACA,UACE,GACsBG,EAAkBN,EAAMO,CAAY,EAC5D,IAA2C,GACzCC,EAAYR,EAAMO,CAAY,EAChC,KACE,GAGGE,EAAaT,EAAMO,CAAY,CACtC,CAAC,EACD,OAAOJ,CACT,C"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=Object.freeze({maxMessageBytes:65536,maxChannelsPerConnection:64,maxSubscribersPerChannel:5e4,maxPresenceMembers:1e3,maxStateBytes:1024,maxPendingRequests:32,maxQueuedFrames:256,maxBufferedBytes:1048576,publishesPerSecond:20,publishBurst:40,channelPublishesPerSecond:500,requestTimeoutMs:1e4,history:100,historyTtlMs:3e5,idempotencyTtlMs:6e4,presenceTtlMs:45e3,presenceHeartbeatMs:15e3,heartbeatMs:3e4}),t=1e4;function n(
|
|
1
|
+
const e=Object.freeze({maxMessageBytes:65536,maxChannelsPerConnection:64,maxSubscribersPerChannel:5e4,maxPresenceMembers:1e3,maxStateBytes:1024,maxPendingRequests:32,maxQueuedFrames:256,maxBufferedBytes:1048576,publishesPerSecond:20,publishBurst:40,channelPublishesPerSecond:500,requestTimeoutMs:1e4,history:100,historyTtlMs:3e5,idempotencyTtlMs:6e4,presenceTtlMs:45e3,presenceHeartbeatMs:15e3,heartbeatMs:3e4}),t=1e4;function n(t){let n={...e};for(let[r,i]of Object.entries(t??{})){if(!(r in e))throw TypeError(`REALTIME_LIMIT_2`);if(i===void 0)continue;let t=r===`history`?0:1;if(typeof i!=`number`||!Number.isInteger(i)||i<t)throw RangeError(`REALTIME_LIMIT_4`);n[r]=i}if(n.history>1e4)throw RangeError(`REALTIME_LIMIT_1`);if(n.presenceTtlMs<=n.presenceHeartbeatMs)throw RangeError(`REALTIME_LIMIT_3`);return n}var r=class{#e;#t;#n;#r;constructor(e,t,n){this.#n=e,this.#r=t,this.#e=t,this.#t=n}take(e){let t=Math.max(0,e-this.#t);return this.#t=e,this.#e=Math.min(this.#r,this.#e+t*this.#n/1e3),this.#e<1?!1:(--this.#e,!0)}},i=class{#e=[];#t;#n;constructor(e,t){this.#t=e,this.#n=t}hit(e){this.#e.push(e);let t=e-this.#n;for(;this.#e.length>0&&this.#e[0]<=t;)this.#e.shift();return this.#e.length>=this.#t}};export{e as DEFAULT_HUB_LIMITS,t as MAX_HISTORY,r as TokenBucket,i as WindowCounter,n as resolveLimits};
|
|
2
2
|
//# sourceMappingURL=limits.js.map
|