dynamodb-reactive 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/{client-wZQ9XC9S.d.ts → client-BPAct-Dz.d.ts} +8 -0
- package/dist/client.cjs +15 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.cjs +165 -44
- package/dist/cloudflare.cjs.map +1 -1
- package/dist/cloudflare.d.cts +64 -8
- package/dist/cloudflare.d.ts +64 -8
- package/dist/cloudflare.js +164 -45
- package/dist/cloudflare.js.map +1 -1
- package/dist/cluster.cjs +15 -0
- package/dist/cluster.cjs.map +1 -1
- package/dist/cluster.js +15 -0
- package/dist/cluster.js.map +1 -1
- package/dist/hub.cjs +27 -5
- package/dist/hub.cjs.map +1 -1
- package/dist/hub.d.cts +15 -2
- package/dist/hub.d.ts +15 -2
- package/dist/hub.js +27 -5
- package/dist/hub.js.map +1 -1
- package/dist/react.cjs +15 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +15 -0
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/hub.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../client/src/hub.ts"],"names":[],"mappings":"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","file":"hub.cjs","sourcesContent":["import type {\n JsonPatch,\n SnapshotResponse,\n SubscribeRequest,\n UnsubscribeRequest,\n WebSocketEvent,\n} from './protocol.js';\n\n/**\n * Protocol core for a reactive hub: the server half of the wire protocol for\n * backends that hold WebSockets themselves (for example a Cloudflare Durable\n * Object). It keeps every piece of per-socket state on the socket's attachment so\n * the state survives hibernation, and it has no runtime imports, so it runs and\n * tests in Node.\n */\n\n/** Hibernation limits a socket attachment to 2,048 bytes of serialized JSON. */\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 2_048;\n\n/** One subscription held by a socket. */\nexport interface HubSubscription {\n subscriptionId: string;\n generation: number;\n path: string;\n}\n\n/** State kept on each socket's attachment. */\nexport interface HubSocketState {\n connectionId: string;\n principal: string;\n subscriptions: HubSubscription[];\n /** Epoch ms the socket was accepted; the liveness baseline before its first ping. */\n connectedAt?: number;\n}\n\n/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */\nexport interface HubSocket {\n send(message: string): void;\n deserializeAttachment(): unknown;\n serializeAttachment(value: unknown): void;\n}\n\nexport interface HubSockets {\n all(): HubSocket[];\n byConnectionId(connectionId: string): HubSocket | undefined;\n}\n\n/** Monotonic per-path versions. Versions only need to grow while sockets stay open. */\nexport interface HubVersions {\n get(path: string): Promise<number>;\n set(path: string, version: number): Promise<void>;\n}\n\nexport interface HubPathContext {\n path: string;\n principal: string;\n input: unknown;\n}\n\nexport interface HubPath {\n /**\n * Runs before the subscription is stored. Return false to reject it. Every\n * subscriber of a path receives its pushes, so authorise here, not in `snapshot`.\n */\n authorize?(context: HubPathContext): boolean | Promise<boolean>;\n /** Current data for the path, sent as the subscription snapshot. */\n snapshot(context: HubPathContext): unknown;\n}\n\n/** Fixed paths by name, or a resolver for parameterised paths such as `drafts.<id>`. */\nexport type HubPaths = Record<string, HubPath> | ((path: string) => HubPath | undefined);\n\nexport interface ReactiveHubOptions {\n sockets: HubSockets;\n versions: HubVersions;\n paths: HubPaths;\n /** Rejects subscribes that would grow an attachment past this size. */\n maxAttachmentBytes?: number;\n}\n\n/** Thrown for requests the caller is not allowed to make. */\nexport class ReactiveHubError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ReactiveHubError';\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a reactive HTTP body for the subscribe and unsubscribe requests a hub serves. */\nexport function isHubRequest(value: unknown): value is SubscribeRequest | UnsubscribeRequest {\n if (!isRecord(value)) return false;\n return (\n (value.type === 'subscribe' || value.type === 'unsubscribe') &&\n typeof value.connectionId === 'string' &&\n typeof value.subscriptionId === 'string' &&\n typeof value.generation === 'number' &&\n (value.type === 'unsubscribe' || typeof value.path === 'string')\n );\n}\n\n/** Returns the bearer token from an `Authorization` header, or an empty string. */\nexport function bearerToken(request: Request): string {\n const match = /^Bearer ([^\\s]+)$/u.exec(request.headers.get('authorization') ?? '');\n return match?.[1] ?? '';\n}\n\n/** Initial attachment for a newly accepted socket. */\nexport function createHubSocketState(\n connectionId: string,\n principal: string,\n connectedAt?: number,\n): HubSocketState {\n return {\n connectionId,\n principal,\n subscriptions: [],\n ...(connectedAt === undefined ? {} : { connectedAt }),\n };\n}\n\nfunction socketState(socket: HubSocket): HubSocketState {\n const state = socket.deserializeAttachment() as HubSocketState;\n // Attachments written before subscriptions carried a path have no path; they\n // are dropped rather than fanned out to every notification.\n return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };\n}\n\nfunction send(socket: HubSocket, event: WebSocketEvent): void {\n try {\n socket.send(JSON.stringify(event));\n } catch {\n // A socket closing mid-broadcast is cleaned up by its close handler.\n }\n}\n\n/** Creates the protocol core for one hub instance. */\nexport function createReactiveHub(options: ReactiveHubOptions) {\n const { sockets, versions } = options;\n const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;\n const resolve = (path: string): HubPath | undefined =>\n typeof options.paths === 'function'\n ? options.paths(path)\n : Object.hasOwn(options.paths, path)\n ? options.paths[path]\n : undefined;\n\n const ownedSocket = (connectionId: string, principal: string): HubSocket | undefined => {\n const socket = sockets.byConnectionId(connectionId);\n // The HTTP caller must own the WebSocket it registers against.\n return socket && socketState(socket).principal === principal ? socket : undefined;\n };\n\n const subscribersOf = (path: string) =>\n sockets.all().flatMap((socket) =>\n socketState(socket)\n .subscriptions.filter((subscription) => subscription.path === path)\n .map((subscription) => ({ socket, subscription })),\n );\n\n const bump = async (path: string) => {\n const fromVersion = await versions.get(path);\n const toVersion = fromVersion + 1;\n await versions.set(path, toVersion);\n return { fromVersion, toVersion };\n };\n\n return {\n /** Answers the client's `init` and `ping` frames. */\n message(socket: HubSocket, raw: string): void {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return;\n }\n const type = isRecord(frame) ? frame.type : undefined;\n if (type === 'init') {\n send(socket, { type: 'connected', connectionId: socketState(socket).connectionId });\n } else if (type === 'ping') {\n send(socket, { type: 'pong' });\n }\n },\n\n async subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse> {\n const handler = resolve(request.path);\n if (!handler) throw new ReactiveHubError('Unknown subscription path');\n if (!ownedSocket(request.connectionId, principal)) {\n throw new ReactiveHubError('Unknown connection');\n }\n const context: HubPathContext = { path: request.path, principal, input: request.input };\n if (handler.authorize && !(await handler.authorize(context))) {\n throw new ReactiveHubError('Subscription not allowed');\n }\n // Re-read after the awaits so a concurrent subscribe on the same socket is kept.\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) throw new ReactiveHubError('Unknown connection');\n const state = socketState(socket);\n const next: HubSocketState = {\n ...state,\n subscriptions: [\n ...state.subscriptions.filter((entry) => entry.subscriptionId !== request.subscriptionId),\n {\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n path: request.path,\n },\n ],\n };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Too many subscriptions on this connection');\n }\n socket.serializeAttachment(next);\n // Read the version before the data so a concurrent notify can only make the\n // client re-fetch, never miss a change.\n const version = await versions.get(request.path);\n const data = await handler.snapshot(context);\n return {\n type: 'snapshot',\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n version,\n data,\n };\n },\n\n unsubscribe(request: UnsubscribeRequest, principal: string): void {\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) return;\n const state = socketState(socket);\n socket.serializeAttachment({\n ...state,\n subscriptions: state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n } satisfies HubSocketState);\n },\n\n /** Bumps the path's version and asks each of its subscribers to re-fetch. */\n async notify(path: string): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'resync',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n });\n }\n },\n\n /**\n * Bumps the path's version and sends the same JSON patches to each subscriber.\n * Use it only when every subscriber of the path sees the same data.\n */\n async publish(path: string, patches: JsonPatch[]): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'patch',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n patches,\n });\n }\n },\n };\n}\n\nexport type ReactiveHub = ReturnType<typeof createReactiveHub>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../client/src/hub.ts"],"names":[],"mappings":";;;AAiBO,IAAM,4BAAA,GAA+B;AAkErC,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA,EAC1C,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AAAA,EACd;AACF;AAEA,SAAS,SAAS,KAAA,EAAkD;AAClE,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA;AAChD;AAGO,SAAS,aAAa,KAAA,EAAgE;AAC3F,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,KAAA;AAC7B,EAAA,OAAA,CACG,KAAA,CAAM,SAAS,WAAA,IAAe,KAAA,CAAM,SAAS,aAAA,KAC9C,OAAO,KAAA,CAAM,YAAA,KAAiB,QAAA,IAC9B,OAAO,MAAM,cAAA,KAAmB,QAAA,IAChC,OAAO,KAAA,CAAM,UAAA,KAAe,QAAA,KAC3B,MAAM,IAAA,KAAS,aAAA,IAAiB,OAAO,KAAA,CAAM,IAAA,KAAS,QAAA,CAAA;AAE3D;AAGO,SAAS,YAAY,OAAA,EAA0B;AACpD,EAAA,MAAM,KAAA,GAAQ,qBAAqB,IAAA,CAAK,OAAA,CAAQ,QAAQ,GAAA,CAAI,eAAe,KAAK,EAAE,CAAA;AAClF,EAAA,OAAO,KAAA,GAAQ,CAAC,CAAA,IAAK,EAAA;AACvB;AAGO,SAAS,oBAAA,CACd,YAAA,EACA,SAAA,EACA,WAAA,EACgB;AAChB,EAAA,OAAO;AAAA,IACL,YAAA;AAAA,IACA,SAAA;AAAA,IACA,eAAe,EAAC;AAAA,IAChB,GAAI,WAAA,KAAgB,MAAA,GAAY,EAAC,GAAI,EAAE,WAAA;AAAY,GACrD;AACF;AAEA,SAAS,YAAY,MAAA,EAAmC;AACtD,EAAA,MAAM,KAAA,GAAQ,OAAO,qBAAA,EAAsB;AAG3C,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,aAAA,EAAe,KAAA,CAAM,aAAA,CAAc,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,IAAI,CAAA,EAAE;AACtF;AAEA,SAAS,IAAA,CAAK,QAAmB,KAAA,EAA6B;AAC5D,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,EACnC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAGO,SAAS,kBAAkB,OAAA,EAA6B;AAC7D,EAAA,MAAM,EAAE,OAAA,EAAS,QAAA,EAAS,GAAI,OAAA;AAC9B,EAAA,MAAM,kBAAA,GAAqB,QAAQ,kBAAA,IAAsB,4BAAA;AACzD,EAAA,MAAM,OAAA,GAAU,CAAC,IAAA,KACf,OAAO,QAAQ,KAAA,KAAU,UAAA,GACrB,QAAQ,KAAA,CAAM,IAAI,IAClB,MAAA,CAAO,MAAA,CAAO,QAAQ,KAAA,EAAO,IAAI,IAC/B,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,GAClB,MAAA;AAER,EAAA,MAAM,WAAA,GAAc,CAAC,YAAA,EAAsB,SAAA,KAA6C;AACtF,IAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,cAAA,CAAe,YAAY,CAAA;AAElD,IAAA,OAAO,UAAU,WAAA,CAAY,MAAM,CAAA,CAAE,SAAA,KAAc,YAAY,MAAA,GAAS,MAAA;AAAA,EAC1E,CAAA;AAEA,EAAA,MAAM,aAAA,GAAgB,CAAC,IAAA,KACrB,OAAA,CAAQ,KAAI,CAAE,OAAA;AAAA,IAAQ,CAAC,MAAA,KACrB,WAAA,CAAY,MAAM,CAAA,CACf,aAAA,CAAc,OAAO,CAAC,YAAA,KAAiB,aAAa,IAAA,KAAS,IAAI,EACjE,GAAA,CAAI,CAAC,kBAAkB,EAAE,MAAA,EAAQ,cAAa,CAAE;AAAA,GACrD;AAEF,EAAA,MAAM,IAAA,GAAO,OAAO,IAAA,KAAiB;AACnC,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA;AAC3C,IAAA,MAAM,YAAY,WAAA,GAAc,CAAA;AAChC,IAAA,MAAM,QAAA,CAAS,GAAA,CAAI,IAAA,EAAM,SAAS,CAAA;AAClC,IAAA,OAAO,EAAE,aAAa,SAAA,EAAU;AAAA,EAClC,CAAA;AAEA,EAAA,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKL,OAAA,CACE,QACA,GAAA,EAC0D;AAC1D,MAAA,IAAI,KAAA;AACJ,MAAA,IAAI;AACF,QAAA,KAAA,GAAQ,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,MACxB,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,IAAI,CAAC,SAAS,KAAK,CAAA,IAAK,OAAO,KAAA,CAAM,IAAA,KAAS,UAAU,OAAO,MAAA;AAC/D,MAAA,IAAI,KAAA,CAAM,SAAS,MAAA,EAAQ;AACzB,QAAA,IAAA,CAAK,MAAA,EAAQ,EAAE,IAAA,EAAM,WAAA,EAAa,cAAc,WAAA,CAAY,MAAM,CAAA,CAAE,YAAA,EAAc,CAAA;AAAA,MACpF,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,MAAA,EAAQ;AAChC,QAAA,IAAA,CAAK,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA;AAAA,MAC/B,CAAA,MAAO;AACL,QAAA,OAAO,KAAA;AAAA,MACT;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA;AAAA,IAGA,WAAwB,MAAA,EAAkC;AACxD,MAAA,OAAO,WAAA,CAAY,MAAM,CAAA,CAAE,IAAA;AAAA,IAC7B,CAAA;AAAA;AAAA,IAGA,aAAA,CAAc,QAAmB,IAAA,EAAqB;AACpD,MAAA,MAAM,OAAuB,EAAE,GAAG,WAAA,CAAY,MAAM,GAAG,IAAA,EAAK;AAC5D,MAAA,IAAI,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,IAAA,CAAK,UAAU,IAAI,CAAC,CAAA,CAAE,MAAA,GAAS,kBAAA,EAAoB;AAC9E,QAAA,MAAM,IAAI,iBAAiB,0BAA0B,CAAA;AAAA,MACvD;AACA,MAAA,MAAA,CAAO,oBAAoB,IAAI,CAAA;AAAA,IACjC,CAAA;AAAA;AAAA,IAGA,YAAY,MAAA,EAA2B;AACrC,MAAA,OAAO,WAAA,CAAY,MAAM,CAAA,CAAE,SAAA;AAAA,IAC7B,CAAA;AAAA,IAEA,MAAM,SAAA,CAAU,OAAA,EAA2B,SAAA,EAA8C;AACvF,MAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,IAAI,CAAA;AACpC,MAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,iBAAiB,2BAA2B,CAAA;AACpE,MAAA,IAAI,CAAC,WAAA,CAAY,OAAA,CAAQ,YAAA,EAAc,SAAS,CAAA,EAAG;AACjD,QAAA,MAAM,IAAI,iBAAiB,oBAAoB,CAAA;AAAA,MACjD;AACA,MAAA,MAAM,OAAA,GAA0B,EAAE,IAAA,EAAM,OAAA,CAAQ,MAAM,SAAA,EAAW,KAAA,EAAO,QAAQ,KAAA,EAAM;AACtF,MAAA,IAAI,QAAQ,SAAA,IAAa,CAAE,MAAM,OAAA,CAAQ,SAAA,CAAU,OAAO,CAAA,EAAI;AAC5D,QAAA,MAAM,IAAI,iBAAiB,0BAA0B,CAAA;AAAA,MACvD;AAEA,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,CAAQ,YAAA,EAAc,SAAS,CAAA;AAC1D,MAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,iBAAiB,oBAAoB,CAAA;AAC5D,MAAA,MAAM,KAAA,GAAQ,YAAY,MAAM,CAAA;AAChC,MAAA,MAAM,IAAA,GAAuB;AAAA,QAC3B,GAAG,KAAA;AAAA,QACH,aAAA,EAAe;AAAA,UACb,GAAG,MAAM,aAAA,CAAc,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,cAAA,KAAmB,OAAA,CAAQ,cAAc,CAAA;AAAA,UACxF;AAAA,YACE,gBAAgB,OAAA,CAAQ,cAAA;AAAA,YACxB,YAAY,OAAA,CAAQ,UAAA;AAAA,YACpB,MAAM,OAAA,CAAQ;AAAA;AAChB;AACF,OACF;AACA,MAAA,IAAI,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,IAAA,CAAK,UAAU,IAAI,CAAC,CAAA,CAAE,MAAA,GAAS,kBAAA,EAAoB;AAC9E,QAAA,MAAM,IAAI,iBAAiB,2CAA2C,CAAA;AAAA,MACxE;AACA,MAAA,MAAA,CAAO,oBAAoB,IAAI,CAAA;AAG/B,MAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,GAAA,CAAI,QAAQ,IAAI,CAAA;AAC/C,MAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA;AAC3C,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,UAAA;AAAA,QACN,gBAAgB,OAAA,CAAQ,cAAA;AAAA,QACxB,YAAY,OAAA,CAAQ,UAAA;AAAA,QACpB,OAAA;AAAA,QACA;AAAA,OACF;AAAA,IACF,CAAA;AAAA,IAEA,WAAA,CAAY,SAA6B,SAAA,EAAyB;AAChE,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,CAAQ,YAAA,EAAc,SAAS,CAAA;AAC1D,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,MAAM,KAAA,GAAQ,YAAY,MAAM,CAAA;AAChC,MAAA,MAAA,CAAO,mBAAA,CAAoB;AAAA,QACzB,GAAG,KAAA;AAAA,QACH,aAAA,EAAe,MAAM,aAAA,CAAc,MAAA;AAAA,UACjC,CAAC,KAAA,KAAU,KAAA,CAAM,cAAA,KAAmB,OAAA,CAAQ;AAAA;AAC9C,OACwB,CAAA;AAAA,IAC5B,CAAA;AAAA;AAAA,IAGA,MAAM,OAAO,IAAA,EAA6B;AACxC,MAAA,MAAM,EAAE,WAAA,EAAa,SAAA,EAAU,GAAI,MAAM,KAAK,IAAI,CAAA;AAClD,MAAA,KAAA,MAAW,EAAE,MAAA,EAAQ,YAAA,EAAa,IAAK,aAAA,CAAc,IAAI,CAAA,EAAG;AAC1D,QAAA,IAAA,CAAK,MAAA,EAAQ;AAAA,UACX,IAAA,EAAM,QAAA;AAAA,UACN,gBAAgB,YAAA,CAAa,cAAA;AAAA,UAC7B,YAAY,YAAA,CAAa,UAAA;AAAA,UACzB,WAAA;AAAA,UACA,SAAA;AAAA,UACA,SAAA,EAAW,OAAO,UAAA;AAAW,SAC9B,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,OAAA,CAAQ,IAAA,EAAc,OAAA,EAAqC;AAC/D,MAAA,MAAM,EAAE,WAAA,EAAa,SAAA,EAAU,GAAI,MAAM,KAAK,IAAI,CAAA;AAClD,MAAA,KAAA,MAAW,EAAE,MAAA,EAAQ,YAAA,EAAa,IAAK,aAAA,CAAc,IAAI,CAAA,EAAG;AAC1D,QAAA,IAAA,CAAK,MAAA,EAAQ;AAAA,UACX,IAAA,EAAM,OAAA;AAAA,UACN,gBAAgB,YAAA,CAAa,cAAA;AAAA,UAC7B,YAAY,YAAA,CAAa,UAAA;AAAA,UACzB,WAAA;AAAA,UACA,SAAA;AAAA,UACA,SAAA,EAAW,OAAO,UAAA,EAAW;AAAA,UAC7B;AAAA,SACD,CAAA;AAAA,MACH;AAAA,IACF;AAAA,GACF;AACF","file":"hub.cjs","sourcesContent":["import type {\n JsonPatch,\n SnapshotResponse,\n SubscribeRequest,\n UnsubscribeRequest,\n WebSocketEvent,\n} from './protocol.js';\n\n/**\n * Protocol core for a reactive hub: the server half of the wire protocol for\n * backends that hold WebSockets themselves (for example a Cloudflare Durable\n * Object). It keeps every piece of per-socket state on the socket's attachment so\n * the state survives hibernation, and it has no runtime imports, so it runs and\n * tests in Node.\n */\n\n/** Hibernation limits a socket attachment to 2,048 bytes of serialized JSON. */\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 2_048;\n\n/** One subscription held by a socket. */\nexport interface HubSubscription {\n subscriptionId: string;\n generation: number;\n path: string;\n}\n\n/** State kept on each socket's attachment. */\nexport interface HubSocketState {\n connectionId: string;\n principal: string;\n subscriptions: HubSubscription[];\n /** Epoch ms the socket was accepted; the liveness baseline before its first ping. */\n connectedAt?: number;\n /** Application data kept with the socket, such as what a listener is playing. */\n data?: unknown;\n}\n\n/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */\nexport interface HubSocket {\n send(message: string): void;\n deserializeAttachment(): unknown;\n serializeAttachment(value: unknown): void;\n}\n\nexport interface HubSockets {\n all(): HubSocket[];\n byConnectionId(connectionId: string): HubSocket | undefined;\n}\n\n/** Monotonic per-path versions. Versions only need to grow while sockets stay open. */\nexport interface HubVersions {\n get(path: string): Promise<number>;\n set(path: string, version: number): Promise<void>;\n}\n\nexport interface HubPathContext {\n path: string;\n principal: string;\n input: unknown;\n}\n\nexport interface HubPath {\n /**\n * Runs before the subscription is stored. Return false to reject it. Every\n * subscriber of a path receives its pushes, so authorise here, not in `snapshot`.\n */\n authorize?(context: HubPathContext): boolean | Promise<boolean>;\n /** Current data for the path, sent as the subscription snapshot. */\n snapshot(context: HubPathContext): unknown;\n}\n\n/** Fixed paths by name, or a resolver for parameterised paths such as `drafts.<id>`. */\nexport type HubPaths = Record<string, HubPath> | ((path: string) => HubPath | undefined);\n\nexport interface ReactiveHubOptions {\n sockets: HubSockets;\n versions: HubVersions;\n paths: HubPaths;\n /** Rejects subscribes that would grow an attachment past this size. */\n maxAttachmentBytes?: number;\n}\n\n/** Thrown for requests the caller is not allowed to make. */\nexport class ReactiveHubError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ReactiveHubError';\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a reactive HTTP body for the subscribe and unsubscribe requests a hub serves. */\nexport function isHubRequest(value: unknown): value is SubscribeRequest | UnsubscribeRequest {\n if (!isRecord(value)) return false;\n return (\n (value.type === 'subscribe' || value.type === 'unsubscribe') &&\n typeof value.connectionId === 'string' &&\n typeof value.subscriptionId === 'string' &&\n typeof value.generation === 'number' &&\n (value.type === 'unsubscribe' || typeof value.path === 'string')\n );\n}\n\n/** Returns the bearer token from an `Authorization` header, or an empty string. */\nexport function bearerToken(request: Request): string {\n const match = /^Bearer ([^\\s]+)$/u.exec(request.headers.get('authorization') ?? '');\n return match?.[1] ?? '';\n}\n\n/** Initial attachment for a newly accepted socket. */\nexport function createHubSocketState(\n connectionId: string,\n principal: string,\n connectedAt?: number,\n): HubSocketState {\n return {\n connectionId,\n principal,\n subscriptions: [],\n ...(connectedAt === undefined ? {} : { connectedAt }),\n };\n}\n\nfunction socketState(socket: HubSocket): HubSocketState {\n const state = socket.deserializeAttachment() as HubSocketState;\n // Attachments written before subscriptions carried a path have no path; they\n // are dropped rather than fanned out to every notification.\n return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };\n}\n\nfunction send(socket: HubSocket, event: WebSocketEvent): void {\n try {\n socket.send(JSON.stringify(event));\n } catch {\n // A socket closing mid-broadcast is cleaned up by its close handler.\n }\n}\n\n/** Creates the protocol core for one hub instance. */\nexport function createReactiveHub(options: ReactiveHubOptions) {\n const { sockets, versions } = options;\n const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;\n const resolve = (path: string): HubPath | undefined =>\n typeof options.paths === 'function'\n ? options.paths(path)\n : Object.hasOwn(options.paths, path)\n ? options.paths[path]\n : undefined;\n\n const ownedSocket = (connectionId: string, principal: string): HubSocket | undefined => {\n const socket = sockets.byConnectionId(connectionId);\n // The HTTP caller must own the WebSocket it registers against.\n return socket && socketState(socket).principal === principal ? socket : undefined;\n };\n\n const subscribersOf = (path: string) =>\n sockets.all().flatMap((socket) =>\n socketState(socket)\n .subscriptions.filter((subscription) => subscription.path === path)\n .map((subscription) => ({ socket, subscription })),\n );\n\n const bump = async (path: string) => {\n const fromVersion = await versions.get(path);\n const toVersion = fromVersion + 1;\n await versions.set(path, toVersion);\n return { fromVersion, toVersion };\n };\n\n return {\n /**\n * Answers the client's `init` and `ping` frames. Any other frame with a\n * string `type` is returned for the application to handle.\n */\n message(\n socket: HubSocket,\n raw: string,\n ): (Record<string, unknown> & { type: string }) | undefined {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(frame) || typeof frame.type !== 'string') return undefined;\n if (frame.type === 'init') {\n send(socket, { type: 'connected', connectionId: socketState(socket).connectionId });\n } else if (frame.type === 'ping') {\n send(socket, { type: 'pong' });\n } else {\n return frame as Record<string, unknown> & { type: string };\n }\n return undefined;\n },\n\n /** The application data stored with a socket. */\n socketData<T = unknown>(socket: HubSocket): T | undefined {\n return socketState(socket).data as T | undefined;\n },\n\n /** Stores application data with a socket; it shares the attachment size limit. */\n setSocketData(socket: HubSocket, data: unknown): void {\n const next: HubSocketState = { ...socketState(socket), data };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Socket data is too large');\n }\n socket.serializeAttachment(next);\n },\n\n /** The principal a socket authenticated as. */\n principalOf(socket: HubSocket): string {\n return socketState(socket).principal;\n },\n\n async subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse> {\n const handler = resolve(request.path);\n if (!handler) throw new ReactiveHubError('Unknown subscription path');\n if (!ownedSocket(request.connectionId, principal)) {\n throw new ReactiveHubError('Unknown connection');\n }\n const context: HubPathContext = { path: request.path, principal, input: request.input };\n if (handler.authorize && !(await handler.authorize(context))) {\n throw new ReactiveHubError('Subscription not allowed');\n }\n // Re-read after the awaits so a concurrent subscribe on the same socket is kept.\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) throw new ReactiveHubError('Unknown connection');\n const state = socketState(socket);\n const next: HubSocketState = {\n ...state,\n subscriptions: [\n ...state.subscriptions.filter((entry) => entry.subscriptionId !== request.subscriptionId),\n {\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n path: request.path,\n },\n ],\n };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Too many subscriptions on this connection');\n }\n socket.serializeAttachment(next);\n // Read the version before the data so a concurrent notify can only make the\n // client re-fetch, never miss a change.\n const version = await versions.get(request.path);\n const data = await handler.snapshot(context);\n return {\n type: 'snapshot',\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n version,\n data,\n };\n },\n\n unsubscribe(request: UnsubscribeRequest, principal: string): void {\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) return;\n const state = socketState(socket);\n socket.serializeAttachment({\n ...state,\n subscriptions: state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n } satisfies HubSocketState);\n },\n\n /** Bumps the path's version and asks each of its subscribers to re-fetch. */\n async notify(path: string): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'resync',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n });\n }\n },\n\n /**\n * Bumps the path's version and sends the same JSON patches to each subscriber.\n * Use it only when every subscriber of the path sees the same data.\n */\n async publish(path: string, patches: JsonPatch[]): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'patch',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n patches,\n });\n }\n },\n };\n}\n\nexport type ReactiveHub = ReturnType<typeof createReactiveHub>;\n"]}
|
package/dist/hub.d.cts
CHANGED
|
@@ -22,6 +22,8 @@ interface HubSocketState {
|
|
|
22
22
|
subscriptions: HubSubscription[];
|
|
23
23
|
/** Epoch ms the socket was accepted; the liveness baseline before its first ping. */
|
|
24
24
|
connectedAt?: number;
|
|
25
|
+
/** Application data kept with the socket, such as what a listener is playing. */
|
|
26
|
+
data?: unknown;
|
|
25
27
|
}
|
|
26
28
|
/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */
|
|
27
29
|
interface HubSocket {
|
|
@@ -73,8 +75,19 @@ declare function bearerToken(request: Request): string;
|
|
|
73
75
|
declare function createHubSocketState(connectionId: string, principal: string, connectedAt?: number): HubSocketState;
|
|
74
76
|
/** Creates the protocol core for one hub instance. */
|
|
75
77
|
declare function createReactiveHub(options: ReactiveHubOptions): {
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Answers the client's `init` and `ping` frames. Any other frame with a
|
|
80
|
+
* string `type` is returned for the application to handle.
|
|
81
|
+
*/
|
|
82
|
+
message(socket: HubSocket, raw: string): (Record<string, unknown> & {
|
|
83
|
+
type: string;
|
|
84
|
+
}) | undefined;
|
|
85
|
+
/** The application data stored with a socket. */
|
|
86
|
+
socketData<T = unknown>(socket: HubSocket): T | undefined;
|
|
87
|
+
/** Stores application data with a socket; it shares the attachment size limit. */
|
|
88
|
+
setSocketData(socket: HubSocket, data: unknown): void;
|
|
89
|
+
/** The principal a socket authenticated as. */
|
|
90
|
+
principalOf(socket: HubSocket): string;
|
|
78
91
|
subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse>;
|
|
79
92
|
unsubscribe(request: UnsubscribeRequest, principal: string): void;
|
|
80
93
|
/** Bumps the path's version and asks each of its subscribers to re-fetch. */
|
package/dist/hub.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ interface HubSocketState {
|
|
|
22
22
|
subscriptions: HubSubscription[];
|
|
23
23
|
/** Epoch ms the socket was accepted; the liveness baseline before its first ping. */
|
|
24
24
|
connectedAt?: number;
|
|
25
|
+
/** Application data kept with the socket, such as what a listener is playing. */
|
|
26
|
+
data?: unknown;
|
|
25
27
|
}
|
|
26
28
|
/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */
|
|
27
29
|
interface HubSocket {
|
|
@@ -73,8 +75,19 @@ declare function bearerToken(request: Request): string;
|
|
|
73
75
|
declare function createHubSocketState(connectionId: string, principal: string, connectedAt?: number): HubSocketState;
|
|
74
76
|
/** Creates the protocol core for one hub instance. */
|
|
75
77
|
declare function createReactiveHub(options: ReactiveHubOptions): {
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Answers the client's `init` and `ping` frames. Any other frame with a
|
|
80
|
+
* string `type` is returned for the application to handle.
|
|
81
|
+
*/
|
|
82
|
+
message(socket: HubSocket, raw: string): (Record<string, unknown> & {
|
|
83
|
+
type: string;
|
|
84
|
+
}) | undefined;
|
|
85
|
+
/** The application data stored with a socket. */
|
|
86
|
+
socketData<T = unknown>(socket: HubSocket): T | undefined;
|
|
87
|
+
/** Stores application data with a socket; it shares the attachment size limit. */
|
|
88
|
+
setSocketData(socket: HubSocket, data: unknown): void;
|
|
89
|
+
/** The principal a socket authenticated as. */
|
|
90
|
+
principalOf(socket: HubSocket): string;
|
|
78
91
|
subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse>;
|
|
79
92
|
unsubscribe(request: UnsubscribeRequest, principal: string): void;
|
|
80
93
|
/** Bumps the path's version and asks each of its subscribers to re-fetch. */
|
package/dist/hub.js
CHANGED
|
@@ -53,20 +53,42 @@ function createReactiveHub(options) {
|
|
|
53
53
|
return { fromVersion, toVersion };
|
|
54
54
|
};
|
|
55
55
|
return {
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Answers the client's `init` and `ping` frames. Any other frame with a
|
|
58
|
+
* string `type` is returned for the application to handle.
|
|
59
|
+
*/
|
|
57
60
|
message(socket, raw) {
|
|
58
61
|
let frame;
|
|
59
62
|
try {
|
|
60
63
|
frame = JSON.parse(raw);
|
|
61
64
|
} catch {
|
|
62
|
-
return;
|
|
65
|
+
return void 0;
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
if (type === "init") {
|
|
67
|
+
if (!isRecord(frame) || typeof frame.type !== "string") return void 0;
|
|
68
|
+
if (frame.type === "init") {
|
|
66
69
|
send(socket, { type: "connected", connectionId: socketState(socket).connectionId });
|
|
67
|
-
} else if (type === "ping") {
|
|
70
|
+
} else if (frame.type === "ping") {
|
|
68
71
|
send(socket, { type: "pong" });
|
|
72
|
+
} else {
|
|
73
|
+
return frame;
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
},
|
|
77
|
+
/** The application data stored with a socket. */
|
|
78
|
+
socketData(socket) {
|
|
79
|
+
return socketState(socket).data;
|
|
80
|
+
},
|
|
81
|
+
/** Stores application data with a socket; it shares the attachment size limit. */
|
|
82
|
+
setSocketData(socket, data) {
|
|
83
|
+
const next = { ...socketState(socket), data };
|
|
84
|
+
if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {
|
|
85
|
+
throw new ReactiveHubError("Socket data is too large");
|
|
69
86
|
}
|
|
87
|
+
socket.serializeAttachment(next);
|
|
88
|
+
},
|
|
89
|
+
/** The principal a socket authenticated as. */
|
|
90
|
+
principalOf(socket) {
|
|
91
|
+
return socketState(socket).principal;
|
|
70
92
|
},
|
|
71
93
|
async subscribe(request, principal) {
|
|
72
94
|
const handler = resolve(request.path);
|
package/dist/hub.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../client/src/hub.ts"],"names":[],"mappings":"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","file":"hub.js","sourcesContent":["import type {\n JsonPatch,\n SnapshotResponse,\n SubscribeRequest,\n UnsubscribeRequest,\n WebSocketEvent,\n} from './protocol.js';\n\n/**\n * Protocol core for a reactive hub: the server half of the wire protocol for\n * backends that hold WebSockets themselves (for example a Cloudflare Durable\n * Object). It keeps every piece of per-socket state on the socket's attachment so\n * the state survives hibernation, and it has no runtime imports, so it runs and\n * tests in Node.\n */\n\n/** Hibernation limits a socket attachment to 2,048 bytes of serialized JSON. */\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 2_048;\n\n/** One subscription held by a socket. */\nexport interface HubSubscription {\n subscriptionId: string;\n generation: number;\n path: string;\n}\n\n/** State kept on each socket's attachment. */\nexport interface HubSocketState {\n connectionId: string;\n principal: string;\n subscriptions: HubSubscription[];\n /** Epoch ms the socket was accepted; the liveness baseline before its first ping. */\n connectedAt?: number;\n}\n\n/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */\nexport interface HubSocket {\n send(message: string): void;\n deserializeAttachment(): unknown;\n serializeAttachment(value: unknown): void;\n}\n\nexport interface HubSockets {\n all(): HubSocket[];\n byConnectionId(connectionId: string): HubSocket | undefined;\n}\n\n/** Monotonic per-path versions. Versions only need to grow while sockets stay open. */\nexport interface HubVersions {\n get(path: string): Promise<number>;\n set(path: string, version: number): Promise<void>;\n}\n\nexport interface HubPathContext {\n path: string;\n principal: string;\n input: unknown;\n}\n\nexport interface HubPath {\n /**\n * Runs before the subscription is stored. Return false to reject it. Every\n * subscriber of a path receives its pushes, so authorise here, not in `snapshot`.\n */\n authorize?(context: HubPathContext): boolean | Promise<boolean>;\n /** Current data for the path, sent as the subscription snapshot. */\n snapshot(context: HubPathContext): unknown;\n}\n\n/** Fixed paths by name, or a resolver for parameterised paths such as `drafts.<id>`. */\nexport type HubPaths = Record<string, HubPath> | ((path: string) => HubPath | undefined);\n\nexport interface ReactiveHubOptions {\n sockets: HubSockets;\n versions: HubVersions;\n paths: HubPaths;\n /** Rejects subscribes that would grow an attachment past this size. */\n maxAttachmentBytes?: number;\n}\n\n/** Thrown for requests the caller is not allowed to make. */\nexport class ReactiveHubError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ReactiveHubError';\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a reactive HTTP body for the subscribe and unsubscribe requests a hub serves. */\nexport function isHubRequest(value: unknown): value is SubscribeRequest | UnsubscribeRequest {\n if (!isRecord(value)) return false;\n return (\n (value.type === 'subscribe' || value.type === 'unsubscribe') &&\n typeof value.connectionId === 'string' &&\n typeof value.subscriptionId === 'string' &&\n typeof value.generation === 'number' &&\n (value.type === 'unsubscribe' || typeof value.path === 'string')\n );\n}\n\n/** Returns the bearer token from an `Authorization` header, or an empty string. */\nexport function bearerToken(request: Request): string {\n const match = /^Bearer ([^\\s]+)$/u.exec(request.headers.get('authorization') ?? '');\n return match?.[1] ?? '';\n}\n\n/** Initial attachment for a newly accepted socket. */\nexport function createHubSocketState(\n connectionId: string,\n principal: string,\n connectedAt?: number,\n): HubSocketState {\n return {\n connectionId,\n principal,\n subscriptions: [],\n ...(connectedAt === undefined ? {} : { connectedAt }),\n };\n}\n\nfunction socketState(socket: HubSocket): HubSocketState {\n const state = socket.deserializeAttachment() as HubSocketState;\n // Attachments written before subscriptions carried a path have no path; they\n // are dropped rather than fanned out to every notification.\n return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };\n}\n\nfunction send(socket: HubSocket, event: WebSocketEvent): void {\n try {\n socket.send(JSON.stringify(event));\n } catch {\n // A socket closing mid-broadcast is cleaned up by its close handler.\n }\n}\n\n/** Creates the protocol core for one hub instance. */\nexport function createReactiveHub(options: ReactiveHubOptions) {\n const { sockets, versions } = options;\n const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;\n const resolve = (path: string): HubPath | undefined =>\n typeof options.paths === 'function'\n ? options.paths(path)\n : Object.hasOwn(options.paths, path)\n ? options.paths[path]\n : undefined;\n\n const ownedSocket = (connectionId: string, principal: string): HubSocket | undefined => {\n const socket = sockets.byConnectionId(connectionId);\n // The HTTP caller must own the WebSocket it registers against.\n return socket && socketState(socket).principal === principal ? socket : undefined;\n };\n\n const subscribersOf = (path: string) =>\n sockets.all().flatMap((socket) =>\n socketState(socket)\n .subscriptions.filter((subscription) => subscription.path === path)\n .map((subscription) => ({ socket, subscription })),\n );\n\n const bump = async (path: string) => {\n const fromVersion = await versions.get(path);\n const toVersion = fromVersion + 1;\n await versions.set(path, toVersion);\n return { fromVersion, toVersion };\n };\n\n return {\n /** Answers the client's `init` and `ping` frames. */\n message(socket: HubSocket, raw: string): void {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return;\n }\n const type = isRecord(frame) ? frame.type : undefined;\n if (type === 'init') {\n send(socket, { type: 'connected', connectionId: socketState(socket).connectionId });\n } else if (type === 'ping') {\n send(socket, { type: 'pong' });\n }\n },\n\n async subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse> {\n const handler = resolve(request.path);\n if (!handler) throw new ReactiveHubError('Unknown subscription path');\n if (!ownedSocket(request.connectionId, principal)) {\n throw new ReactiveHubError('Unknown connection');\n }\n const context: HubPathContext = { path: request.path, principal, input: request.input };\n if (handler.authorize && !(await handler.authorize(context))) {\n throw new ReactiveHubError('Subscription not allowed');\n }\n // Re-read after the awaits so a concurrent subscribe on the same socket is kept.\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) throw new ReactiveHubError('Unknown connection');\n const state = socketState(socket);\n const next: HubSocketState = {\n ...state,\n subscriptions: [\n ...state.subscriptions.filter((entry) => entry.subscriptionId !== request.subscriptionId),\n {\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n path: request.path,\n },\n ],\n };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Too many subscriptions on this connection');\n }\n socket.serializeAttachment(next);\n // Read the version before the data so a concurrent notify can only make the\n // client re-fetch, never miss a change.\n const version = await versions.get(request.path);\n const data = await handler.snapshot(context);\n return {\n type: 'snapshot',\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n version,\n data,\n };\n },\n\n unsubscribe(request: UnsubscribeRequest, principal: string): void {\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) return;\n const state = socketState(socket);\n socket.serializeAttachment({\n ...state,\n subscriptions: state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n } satisfies HubSocketState);\n },\n\n /** Bumps the path's version and asks each of its subscribers to re-fetch. */\n async notify(path: string): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'resync',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n });\n }\n },\n\n /**\n * Bumps the path's version and sends the same JSON patches to each subscriber.\n * Use it only when every subscriber of the path sees the same data.\n */\n async publish(path: string, patches: JsonPatch[]): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'patch',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n patches,\n });\n }\n },\n };\n}\n\nexport type ReactiveHub = ReturnType<typeof createReactiveHub>;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../client/src/hub.ts"],"names":[],"mappings":"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","file":"hub.js","sourcesContent":["import type {\n JsonPatch,\n SnapshotResponse,\n SubscribeRequest,\n UnsubscribeRequest,\n WebSocketEvent,\n} from './protocol.js';\n\n/**\n * Protocol core for a reactive hub: the server half of the wire protocol for\n * backends that hold WebSockets themselves (for example a Cloudflare Durable\n * Object). It keeps every piece of per-socket state on the socket's attachment so\n * the state survives hibernation, and it has no runtime imports, so it runs and\n * tests in Node.\n */\n\n/** Hibernation limits a socket attachment to 2,048 bytes of serialized JSON. */\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 2_048;\n\n/** One subscription held by a socket. */\nexport interface HubSubscription {\n subscriptionId: string;\n generation: number;\n path: string;\n}\n\n/** State kept on each socket's attachment. */\nexport interface HubSocketState {\n connectionId: string;\n principal: string;\n subscriptions: HubSubscription[];\n /** Epoch ms the socket was accepted; the liveness baseline before its first ping. */\n connectedAt?: number;\n /** Application data kept with the socket, such as what a listener is playing. */\n data?: unknown;\n}\n\n/** The parts of a hibernatable WebSocket the hub uses; faked in tests. */\nexport interface HubSocket {\n send(message: string): void;\n deserializeAttachment(): unknown;\n serializeAttachment(value: unknown): void;\n}\n\nexport interface HubSockets {\n all(): HubSocket[];\n byConnectionId(connectionId: string): HubSocket | undefined;\n}\n\n/** Monotonic per-path versions. Versions only need to grow while sockets stay open. */\nexport interface HubVersions {\n get(path: string): Promise<number>;\n set(path: string, version: number): Promise<void>;\n}\n\nexport interface HubPathContext {\n path: string;\n principal: string;\n input: unknown;\n}\n\nexport interface HubPath {\n /**\n * Runs before the subscription is stored. Return false to reject it. Every\n * subscriber of a path receives its pushes, so authorise here, not in `snapshot`.\n */\n authorize?(context: HubPathContext): boolean | Promise<boolean>;\n /** Current data for the path, sent as the subscription snapshot. */\n snapshot(context: HubPathContext): unknown;\n}\n\n/** Fixed paths by name, or a resolver for parameterised paths such as `drafts.<id>`. */\nexport type HubPaths = Record<string, HubPath> | ((path: string) => HubPath | undefined);\n\nexport interface ReactiveHubOptions {\n sockets: HubSockets;\n versions: HubVersions;\n paths: HubPaths;\n /** Rejects subscribes that would grow an attachment past this size. */\n maxAttachmentBytes?: number;\n}\n\n/** Thrown for requests the caller is not allowed to make. */\nexport class ReactiveHubError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ReactiveHubError';\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Validates a reactive HTTP body for the subscribe and unsubscribe requests a hub serves. */\nexport function isHubRequest(value: unknown): value is SubscribeRequest | UnsubscribeRequest {\n if (!isRecord(value)) return false;\n return (\n (value.type === 'subscribe' || value.type === 'unsubscribe') &&\n typeof value.connectionId === 'string' &&\n typeof value.subscriptionId === 'string' &&\n typeof value.generation === 'number' &&\n (value.type === 'unsubscribe' || typeof value.path === 'string')\n );\n}\n\n/** Returns the bearer token from an `Authorization` header, or an empty string. */\nexport function bearerToken(request: Request): string {\n const match = /^Bearer ([^\\s]+)$/u.exec(request.headers.get('authorization') ?? '');\n return match?.[1] ?? '';\n}\n\n/** Initial attachment for a newly accepted socket. */\nexport function createHubSocketState(\n connectionId: string,\n principal: string,\n connectedAt?: number,\n): HubSocketState {\n return {\n connectionId,\n principal,\n subscriptions: [],\n ...(connectedAt === undefined ? {} : { connectedAt }),\n };\n}\n\nfunction socketState(socket: HubSocket): HubSocketState {\n const state = socket.deserializeAttachment() as HubSocketState;\n // Attachments written before subscriptions carried a path have no path; they\n // are dropped rather than fanned out to every notification.\n return { ...state, subscriptions: state.subscriptions.filter((entry) => entry.path) };\n}\n\nfunction send(socket: HubSocket, event: WebSocketEvent): void {\n try {\n socket.send(JSON.stringify(event));\n } catch {\n // A socket closing mid-broadcast is cleaned up by its close handler.\n }\n}\n\n/** Creates the protocol core for one hub instance. */\nexport function createReactiveHub(options: ReactiveHubOptions) {\n const { sockets, versions } = options;\n const maxAttachmentBytes = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;\n const resolve = (path: string): HubPath | undefined =>\n typeof options.paths === 'function'\n ? options.paths(path)\n : Object.hasOwn(options.paths, path)\n ? options.paths[path]\n : undefined;\n\n const ownedSocket = (connectionId: string, principal: string): HubSocket | undefined => {\n const socket = sockets.byConnectionId(connectionId);\n // The HTTP caller must own the WebSocket it registers against.\n return socket && socketState(socket).principal === principal ? socket : undefined;\n };\n\n const subscribersOf = (path: string) =>\n sockets.all().flatMap((socket) =>\n socketState(socket)\n .subscriptions.filter((subscription) => subscription.path === path)\n .map((subscription) => ({ socket, subscription })),\n );\n\n const bump = async (path: string) => {\n const fromVersion = await versions.get(path);\n const toVersion = fromVersion + 1;\n await versions.set(path, toVersion);\n return { fromVersion, toVersion };\n };\n\n return {\n /**\n * Answers the client's `init` and `ping` frames. Any other frame with a\n * string `type` is returned for the application to handle.\n */\n message(\n socket: HubSocket,\n raw: string,\n ): (Record<string, unknown> & { type: string }) | undefined {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(frame) || typeof frame.type !== 'string') return undefined;\n if (frame.type === 'init') {\n send(socket, { type: 'connected', connectionId: socketState(socket).connectionId });\n } else if (frame.type === 'ping') {\n send(socket, { type: 'pong' });\n } else {\n return frame as Record<string, unknown> & { type: string };\n }\n return undefined;\n },\n\n /** The application data stored with a socket. */\n socketData<T = unknown>(socket: HubSocket): T | undefined {\n return socketState(socket).data as T | undefined;\n },\n\n /** Stores application data with a socket; it shares the attachment size limit. */\n setSocketData(socket: HubSocket, data: unknown): void {\n const next: HubSocketState = { ...socketState(socket), data };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Socket data is too large');\n }\n socket.serializeAttachment(next);\n },\n\n /** The principal a socket authenticated as. */\n principalOf(socket: HubSocket): string {\n return socketState(socket).principal;\n },\n\n async subscribe(request: SubscribeRequest, principal: string): Promise<SnapshotResponse> {\n const handler = resolve(request.path);\n if (!handler) throw new ReactiveHubError('Unknown subscription path');\n if (!ownedSocket(request.connectionId, principal)) {\n throw new ReactiveHubError('Unknown connection');\n }\n const context: HubPathContext = { path: request.path, principal, input: request.input };\n if (handler.authorize && !(await handler.authorize(context))) {\n throw new ReactiveHubError('Subscription not allowed');\n }\n // Re-read after the awaits so a concurrent subscribe on the same socket is kept.\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) throw new ReactiveHubError('Unknown connection');\n const state = socketState(socket);\n const next: HubSocketState = {\n ...state,\n subscriptions: [\n ...state.subscriptions.filter((entry) => entry.subscriptionId !== request.subscriptionId),\n {\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n path: request.path,\n },\n ],\n };\n if (new TextEncoder().encode(JSON.stringify(next)).length > maxAttachmentBytes) {\n throw new ReactiveHubError('Too many subscriptions on this connection');\n }\n socket.serializeAttachment(next);\n // Read the version before the data so a concurrent notify can only make the\n // client re-fetch, never miss a change.\n const version = await versions.get(request.path);\n const data = await handler.snapshot(context);\n return {\n type: 'snapshot',\n subscriptionId: request.subscriptionId,\n generation: request.generation,\n version,\n data,\n };\n },\n\n unsubscribe(request: UnsubscribeRequest, principal: string): void {\n const socket = ownedSocket(request.connectionId, principal);\n if (!socket) return;\n const state = socketState(socket);\n socket.serializeAttachment({\n ...state,\n subscriptions: state.subscriptions.filter(\n (entry) => entry.subscriptionId !== request.subscriptionId,\n ),\n } satisfies HubSocketState);\n },\n\n /** Bumps the path's version and asks each of its subscribers to re-fetch. */\n async notify(path: string): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'resync',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n });\n }\n },\n\n /**\n * Bumps the path's version and sends the same JSON patches to each subscriber.\n * Use it only when every subscriber of the path sees the same data.\n */\n async publish(path: string, patches: JsonPatch[]): Promise<void> {\n const { fromVersion, toVersion } = await bump(path);\n for (const { socket, subscription } of subscribersOf(path)) {\n send(socket, {\n type: 'patch',\n subscriptionId: subscription.subscriptionId,\n generation: subscription.generation,\n fromVersion,\n toVersion,\n messageId: crypto.randomUUID(),\n patches,\n });\n }\n },\n };\n}\n\nexport type ReactiveHub = ReturnType<typeof createReactiveHub>;\n"]}
|
package/dist/react.cjs
CHANGED
|
@@ -285,6 +285,13 @@ var WebSocketManager = class {
|
|
|
285
285
|
this.setState("disconnected");
|
|
286
286
|
void this.connect().catch(() => void 0);
|
|
287
287
|
}
|
|
288
|
+
/** Sends an application frame on the open socket; false when not connected. */
|
|
289
|
+
send(frame) {
|
|
290
|
+
const socket = this.ws;
|
|
291
|
+
if (!socket || this.state !== "connected" || socket.readyState !== socket.OPEN) return false;
|
|
292
|
+
socket.send(JSON.stringify(frame));
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
288
295
|
onMessage(handler) {
|
|
289
296
|
this.messageHandlers.add(handler);
|
|
290
297
|
return () => this.messageHandlers.delete(handler);
|
|
@@ -508,6 +515,14 @@ var ReactiveClient = class {
|
|
|
508
515
|
getConnectionState() {
|
|
509
516
|
return this.connectionState;
|
|
510
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Sends an application frame over the socket, such as a presence update a
|
|
520
|
+
* hub's `onMessage` handles. Returns false while disconnected; resend from
|
|
521
|
+
* `onConnectionStateChange` once connected again.
|
|
522
|
+
*/
|
|
523
|
+
send(frame) {
|
|
524
|
+
return this.wsManager.send(frame);
|
|
525
|
+
}
|
|
511
526
|
onConnectionStateChange(listener) {
|
|
512
527
|
this.stateListeners.add(listener);
|
|
513
528
|
return () => this.stateListeners.delete(listener);
|