@shadow-js/realtime 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +75 -0
- package/dist/dev/client/core.d.ts +24 -0
- package/dist/dev/client/core.js +845 -0
- package/dist/dev/client/core.js.map +1 -0
- package/dist/dev/client/index.d.ts +5 -0
- package/dist/dev/client/index.js +3 -0
- package/dist/dev/client/presence.js +23 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +76 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +256 -0
- package/dist/dev/client/util.js +30 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +39 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +80 -0
- package/dist/dev/cloudflare/adapter.js +347 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +701 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +397 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +82 -0
- package/dist/dev/cloudflare/object.js +214 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +100 -0
- package/dist/dev/cloudflare/wire.js +284 -0
- package/dist/dev/cloudflare/wire.js.map +1 -0
- package/dist/dev/cloudflare.d.ts +4 -0
- package/dist/dev/cloudflare.js +3 -0
- package/dist/dev/errors.d.ts +25 -0
- package/dist/dev/errors.js +24 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +110 -0
- package/dist/dev/hooks.js +162 -0
- package/dist/dev/hooks.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/memory.d.ts +33 -0
- package/dist/dev/memory.js +255 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +211 -0
- package/dist/dev/protocol.js +400 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +139 -0
- package/dist/dev/redis/client.js +146 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +275 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +8 -0
- package/dist/dev/redis/topics.js +377 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +68 -0
- package/dist/dev/redis.js +422 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +48 -0
- package/dist/dev/server/http.js +286 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +114 -0
- package/dist/dev/server/hub.js +827 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +47 -0
- package/dist/dev/server/index.js +41 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +54 -0
- package/dist/dev/server/limits.js +85 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +43 -0
- package/dist/dev/server/rules.js +72 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +115 -0
- package/dist/dev/testing.js +427 -0
- package/dist/dev/testing.js.map +1 -0
- package/dist/prod/client/core.js +2 -0
- package/dist/prod/client/core.js.map +1 -0
- package/dist/prod/client/index.js +1 -0
- package/dist/prod/client/presence.js +2 -0
- package/dist/prod/client/presence.js.map +1 -0
- package/dist/prod/client/sse.js +5 -0
- package/dist/prod/client/sse.js.map +1 -0
- package/dist/prod/client/util.js +2 -0
- package/dist/prod/client/util.js.map +1 -0
- package/dist/prod/client/ws.js +2 -0
- package/dist/prod/client/ws.js.map +1 -0
- package/dist/prod/cloudflare/adapter.js +3 -0
- package/dist/prod/cloudflare/adapter.js.map +1 -0
- package/dist/prod/cloudflare/core.js +2 -0
- package/dist/prod/cloudflare/core.js.map +1 -0
- package/dist/prod/cloudflare/link.js +2 -0
- package/dist/prod/cloudflare/link.js.map +1 -0
- package/dist/prod/cloudflare/object.js +2 -0
- package/dist/prod/cloudflare/object.js.map +1 -0
- package/dist/prod/cloudflare/wire.js +2 -0
- package/dist/prod/cloudflare/wire.js.map +1 -0
- package/dist/prod/cloudflare.js +1 -0
- package/dist/prod/errors.js +2 -0
- package/dist/prod/errors.js.map +1 -0
- package/dist/prod/hooks.js +2 -0
- package/dist/prod/hooks.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/memory.js +2 -0
- package/dist/prod/memory.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/redis/client.js +2 -0
- package/dist/prod/redis/client.js.map +1 -0
- package/dist/prod/redis/scripts.js +157 -0
- package/dist/prod/redis/scripts.js.map +1 -0
- package/dist/prod/redis/topics.js +2 -0
- package/dist/prod/redis/topics.js.map +1 -0
- package/dist/prod/redis.js +2 -0
- package/dist/prod/redis.js.map +1 -0
- package/dist/prod/server/http.js +2 -0
- package/dist/prod/server/http.js.map +1 -0
- package/dist/prod/server/hub.js +2 -0
- package/dist/prod/server/hub.js.map +1 -0
- package/dist/prod/server/index.js +2 -0
- package/dist/prod/server/index.js.map +1 -0
- package/dist/prod/server/limits.js +2 -0
- package/dist/prod/server/limits.js.map +1 -0
- package/dist/prod/server/rules.js +2 -0
- package/dist/prod/server/rules.js.map +1 -0
- package/dist/prod/testing.js +2 -0
- package/dist/prod/testing.js.map +1 -0
- package/package.json +105 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","names":["MAX_ENTRY_BYTES","MAX_STATE_BYTES","MAX_MEMBERS","MAX_HISTORY","MAX_TTL_MS","MAX_BODY_BYTES","OPERATIONS","isOperation","value","isRecord","isCount","max","fail","parseMember","id","u","v","state","s","text","byteLength","parseEntry","isEventName","event","data","publisher","user","parseOptions","history","historyTtlMs","idempotencyTtlMs","key","isIdempotencyKey","parseRequest","body","isChannelName","channel","op","entry","\"entry\"","options","\"options\"","idleMs","\"idleMs\"","after","limit","member","\"member\"","graceMs","\"graceMs\"","ttlMs","ids","decodeLinkFrame","\"t\"","head","oldest","list","\"members\"","members","item","message","parseStored","\"m\"","\"op\"","\"why\"","seq","at","isWholeNumber","historyKey","seqOfKey"],"sources":["../../../src/cloudflare/wire.ts"],"sourcesContent":["/**\n * The private protocol between the adapter (in a Worker) and the channel object (a Durable Object): JSON requests to\n * `/v1/<op>` and the frames of the subscribe WebSocket. Both sides validate what they read; neither trusts the other's shape.\n * @packageDocumentation\n */\nimport type { PublishOptions } from \"../adapter\";\nimport type { ErrorCode } from \"../errors\";\nimport {\n byteLength,\n isChannelName,\n isEventName,\n isIdempotencyKey,\n type Json,\n type Member,\n type PresenceEvent,\n type StoredEntry,\n type StoredMessage,\n} from \"../protocol\";\n\n/** The version in the request path. @internal */\nexport const WIRE_VERSION: number = 1;\n\n/** The most bytes of one message (data, event and names) the object stores in one value; the platform's smallest value limit is 128 KiB. @internal */\nexport const MAX_ENTRY_BYTES: number = 120 * 1024;\n/** The most bytes of one member's state. @internal */\nexport const MAX_STATE_BYTES: number = 16 * 1024;\n/** The most members one channel object keeps. @internal */\nexport const MAX_MEMBERS: number = 10_000;\n/** The most messages one channel retains. @internal */\nexport const MAX_HISTORY: number = 10_000;\n/** The longest expiry of a presence member or an idempotency key. @internal */\nexport const MAX_TTL_MS: number = 86_400_000;\n/** The most bytes of a request body the object reads. @internal */\nexport const MAX_BODY_BYTES: number = 256 * 1024;\n\n/** The operations of the request protocol. @internal */\nexport type Operation =\n | \"publish\"\n | \"history\"\n | \"presence-set\"\n | \"presence-remove\"\n | \"presence-list\"\n | \"presence-touch\"\n | \"presence-reap\";\n\n/** A parsed request. @internal */\nexport type ParsedRequest =\n | {\n readonly op: \"publish\";\n readonly channel: string;\n readonly entry: StoredEntry;\n readonly options: PublishOptions;\n readonly idleMs: number;\n }\n | {\n readonly op: \"history\";\n readonly channel: string;\n readonly after: number;\n readonly limit: number;\n }\n | {\n readonly op: \"presence-set\";\n readonly channel: string;\n readonly member: Member;\n readonly ttlMs: number;\n readonly graceMs: number;\n }\n | { readonly op: \"presence-remove\"; readonly channel: string; readonly id: string }\n | { readonly op: \"presence-list\"; readonly channel: string }\n | {\n readonly op: \"presence-touch\";\n readonly channel: string;\n readonly ids: readonly string[];\n readonly ttlMs: number;\n }\n | { readonly op: \"presence-reap\"; readonly channel: string };\n\n/** What parsing a request yields. @internal */\nexport type Parsed =\n | { readonly ok: true; readonly request: ParsedRequest }\n | { readonly ok: false; readonly code: ErrorCode; readonly message: string };\n\n/** A page of history as the object returns it. @internal */\nexport interface HistoryPage {\n readonly messages: readonly StoredMessage[];\n readonly head: number;\n readonly oldest: number;\n /** True when retained messages after the last one returned remain. */\n readonly more: boolean;\n}\n\n/** Frames the object sends on a subscribe socket. @internal */\nexport type LinkFrame =\n | { readonly t: \"ready\"; readonly head: number; readonly oldest: number }\n | { readonly t: \"snap\"; readonly members: readonly Member[] }\n | { readonly t: \"msg\"; readonly m: StoredMessage }\n | {\n readonly t: \"pres\";\n readonly op: PresenceEvent[\"op\"];\n readonly member: Member;\n }\n | { readonly t: \"more\" }\n | { readonly t: \"bye\"; readonly why: string };\n\nconst OPERATIONS: ReadonlySet<string> = new Set<Operation>([\n \"publish\",\n \"history\",\n \"presence-set\",\n \"presence-remove\",\n \"presence-list\",\n \"presence-touch\",\n \"presence-reap\",\n]);\n\n/** Whether `value` names an operation. @internal */\nexport function isOperation(value: string): value is Operation {\n return OPERATIONS.has(value);\n}\n\nfunction isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isCount(value: unknown, max = Number.MAX_SAFE_INTEGER): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 && value <= max;\n}\n\nfunction fail(message: string, code: ErrorCode = \"bad_frame\"): Parsed {\n return { ok: false, code, message };\n}\n\n/**\n * Whether `value` is a well formed member: an id, an optional user, a version of 1 or more and a JSON state of bounded size.\n * @internal\n */\nexport function parseMember(value: unknown): Member | undefined {\n if (!isRecord(value)) return undefined;\n const { id, u, s, v } = value;\n if (typeof id !== \"string\" || id.length === 0 || id.length > 256) return undefined;\n if (u !== undefined && (typeof u !== \"string\" || u.length > 256)) return undefined;\n if (typeof v !== \"number\" || !Number.isInteger(v) || v < 1) return undefined;\n const state: Json = s === undefined ? null : (s as Json);\n const text = JSON.stringify(state);\n if (text === undefined || byteLength(text) > MAX_STATE_BYTES) return undefined;\n return u === undefined ? { id, s: state, v } : { id, u, s: state, v };\n}\n\nfunction parseEntry(value: unknown): StoredEntry | undefined {\n if (!isRecord(value)) return undefined;\n const { event, data, publisher, user } = value;\n if (!isEventName(event) || typeof data !== \"string\") return undefined;\n if (publisher !== undefined && (typeof publisher !== \"string\" || publisher.length > 256))\n return undefined;\n if (user !== undefined && (typeof user !== \"string\" || user.length > 256)) return undefined;\n return {\n event,\n data,\n ...(publisher === undefined ? {} : { publisher }),\n ...(user === undefined ? {} : { user }),\n };\n}\n\nfunction parseOptions(value: unknown): PublishOptions | undefined {\n if (!isRecord(value)) return undefined;\n const { history, historyTtlMs, key, idempotencyTtlMs } = value;\n if (!isCount(history, MAX_HISTORY)) return undefined;\n if (!isCount(historyTtlMs, MAX_TTL_MS * 366)) return undefined;\n if (!isCount(idempotencyTtlMs, MAX_TTL_MS)) return undefined;\n if (key !== undefined && !isIdempotencyKey(key)) return undefined;\n return key === undefined\n ? { history, historyTtlMs, idempotencyTtlMs }\n : { history, historyTtlMs, key, idempotencyTtlMs };\n}\n\n/**\n * Parses the JSON body of a request to operation `op`.\n *\n * @param op The operation from the path.\n * @param body The parsed JSON body.\n * @returns The request, or the refusal to answer.\n * @internal\n */\nexport function parseRequest(op: Operation, body: unknown): Parsed {\n if (!isRecord(body)) return fail(\"the body must be an object\");\n const { channel } = body;\n if (!isChannelName(channel)) return fail(\"bad channel name\", \"bad_channel\");\n switch (op) {\n case \"publish\": {\n const entry = parseEntry(body[\"entry\"]);\n const options = parseOptions(body[\"options\"]);\n const idleMs = body[\"idleMs\"] ?? 0;\n if (entry === undefined) return fail(\"bad entry\");\n if (options === undefined) return fail(\"bad options\");\n if (!isCount(idleMs, MAX_TTL_MS * 3650)) return fail(\"bad idleMs\");\n if (byteLength(entry.data) + byteLength(entry.event) > MAX_ENTRY_BYTES)\n return fail(\"the message is too large for one stored value\", \"too_large\");\n return { ok: true, request: { op, channel, entry, options, idleMs } };\n }\n case \"history\": {\n const { after, limit } = body;\n if (!isCount(after) || !isCount(limit, 100_000)) return fail(\"bad after or limit\");\n return { ok: true, request: { op, channel, after, limit } };\n }\n case \"presence-set\": {\n const member = parseMember(body[\"member\"]);\n const { ttlMs } = body;\n const graceMs = body[\"graceMs\"] ?? 0;\n if (member === undefined) return fail(\"bad member\");\n if (!isCount(ttlMs, MAX_TTL_MS) || ttlMs === 0) return fail(\"bad ttlMs\");\n if (!isCount(graceMs, 3_600_000)) return fail(\"bad graceMs\");\n return { ok: true, request: { op, channel, member, ttlMs, graceMs } };\n }\n case \"presence-remove\": {\n const { id } = body;\n if (typeof id !== \"string\" || id.length === 0 || id.length > 256) return fail(\"bad id\");\n return { ok: true, request: { op, channel, id } };\n }\n case \"presence-touch\": {\n const { ids, ttlMs } = body;\n if (!isCount(ttlMs, MAX_TTL_MS) || ttlMs === 0) return fail(\"bad ttlMs\");\n if (\n !Array.isArray(ids) ||\n ids.length > MAX_MEMBERS ||\n !ids.every((id) => typeof id === \"string\" && id.length > 0 && id.length <= 256)\n )\n return fail(\"bad ids\");\n return { ok: true, request: { op, channel, ids: ids as string[], ttlMs } };\n }\n case \"presence-list\":\n case \"presence-reap\":\n return { ok: true, request: { op, channel } };\n }\n}\n\n/** Decodes a frame from the object; `undefined` for anything that is not a known, well formed frame. @internal */\nexport function decodeLinkFrame(text: string): LinkFrame | undefined {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (!isRecord(value)) return undefined;\n switch (value[\"t\"]) {\n case \"ready\": {\n const { head, oldest } = value;\n return isCount(head) && isCount(oldest) ? { t: \"ready\", head, oldest } : undefined;\n }\n case \"snap\": {\n const list = value[\"members\"];\n if (!Array.isArray(list)) return undefined;\n const members: Member[] = [];\n for (const item of list) {\n const member = parseMember(item);\n if (member === undefined) return undefined;\n members.push(member);\n }\n return { t: \"snap\", members };\n }\n case \"msg\": {\n const message = parseStored(value[\"m\"]);\n return message === undefined ? undefined : { t: \"msg\", m: message };\n }\n case \"pres\": {\n const member = parseMember(value[\"member\"]);\n const op = value[\"op\"];\n if (member === undefined || (op !== \"join\" && op !== \"set\" && op !== \"leave\"))\n return undefined;\n return { t: \"pres\", op, member };\n }\n case \"more\":\n return { t: \"more\" };\n case \"bye\":\n return { t: \"bye\", why: typeof value[\"why\"] === \"string\" ? value[\"why\"] : \"\" };\n default:\n return undefined;\n }\n}\n\n/** Reads a stored message; `undefined` when it is malformed. @internal */\nexport function parseStored(value: unknown): StoredMessage | undefined {\n if (!isRecord(value)) return undefined;\n const { seq, event, data, at, publisher, user } = value;\n if (!isCount(seq) || seq < 1 || typeof event !== \"string\" || typeof data !== \"string\")\n return undefined;\n if (!isCount(at)) return undefined;\n if (publisher !== undefined && typeof publisher !== \"string\") return undefined;\n if (user !== undefined && typeof user !== \"string\") return undefined;\n return {\n seq,\n event,\n data,\n at,\n ...(publisher === undefined ? {} : { publisher }),\n ...(user === undefined ? {} : { user }),\n };\n}\n\n/** Whether `value` is a count (a non-negative safe integer). @internal */\nexport function isWholeNumber(value: unknown): value is number {\n return isCount(value);\n}\n\n/** The key of retained message `seq`: zero padded so keys sort by seq. @internal */\nexport function historyKey(seq: number): string {\n return `h:${String(seq).padStart(16, \"0\")}`;\n}\n\n/** The seq a history key stands for. @internal */\nexport function seqOfKey(key: string): number {\n return Number(key.slice(2));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloudflareAdapter as e}from"./cloudflare/adapter.js";import{RealtimeChannelObject as t}from"./cloudflare/object.js";export{t as RealtimeChannelObject,e as cloudflareAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":["RealtimeError","message","code","options"],"sources":["../../src/errors.ts"],"sourcesContent":["/**\n * The codes an error of this package carries. They are the same strings the wire protocol uses in `err` frames.\n * @since 1.0.0\n */\nexport type ErrorCode =\n | \"bad_frame\"\n | \"bad_channel\"\n | \"unauthorized\"\n | \"forbidden\"\n | \"not_subscribed\"\n | \"too_large\"\n | \"rate_limited\"\n | \"limit\"\n | \"timeout\"\n | \"unavailable\"\n | \"no_handler\"\n | \"handler_error\"\n | \"closed\"\n | \"queue_full\"\n | \"conflict\";\n\n/**\n * An error with a protocol code. A request handler that throws one answers the client with its code and message; any\n * other error answers `handler_error` with no message, so internals never reach a client.\n *\n * @example\n * import { RealtimeError } from \"@shadow-js/realtime\";\n *\n * const refusal = new RealtimeError(\"forbidden\", \"only moderators can pin messages\");\n * refusal.code; // \"forbidden\"\n * @since 1.0.0\n */\nexport class RealtimeError extends Error {\n readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message?: string, options?: { readonly cause?: unknown }) {\n super(message ?? code, options);\n this.name = \"RealtimeError\";\n this.code = code;\n }\n}\n"],"mappings":"AAgCA,IAAaA,EAAb,cAAmC,KAAM,CACvC,KAEA,YAAY,EAAiB,EAAkB,EAAwC,CACrF,MAAMC,GAAWC,EAAMC,CAAO,EAC9B,KAAK,KAAO,gBACZ,KAAK,KAAOD,CACd,CACF,E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{RealtimeError as e}from"./errors.js";import{isServer as t,onCleanup as n,useSignal as r}from"@shadow-js/core";function i(t){let n=()=>Promise.reject(new e(`closed`,`a realtime channel is not available during the server render`)),r=()=>()=>void 0;return{name:t,status:`idle`,lastSeq:0,on:r,onAny:r,onGap:r,onStatus:r,publish:n,request:n,presence:{members:()=>[],set:()=>void 0,leave:()=>void 0,on:r},close:()=>void 0}}function a(e,a,o){let s=t?i(a):e.channel(a,o),[c,l]=r(s.status),[u,d]=r(void 0);if(!t){let e=s.onStatus(e=>l(e)),t=s.onAny(e=>d(()=>e));n(()=>{e(),t()})}return{status:c,lastMessage:u,publish:(e,t,n)=>s.publish(e,t,n),request:(e,t,n)=>s.request(e,t,n),channel:s}}function o(e,r,i){t||n(e.on(r,i))}function s(e){let[i,a]=r(e.presence.members()),o=!1;if(!t){let t=e.presence.on(e=>a(()=>e));n(()=>{t(),o&&e.presence.leave()})}return[i,n=>{t||(n===void 0?(o=!1,e.presence.leave()):(o=!0,e.presence.set(n)))}]}function c(e){let[i,a]=r(e.status);return t||n(e.onStatus(e=>a(e))),i}export{a as useChannel,o as useChannelEvent,s as useChannelPresence,c as useRealtimeStatus};
|
|
2
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","names":["inertChannel","refuse","RealtimeError","none","useChannel","channel","isServer","name","rt","options","status","setStatus","useSignal","lastMessage","setLastMessage","stopStatus","next","stopMessages","message","onCleanup","event","data","publishOptions","requestOptions","useChannelEvent","handler","useChannelPresence","members","setMembers","joined","stop","list","state","useRealtimeStatus"],"sources":["../../src/hooks.ts"],"sourcesContent":["import type { Accessor } from \"@shadow-js/core\";\nimport { isServer, onCleanup, useSignal } from \"@shadow-js/core\";\nimport type {\n AnyMessage,\n Channel,\n ChannelDef,\n ChannelOptions,\n ChannelSchema,\n ChannelStatus,\n MessageMeta,\n PresenceOf,\n Realtime,\n RequestData,\n RequestReply,\n RequestsOf,\n Status,\n} from \"./client/types\";\nimport { RealtimeError } from \"./errors\";\nimport type { Member } from \"./protocol\";\n\n/**\n * What {@link useChannel} returns: the reactive state of a channel and the calls on it.\n * @since 1.0.0\n */\nexport interface ChannelHandle<Def extends ChannelDef> {\n /** The channel's status, as a signal. */\n readonly status: Accessor<ChannelStatus>;\n /** The latest message of any event, as a signal (undefined until one arrives). */\n readonly lastMessage: Accessor<AnyMessage<Def> | undefined>;\n /** Publishes a message; see `Channel.publish`. */\n readonly publish: Channel<Def>[\"publish\"];\n /** Sends a request to the server handler; see `Channel.request`. */\n readonly request: <E extends keyof RequestsOf<Def> & string>(\n event: E,\n data: RequestData<Def, E>,\n options?: { readonly timeoutMs?: number },\n ) => Promise<RequestReply<Def, E>>;\n /** The channel itself, for `onGap`, `presence` and the other calls. */\n readonly channel: Channel<Def>;\n}\n\n/** A channel for the server render: it never connects, listens or publishes. */\nfunction inertChannel<Def extends ChannelDef>(name: string): Channel<Def> {\n const refuse = (): Promise<never> =>\n Promise.reject(\n new RealtimeError(\"closed\", \"a realtime channel is not available during the server render\"),\n );\n const none = (): (() => void) => () => undefined;\n return {\n name,\n status: \"idle\",\n lastSeq: 0,\n on: none,\n onAny: none,\n onGap: none,\n onStatus: none,\n publish: refuse,\n request: refuse,\n presence: { members: () => [], set: () => undefined, leave: () => undefined, on: none },\n close: () => undefined,\n };\n}\n\n/**\n * Opens a channel for the owning component and releases it when the component is disposed: `status` and `lastMessage` are\n * signals, `publish` and `request` act on the channel. The channel subscribes while the component lives; other components may\n * share it. During the server render nothing connects.\n *\n * @param rt The client from `createRealtime`.\n * @param name The channel name (fixed for the life of the component; key the component to change it).\n * @param options Echo, presence and replay, applied when the channel is first created.\n * @returns The reactive handle.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Lobby() {\n * const lobby = useChannel(rt, \"chat:lobby\");\n * return <button onClick={() => lobby.publish(\"message\", { text: \"hi\" })}>{lobby.status()}</button>;\n * }\n * @since 1.0.0\n */\nexport function useChannel<Schema extends ChannelSchema, Name extends keyof Schema & string>(\n rt: Realtime<Schema>,\n name: Name,\n options?: ChannelOptions<PresenceOf<Schema[Name]>>,\n): ChannelHandle<Schema[Name]> {\n const channel: Channel<Schema[Name]> = isServer\n ? inertChannel<Schema[Name]>(name)\n : rt.channel(name, options);\n const [status, setStatus] = useSignal<ChannelStatus>(channel.status);\n const [lastMessage, setLastMessage] = useSignal<AnyMessage<Schema[Name]> | undefined>(undefined);\n if (!isServer) {\n const stopStatus = channel.onStatus((next) => setStatus(next));\n const stopMessages = channel.onAny((message) => setLastMessage(() => message));\n onCleanup(() => {\n stopStatus();\n stopMessages();\n });\n }\n return {\n status,\n lastMessage,\n publish: (event, data, publishOptions) => channel.publish(event, data, publishOptions),\n request: (event, data, requestOptions) => channel.request(event, data, requestOptions),\n channel,\n };\n}\n\n/**\n * Calls `handler` for each message of `event` on `channel` while the owning component lives.\n *\n * @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.\n * @param event The event name.\n * @param handler Called with the data and the message meta.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel, useChannelEvent } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Log() {\n * const { channel } = useChannel(rt, \"chat:lobby\");\n * useChannelEvent(channel, \"message\", (data) => console.log(data.text));\n * return <p>listening</p>;\n * }\n * @since 1.0.0\n */\nexport function useChannelEvent<Def extends ChannelDef, E extends keyof Def[\"events\"] & string>(\n channel: Channel<Def>,\n event: E,\n handler: (data: Def[\"events\"][E], meta: MessageMeta) => void,\n): void {\n if (isServer) return;\n onCleanup(channel.on(event, handler));\n}\n\n/**\n * The presence set of `channel` as a signal, and a setter that joins it (or changes this client's state) from the owning\n * component. Passing `undefined` leaves. The component leaves the set when it is disposed.\n *\n * @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.\n * @returns The members and `setPresence`.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel, useChannelPresence } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = {\n * [name: `chat:${string}`]: { events: { message: string }; presence: { name: string } };\n * };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Who() {\n * const { channel } = useChannel(rt, \"chat:lobby\");\n * const [members, setPresence] = useChannelPresence(channel);\n * setPresence({ name: \"ann\" });\n * return <p>{members().length} here</p>;\n * }\n * @since 1.0.0\n */\nexport function useChannelPresence<Def extends ChannelDef>(\n channel: Channel<Def>,\n): readonly [\n Accessor<readonly Member<PresenceOf<Def>>[]>,\n (state: PresenceOf<Def> | undefined) => void,\n] {\n const [members, setMembers] = useSignal<readonly Member<PresenceOf<Def>>[]>(\n channel.presence.members(),\n );\n let joined = false;\n if (!isServer) {\n const stop = channel.presence.on((list) => setMembers(() => list));\n onCleanup(() => {\n stop();\n if (joined) channel.presence.leave();\n });\n }\n return [\n members,\n (state) => {\n if (isServer) return;\n if (state === undefined) {\n joined = false;\n channel.presence.leave();\n } else {\n joined = true;\n channel.presence.set(state);\n }\n },\n ];\n}\n\n/**\n * The connection status of `rt` as a signal.\n *\n * @param rt The client from `createRealtime`.\n * @returns The status accessor.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useRealtimeStatus } from \"@shadow-js/realtime/hooks\";\n *\n * const rt = createRealtime({ url: \"/rt\" });\n *\n * function Badge() {\n * const status = useRealtimeStatus(rt);\n * return <span>{status()}</span>;\n * }\n * @since 1.0.0\n */\nexport function useRealtimeStatus<Schema extends ChannelSchema>(\n rt: Realtime<Schema>,\n): Accessor<Status> {\n const [status, setStatus] = useSignal<Status>(rt.status);\n if (!isServer) onCleanup(rt.onStatus((next) => setStatus(next)));\n return status;\n}\n"],"mappings":"A,qHA0CA,SAASA,EAAqC,EAA4B,CACxE,IAAMC,MACJ,QAAQ,OACN,IAAIC,EAAc,SAAU,8DAA8D,CAC5F,EACIC,UAAiC,IAAA,GACvC,MAAO,CACL,OACA,OAAQ,OACR,QAAS,EACT,GAAIA,EACJ,MAAOA,EACP,MAAOA,EACP,SAAUA,EACV,QAASF,EACT,QAASA,EACT,SAAU,CAAE,YAAe,CAAC,EAAG,QAAW,IAAA,GAAW,UAAa,IAAA,GAAW,GAAIE,CAAK,EACtF,UAAa,IAAA,EACf,CACF,CAwBA,SAAgBC,EACd,EACA,EACA,EAC6B,CAC7B,IAAMC,EAAiCC,EACnCN,EAA2BO,CAAI,EAC/BC,EAAG,QAAQD,EAAME,CAAO,EACtB,CAACC,EAAQC,GAAaC,EAAyBP,EAAQ,MAAM,EAC7D,CAACQ,EAAaC,GAAkBF,EAAgD,IAAA,EAAS,EAC/F,GAAI,CAACN,EAAU,CACb,IAAMS,EAAaV,EAAQ,SAAU,GAASM,EAAUK,CAAI,CAAC,EACvDC,EAAeZ,EAAQ,MAAO,GAAYS,MAAqBI,CAAO,CAAC,EAC7EC,MAAgB,CACdJ,EAAW,EACXE,EAAa,CACf,CAAC,CACH,CACA,MAAO,CACL,SACA,cACA,SAAU,EAAO,EAAM,IAAmBZ,EAAQ,QAAQe,EAAOC,EAAMC,CAAc,EACrF,SAAU,EAAO,EAAM,IAAmBjB,EAAQ,QAAQe,EAAOC,EAAME,CAAc,EACrF,SACF,CACF,CAsBA,SAAgBC,EACd,EACA,EACA,EACM,CACFlB,GACJa,EAAUd,EAAQ,GAAGe,EAAOK,CAAO,CAAC,CACtC,CAyBA,SAAgBC,EACd,EAIA,CACA,GAAM,CAACC,EAASC,GAAchB,EAC5BP,EAAQ,SAAS,QAAQ,CAC3B,EACIwB,EAAS,GACb,GAAI,CAACvB,EAAU,CACb,IAAMwB,EAAOzB,EAAQ,SAAS,GAAI,GAASuB,MAAiBG,CAAI,CAAC,EACjEZ,MAAgB,CACdW,EAAK,EACDD,GAAQxB,EAAQ,SAAS,MAAM,CACrC,CAAC,CACH,CACA,MAAO,CACLsB,EACC,GAAU,CACLrB,IACA0B,IAAU,IAAA,IACZ,EAAS,GACT3B,EAAQ,SAAS,MAAM,IAEvB,EAAS,GACTA,EAAQ,SAAS,IAAI2B,CAAK,GAE9B,CACF,CACF,CAmBA,SAAgBC,EACd,EACkB,CAClB,GAAM,CAACvB,EAAQC,GAAaC,EAAkBJ,EAAG,MAAM,EAEvD,OADKF,GAAUa,EAAUX,EAAG,SAAU,GAASG,EAAUK,CAAI,CAAC,CAAC,EACxDN,CACT,C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RealtimeError as e}from"./errors.js";import{PROTOCOL_VERSION as t,decodeClientFrame as n,decodeServerFrame as r,isChannelName as i,isEventName as a}from"./protocol.js";export{t as PROTOCOL_VERSION,e as RealtimeError,n as decodeClientFrame,r as decodeServerFrame,i as isChannelName,a as isEventName};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e={}){let t=e.now??Date.now,n=e.idleMs??36e5,r=1024,i=/* @__PURE__ */ new Map,a=/* @__PURE__ */ new Map,o=/* @__PURE__ */ new Map,s=[],c=!1,l=!1;function u(){let e=t()-n;for(let[t,r]of i)r.listeners.size===0&&r.lastActive<e&&(m(r,r.log.length,n),r.log.length===0&&i.delete(t));r=Math.max(1024,i.size*2)}function d(e){let n=i.get(e);return n===void 0&&(i.size>=r&&u(),n={lastActive:t(),seq:0,log:[],keys:/* @__PURE__ */ new Map,listeners:/* @__PURE__ */ new Set,pending:[],draining:!1},i.set(e,n)),n.lastActive=t(),n}function f(e){if(!e.draining){e.draining=!0;try{for(let t=e.pending.shift();t!==void 0;t=e.pending.shift())for(let n of[...e.listeners])try{n(t)}catch{}}finally{e.draining=!1}}}function p(e){if(s.push(e),!c){c=!0;try{for(let e=s.shift();e!==void 0;e=s.shift())for(let t of[...o.get(e.channel)??[]])try{t(e)}catch{}}finally{c=!1}}}function m(e,n,r){let i=t()-r,a=Math.max(0,e.log.length-n);for(;a<e.log.length&&(e.log[a]?.at??1/0)<i;)a+=1;a>0&&(e.log=e.log.slice(a))}function h(e){let t=a.get(e);return t===void 0&&(t=/* @__PURE__ */ new Map,a.set(e,t)),t}function g(){if(l)throw Error(`the adapter is closed.`)}return{async publish(e,n,r){g();let i=d(e),a=t();if(r.key!==void 0){let e=i.keys.get(r.key);if(e!==void 0&&e.expires>a)return{seq:e.seq,duplicate:!0};if(i.keys.size>=4096)for(let[e,t]of i.keys)t.expires<=a&&i.keys.delete(e)}i.seq+=1;let o={...n,seq:i.seq,at:a};return r.key!==void 0&&i.keys.set(r.key,{seq:i.seq,expires:a+r.idempotencyTtlMs}),r.history>0?(i.log.push(o),m(i,r.history,r.historyTtlMs)):i.log=[],i.pending.push(o),f(i),{seq:o.seq,duplicate:!1}},async subscribe(e,t){g();let n=d(e);return n.listeners.add(t),async()=>{n.listeners.delete(t)}},async history(e,t,n){g();let r=i.get(e);if(r===void 0)return{messages:[],head:0,oldest:0};let a=r.log,o=[];for(let e of a){if(o.length>=n)break;e.seq>t&&o.push(e)}return{messages:o,head:r.seq,oldest:a[0]?.seq??0}},async presenceSet(e,n,r){g();let i=h(e),a=i.get(n.id);a!==void 0&&a.member.v>n.v||(i.set(n.id,{member:n,expires:t()+r}),a===void 0?p({channel:e,op:`join`,member:n}):a.member.v<n.v&&p({channel:e,op:`set`,member:n}))},async presenceRemove(e,t){g();let n=a.get(e),r=n?.get(t);return n===void 0||r===void 0?!1:(n.delete(t),n.size===0&&a.delete(e),p({channel:e,op:`leave`,member:r.member}),!0)},async presenceList(e){g();let n=t(),r=[];for(let t of a.get(e)?.values()??[])t.expires>n&&r.push(t.member);return r},async presenceTouch(e,n){g();let r=t()+n,i=[];for(let t of e){let e=a.get(t.channel)?.get(t.id);e===void 0?i.push(t):e.expires=r}return i},async presenceReap(e){g();let n=a.get(e);if(n===void 0)return[];let r=t(),i=[];for(let[t,a]of[...n])a.expires>r||(n.delete(t),i.push(a.member),p({channel:e,op:`leave`,member:a.member}));return n.size===0&&a.delete(e),i},async presenceSubscribe(e,t){g();let n=o.get(e);n===void 0&&(n=/* @__PURE__ */ new Set,o.set(e,n)),n.add(t);let r=n;return async()=>{r.delete(t),r.size===0&&o.get(e)===r&&o.delete(e)}},async close(){l=!0,i.clear(),a.clear(),o.clear()}}}export{e as memoryAdapter};
|
|
2
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","names":["memoryAdapter","now","options","idleMs","sweepAt","channels","presence","presenceListeners","presenceQueue","presenceDraining","closed","sweep","cutoff","name","state","trim","channelOf","drain","next","listener","emitPresence","event","ttl","drop","history","live","set","check","channel","at","publish","known","key","value","message","entry","retained","messages","limit","after","member","ttlMs","id","out","expires","missing","ref","entries","removed","owner"],"sources":["../../src/memory.ts"],"sourcesContent":["import type {\n Adapter,\n HistoryResult,\n PresenceRef,\n PublishOptions,\n PublishResult,\n Unsubscribe,\n} from \"./adapter\";\nimport type { Member, PresenceEvent, StoredEntry, StoredMessage } from \"./protocol\";\n\ninterface ChannelState {\n seq: number;\n log: StoredMessage[];\n readonly keys: Map<string, { readonly seq: number; readonly expires: number }>;\n readonly listeners: Set<(message: StoredMessage) => void>;\n readonly pending: StoredMessage[];\n draining: boolean;\n lastActive: number;\n}\n\ninterface PresenceEntry {\n member: Member;\n expires: number;\n}\n\n/**\n * Options of {@link memoryAdapter}.\n * @since 1.0.0\n */\nexport interface MemoryAdapterOptions {\n /** The clock, in epoch milliseconds. Default: `Date.now`. */\n readonly now?: () => number;\n /**\n * Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its\n * numbering restarts, which a resuming client sees as `reset`. Default: one hour.\n */\n readonly idleMs?: number;\n}\n\n/**\n * The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory\n * of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis\n * or Cloudflare adapter when more than one process serves clients.\n *\n * @param options The clock.\n * @returns The adapter.\n * @example\n * import { createHub, memoryAdapter } from \"@shadow-js/realtime/server\";\n *\n * const adapter = memoryAdapter();\n * const hub = createHub({ adapter });\n * void hub.close();\n * @since 1.0.0\n */\nexport function memoryAdapter(options: MemoryAdapterOptions = {}): Adapter {\n const now = options.now ?? Date.now;\n const idleMs = options.idleMs ?? 3_600_000;\n let sweepAt = 1024;\n const channels = new Map<string, ChannelState>();\n const presence = new Map<string, Map<string, PresenceEntry>>();\n const presenceListeners = new Map<string, Set<(event: PresenceEvent) => void>>();\n const presenceQueue: PresenceEvent[] = [];\n let presenceDraining = false;\n let closed = false;\n\n /** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */\n function sweep(): void {\n const cutoff = now() - idleMs;\n for (const [name, state] of channels)\n if (state.listeners.size === 0 && state.lastActive < cutoff) {\n trim(state, state.log.length, idleMs);\n if (state.log.length === 0) channels.delete(name);\n }\n sweepAt = Math.max(1024, channels.size * 2);\n }\n\n function channelOf(name: string): ChannelState {\n let state = channels.get(name);\n if (state === undefined) {\n if (channels.size >= sweepAt) sweep();\n state = {\n lastActive: now(),\n seq: 0,\n log: [],\n keys: new Map(),\n listeners: new Set(),\n pending: [],\n draining: false,\n };\n channels.set(name, state);\n }\n state.lastActive = now();\n return state;\n }\n\n /** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */\n function drain(state: ChannelState): void {\n if (state.draining) return;\n state.draining = true;\n try {\n for (let next = state.pending.shift(); next !== undefined; next = state.pending.shift()) {\n for (const listener of [...state.listeners]) {\n try {\n listener(next);\n } catch {\n // A listener's failure is its own: delivery to the others goes on (guarantee G13).\n }\n }\n }\n } finally {\n state.draining = false;\n }\n }\n\n function emitPresence(event: PresenceEvent): void {\n presenceQueue.push(event);\n if (presenceDraining) return;\n presenceDraining = true;\n try {\n for (let next = presenceQueue.shift(); next !== undefined; next = presenceQueue.shift()) {\n for (const listener of [...(presenceListeners.get(next.channel) ?? [])]) {\n try {\n listener(next);\n } catch {\n // See `drain`.\n }\n }\n }\n } finally {\n presenceDraining = false;\n }\n }\n\n function trim(state: ChannelState, history: number, ttl: number): void {\n const cutoff = now() - ttl;\n let drop = Math.max(0, state.log.length - history);\n while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;\n if (drop > 0) state.log = state.log.slice(drop);\n }\n\n function live(name: string): Map<string, PresenceEntry> {\n let set = presence.get(name);\n if (set === undefined) {\n set = new Map();\n presence.set(name, set);\n }\n return set;\n }\n\n function check(): void {\n if (closed) throw new Error(\"the adapter is closed.\");\n }\n\n return {\n async publish(\n channel: string,\n entry: StoredEntry,\n publish: PublishOptions,\n ): Promise<PublishResult> {\n check();\n const state = channelOf(channel);\n const at = now();\n if (publish.key !== undefined) {\n const known = state.keys.get(publish.key);\n if (known !== undefined && known.expires > at) return { seq: known.seq, duplicate: true };\n // Expired keys are swept when a new one is added, so the map stays bounded by the rate of publishes.\n if (state.keys.size >= 4096)\n for (const [key, value] of state.keys) if (value.expires <= at) state.keys.delete(key);\n }\n state.seq += 1;\n const message: StoredMessage = { ...entry, seq: state.seq, at };\n if (publish.key !== undefined)\n state.keys.set(publish.key, {\n seq: state.seq,\n expires: at + publish.idempotencyTtlMs,\n });\n if (publish.history > 0) {\n state.log.push(message);\n trim(state, publish.history, publish.historyTtlMs);\n } else {\n state.log = [];\n }\n state.pending.push(message);\n drain(state);\n return { seq: message.seq, duplicate: false };\n },\n\n async subscribe(\n channel: string,\n listener: (message: StoredMessage) => void,\n ): Promise<Unsubscribe> {\n check();\n const state = channelOf(channel);\n state.listeners.add(listener);\n return async () => {\n state.listeners.delete(listener);\n };\n },\n\n async history(channel: string, after: number, limit: number): Promise<HistoryResult> {\n check();\n const state = channels.get(channel);\n if (state === undefined) return { messages: [], head: 0, oldest: 0 };\n const retained = state.log;\n const messages: StoredMessage[] = [];\n for (const message of retained) {\n if (messages.length >= limit) break;\n if (message.seq > after) messages.push(message);\n }\n return { messages, head: state.seq, oldest: retained[0]?.seq ?? 0 };\n },\n\n async presenceSet(channel: string, member: Member, ttlMs: number): Promise<void> {\n check();\n const set = live(channel);\n const known = set.get(member.id);\n if (known !== undefined && known.member.v > member.v) return;\n set.set(member.id, { member, expires: now() + ttlMs });\n if (known === undefined) emitPresence({ channel, op: \"join\", member });\n else if (known.member.v < member.v) emitPresence({ channel, op: \"set\", member });\n },\n\n async presenceRemove(channel: string, id: string): Promise<boolean> {\n check();\n const set = presence.get(channel);\n const known = set?.get(id);\n if (set === undefined || known === undefined) return false;\n set.delete(id);\n if (set.size === 0) presence.delete(channel);\n emitPresence({ channel, op: \"leave\", member: known.member });\n return true;\n },\n\n async presenceList(channel: string): Promise<readonly Member[]> {\n check();\n const at = now();\n const out: Member[] = [];\n for (const entry of presence.get(channel)?.values() ?? [])\n if (entry.expires > at) out.push(entry.member);\n return out;\n },\n\n async presenceTouch(entries, ttlMs): Promise<readonly PresenceRef[]> {\n check();\n const expires = now() + ttlMs;\n const missing: PresenceRef[] = [];\n for (const ref of entries) {\n const entry = presence.get(ref.channel)?.get(ref.id);\n if (entry === undefined) missing.push(ref);\n else entry.expires = expires;\n }\n return missing;\n },\n\n async presenceReap(channel: string): Promise<readonly Member[]> {\n check();\n const set = presence.get(channel);\n if (set === undefined) return [];\n const at = now();\n const removed: Member[] = [];\n for (const [id, entry] of [...set]) {\n if (entry.expires > at) continue;\n set.delete(id);\n removed.push(entry.member);\n emitPresence({ channel, op: \"leave\", member: entry.member });\n }\n if (set.size === 0) presence.delete(channel);\n return removed;\n },\n\n async presenceSubscribe(\n channel: string,\n listener: (event: PresenceEvent) => void,\n ): Promise<Unsubscribe> {\n check();\n let set = presenceListeners.get(channel);\n if (set === undefined) {\n set = new Set();\n presenceListeners.set(channel, set);\n }\n set.add(listener);\n const owner = set;\n return async () => {\n owner.delete(listener);\n if (owner.size === 0 && presenceListeners.get(channel) === owner)\n presenceListeners.delete(channel);\n };\n },\n\n async close(): Promise<void> {\n closed = true;\n channels.clear();\n presence.clear();\n presenceListeners.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=1,t=/^[A-Za-z0-9_.:/@-]{1,128}$/,n=/^[A-Za-z0-9_.:@-]{1,64}$/,r=/^[A-Za-z0-9_.:@-]{1,64}$/,i=new TextEncoder;function a(e){return typeof e==`string`&&t.test(e)}function o(e){return typeof e==`string`&&n.test(e)}function s(e){return typeof e==`string`&&r.test(e)}function c(e){if(e.length*3<=2147483647){let t=!0;for(let n=0;n<e.length;n+=1)if(e.charCodeAt(n)>127){t=!1;break}if(t)return e.length}return i.encode(e).length}function l(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function u(e,t){return l(t)?{ok:!1,code:`bad_frame`,n:t,why:e}:{ok:!1,code:`bad_frame`,why:e}}function d(e,t){if(c(e)>t)return{ok:!1,code:`too_large`,why:`the frame is larger than the limit`};let n;try{n=JSON.parse(e)}catch{return u(`the frame is not JSON`)}if(typeof n!=`object`||!n||Array.isArray(n))return u(`the frame is not an object`);let r=n,i=r.t,d=r.n;if(d!==void 0&&!l(d))return u(`n must be a non-negative integer`);let f=r.c,p=()=>a(f)?void 0:{...u(`c is not a valid channel name`,d),code:`bad_channel`};switch(i){case`hello`:{let e=r.v;return typeof e!=`number`||!Number.isInteger(e)||e<1?u(`v must be a version`):{ok:!0,frame:{t:`hello`,v:e}}}case`ping`:return{ok:!0,frame:d===void 0?{t:`ping`}:{t:`ping`,n:d}};case`sub`:{let e=p();if(e!==void 0)return e;if(d===void 0)return u(`sub needs n`);let t=r.after;if(t!==void 0&&!l(t))return u(`after must be a non-negative integer`,d);let n=r.echo;if(n!==void 0&&typeof n!=`boolean`)return u(`echo must be a boolean`,d);let i=r.watch;if(i!==void 0&&typeof i!=`boolean`)return u(`watch must be a boolean`,d);let a=`presence`in r;return{ok:!0,frame:{t:`sub`,n:d,c:f,hasPresence:a,...t===void 0?{}:{after:t},...n===void 0?{}:{echo:n},...i===void 0?{}:{watch:i},...a?{presence:r.presence}:{}}}}case`unsub`:{let e=p();return e===void 0?{ok:!0,frame:d===void 0?{t:`unsub`,c:f}:{t:`unsub`,n:d,c:f}}:e}case`pub`:{let e=p();if(e!==void 0)return e;let t=r.e;if(!o(t))return u(`e is not a valid event name`,d);let n=r.k;return n!==void 0&&!s(n)?u(`k is not a valid key`,d):{ok:!0,frame:{t:`pub`,c:f,e:t,d:JSON.stringify(r.d??null),...d===void 0?{}:{n:d},...n===void 0?{}:{k:n}}}}case`req`:{let e=p();if(e!==void 0)return e;if(d===void 0)return u(`req needs n`);let t=r.e;if(!o(t))return u(`e is not a valid event name`,d);let n=r.ms;return n!==void 0&&!(typeof n==`number`&&Number.isFinite(n)&&n>0)?u(`ms must be a positive number`,d):{ok:!0,frame:{t:`req`,n:d,c:f,e:t,d:r.d??null,...n===void 0?{}:{ms:n}}}}case`pres`:{let e=p();return e===void 0?{ok:!0,frame:{t:`pres`,c:f,s:r.s??null}}:e}default:return u(`unknown frame type`,d)}}function f(e){let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t||Array.isArray(t))return;let n=t,r=e=>{let t=n[e];return typeof t==`string`?t:void 0},i=e=>{let t=n[e];return l(t)?t:void 0};switch(n.t){case`ready`:{let e=i(`v`),t=r(`id`);return e===void 0||t===void 0?void 0:{t:`ready`,v:e,id:t,hb:i(`hb`)??3e4,max:i(`max`)??65536}}case`subbed`:{let e=i(`n`),t=r(`c`),a=i(`head`);return e===void 0||t===void 0||a===void 0?void 0:{t:`subbed`,n:e,c:t,head:a,...n.gap===!0?{gap:!0}:{},...n.reset===!0?{reset:!0}:{}}}case`unsubbed`:{let e=r(`c`);if(e===void 0)return;let t=r(`why`);return t===void 0?{t:`unsubbed`,c:e}:{t:`unsubbed`,c:e,why:t}}case`msg`:{let e=r(`c`),t=i(`s`),a=r(`e`);if(e===void 0||t===void 0||a===void 0||t<1)return;let o=r(`p`),s=r(`u`);return{t:`msg`,c:e,s:t,e:a,d:n.d,...o===void 0?{}:{p:o},...s===void 0?{}:{u:s},...n.r===1?{r:1}:{}}}case`ack`:{let e=i(`n`),t=i(`s`);return e===void 0||t===void 0?void 0:{t:`ack`,n:e,s:t,...n.dup===!0?{dup:!0}:{}}}case`rep`:{let e=i(`n`);return e===void 0?void 0:{t:`rep`,n:e,d:n.d}}case`err`:{let e=r(`code`);if(e===void 0)return;let t=i(`n`),n=r(`c`),a=r(`msg`);return{t:`err`,code:e,...t===void 0?{}:{n:t},...n===void 0?{}:{c:n},...a===void 0?{}:{msg:a}}}case`pres`:{let e=r(`c`),t=n.op,i=n.m;if(e===void 0||t!==`sync`&&t!==`join`&&t!==`set`&&t!==`leave`)return;let a=e=>{if(typeof e!=`object`||!e)return;let t=e;if(typeof t.id==`string`&&l(t.v))return{id:t.id,v:t.v,s:t.s??null,...typeof t.u==`string`?{u:t.u}:{}}};if(t===`sync`){if(!Array.isArray(i))return;let n=i.map(a);return n.some(e=>e===void 0)?void 0:{t:`pres`,c:e,op:t,m:n}}let o=a(i);return o===void 0?void 0:{t:`pres`,c:e,op:t,m:o}}case`pong`:{let e=i(`n`);return e===void 0?{t:`pong`}:{t:`pong`,n:e}}case`bye`:{let e=i(`code`);return e===void 0?void 0:{t:`bye`,code:e,why:r(`why`)??``}}default:return}}export{e as PROTOCOL_VERSION,c as byteLength,d as decodeClientFrame,f as decodeServerFrame,a as isChannelName,o as isEventName,s as isIdempotencyKey};
|
|
2
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","names":["PROTOCOL_VERSION","CHANNEL","EVENT","KEY","encoder","isChannelName","value","isEventName","isIdempotencyKey","byteLength","text","ascii","at","isCount","bad","n","decodeClientFrame","maxBytes","raw","type","\"t\"","\"n\"","c","\"c\"","needChannel","v","\"v\"","refusal","after","\"after\"","echo","\"echo\"","watch","\"watch\"","hasPresence","\"presence\"","e","\"e\"","k","\"k\"","\"d\"","ms","\"ms\"","\"s\"","decodeServerFrame","str","item","key","num","id","head","\"gap\"","\"reset\"","why","s","p","u","\"r\"","\"dup\"","code","msg","op","\"op\"","m","\"m\"","member","entry","\"id\"","\"u\"","list","one"],"sources":["../../src/protocol.ts"],"sourcesContent":["import type { ErrorCode } from \"./errors\";\n\n/**\n * The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).\n * @example\n * import { PROTOCOL_VERSION } from \"@shadow-js/realtime\";\n *\n * const hello = JSON.stringify({ t: \"hello\", v: PROTOCOL_VERSION });\n * @since 1.0.0\n */\nexport const PROTOCOL_VERSION: 1 = 1;\n\n/**\n * Any JSON value.\n * @since 1.0.0\n */\nexport type Json =\n | null\n | boolean\n | number\n | string\n | readonly Json[]\n | { readonly [key: string]: Json };\n\n/**\n * One member of a channel's presence set: the connection that joined, its user, its state, and the version of that state.\n * @since 1.0.0\n */\nexport interface Member<State = Json> {\n /** The connection id. */\n readonly id: string;\n /** The user of the connection's identity, when it has one. */\n readonly u?: string;\n /** The member's state. */\n readonly s: State;\n /** A counter that rises with every state change of this member, from 1. */\n readonly v: number;\n}\n\n/** A message as stored by an adapter: the data is its JSON text. @since 1.0.0 */\nexport interface StoredMessage {\n readonly seq: number;\n readonly event: string;\n /** The JSON text of the data. */\n readonly data: string;\n /** The publishing connection id, when a connection published it. */\n readonly publisher?: string;\n /** The publishing user, when it has one. */\n readonly user?: string;\n /** When the adapter accepted it, in epoch milliseconds. */\n readonly at: number;\n}\n\n/** What a publisher supplies; the adapter adds `seq` and `at`. @since 1.0.0 */\nexport type StoredEntry = Omit<StoredMessage, \"seq\" | \"at\">;\n\n/** A change of a presence set, as adapters deliver it. @since 1.0.0 */\nexport interface PresenceEvent {\n readonly channel: string;\n readonly op: \"join\" | \"set\" | \"leave\";\n readonly member: Member;\n}\n\n/** Frames a client sends. @since 1.0.0 */\nexport type ClientFrame =\n | { readonly t: \"hello\"; readonly v: number }\n | {\n readonly t: \"sub\";\n readonly n: number;\n readonly c: string;\n readonly after?: number;\n readonly echo?: boolean;\n readonly presence?: Json;\n readonly hasPresence: boolean;\n readonly watch?: boolean;\n }\n | { readonly t: \"unsub\"; readonly n?: number; readonly c: string }\n | {\n readonly t: \"pub\";\n readonly c: string;\n readonly e: string;\n readonly d: string;\n readonly n?: number;\n readonly k?: string;\n }\n | {\n readonly t: \"req\";\n readonly n: number;\n readonly c: string;\n readonly e: string;\n readonly d: Json;\n readonly ms?: number;\n }\n | { readonly t: \"pres\"; readonly c: string; readonly s: Json }\n | { readonly t: \"ping\"; readonly n?: number };\n\n/** What decoding a client frame yields. @since 1.0.0 */\nexport type DecodedClientFrame =\n | { readonly ok: true; readonly frame: ClientFrame }\n | { readonly ok: false; readonly code: ErrorCode; readonly n?: number; readonly why: string };\n\nconst CHANNEL = /^[A-Za-z0-9_.:/@-]{1,128}$/;\nconst EVENT = /^[A-Za-z0-9_.:@-]{1,64}$/;\nconst KEY = /^[A-Za-z0-9_.:@-]{1,64}$/;\nconst encoder = new TextEncoder();\n\n/**\n * Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isChannelName } from \"@shadow-js/realtime\";\n *\n * isChannelName(\"chat:lobby\"); // true\n * isChannelName(\"chat lobby\"); // false\n * @since 1.0.0\n */\nexport function isChannelName(value: unknown): value is string {\n return typeof value === \"string\" && CHANNEL.test(value);\n}\n\n/**\n * Whether `value` is a valid event name: 1..64 characters of `A-Za-z0-9_.:@-`.\n * @param value The candidate.\n * @returns True for a valid name.\n * @example\n * import { isEventName } from \"@shadow-js/realtime\";\n *\n * isEventName(\"message\"); // true\n * isEventName(\"a/b\"); // false\n * @since 1.0.0\n */\nexport function isEventName(value: unknown): value is string {\n return typeof value === \"string\" && EVENT.test(value);\n}\n\n/** Whether `value` is a valid idempotency key. @internal */\nexport function isIdempotencyKey(value: unknown): value is string {\n return typeof value === \"string\" && KEY.test(value);\n}\n\n/** The number of bytes of `text` in UTF-8, without encoding the common cases. @internal */\nexport function byteLength(text: string): number {\n // Every UTF-16 unit is at most 3 bytes in UTF-8, and at least 1.\n if (text.length * 3 <= 0x7fffffff) {\n let ascii = true;\n for (let at = 0; at < text.length; at += 1) {\n if (text.charCodeAt(at) > 0x7f) {\n ascii = false;\n break;\n }\n }\n if (ascii) return text.length;\n }\n return encoder.encode(text).length;\n}\n\nfunction isCount(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\ntype Refusal = Extract<DecodedClientFrame, { ok: false }>;\n\nfunction bad(why: string, n?: unknown): Refusal {\n return isCount(n)\n ? { ok: false, code: \"bad_frame\", n, why }\n : { ok: false, code: \"bad_frame\", why };\n}\n\n/**\n * Decodes and validates one client frame (server side). Never throws: any text yields a frame or a refusal.\n *\n * @param text The frame text.\n * @param maxBytes The largest accepted frame, in UTF-8 bytes.\n * @returns The frame, or the code and reason to refuse it with.\n * @example\n * import { decodeClientFrame } from \"@shadow-js/realtime\";\n *\n * const result = decodeClientFrame('{\"t\":\"ping\"}', 65_536);\n * result.ok; // true\n * @since 1.0.0\n */\nexport function decodeClientFrame(text: string, maxBytes: number): DecodedClientFrame {\n if (byteLength(text) > maxBytes)\n return { ok: false, code: \"too_large\", why: \"the frame is larger than the limit\" };\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return bad(\"the frame is not JSON\");\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return bad(\"the frame is not an object\");\n const raw = value as Record<string, unknown>;\n const type = raw[\"t\"];\n const n = raw[\"n\"];\n if (n !== undefined && !isCount(n)) return bad(\"n must be a non-negative integer\");\n const c = raw[\"c\"];\n const needChannel = (): Refusal | undefined =>\n isChannelName(c)\n ? undefined\n : { ...bad(\"c is not a valid channel name\", n), code: \"bad_channel\" };\n switch (type) {\n case \"hello\": {\n const v = raw[\"v\"];\n if (typeof v !== \"number\" || !Number.isInteger(v) || v < 1) return bad(\"v must be a version\");\n return { ok: true, frame: { t: \"hello\", v } };\n }\n case \"ping\":\n return { ok: true, frame: n === undefined ? { t: \"ping\" } : { t: \"ping\", n } };\n case \"sub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(\"sub needs n\");\n const after = raw[\"after\"];\n if (after !== undefined && !isCount(after))\n return bad(\"after must be a non-negative integer\", n);\n const echo = raw[\"echo\"];\n if (echo !== undefined && typeof echo !== \"boolean\") return bad(\"echo must be a boolean\", n);\n const watch = raw[\"watch\"];\n if (watch !== undefined && typeof watch !== \"boolean\")\n return bad(\"watch must be a boolean\", n);\n const hasPresence = \"presence\" in raw;\n return {\n ok: true,\n frame: {\n t: \"sub\",\n n,\n c: c as string,\n hasPresence,\n ...(after === undefined ? {} : { after }),\n ...(echo === undefined ? {} : { echo }),\n ...(watch === undefined ? {} : { watch }),\n ...(hasPresence ? { presence: raw[\"presence\"] as Json } : {}),\n },\n };\n }\n case \"unsub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return {\n ok: true,\n frame: n === undefined ? { t: \"unsub\", c: c as string } : { t: \"unsub\", n, c: c as string },\n };\n }\n case \"pub\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n const e = raw[\"e\"];\n if (!isEventName(e)) return bad(\"e is not a valid event name\", n);\n const k = raw[\"k\"];\n if (k !== undefined && !isIdempotencyKey(k)) return bad(\"k is not a valid key\", n);\n const data = JSON.stringify(raw[\"d\"] ?? null);\n return {\n ok: true,\n frame: {\n t: \"pub\",\n c: c as string,\n e,\n d: data,\n ...(n === undefined ? {} : { n }),\n ...(k === undefined ? {} : { k }),\n },\n };\n }\n case \"req\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n if (n === undefined) return bad(\"req needs n\");\n const e = raw[\"e\"];\n if (!isEventName(e)) return bad(\"e is not a valid event name\", n);\n const ms = raw[\"ms\"];\n if (ms !== undefined && !(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0))\n return bad(\"ms must be a positive number\", n);\n return {\n ok: true,\n frame: {\n t: \"req\",\n n,\n c: c as string,\n e,\n d: (raw[\"d\"] ?? null) as Json,\n ...(ms === undefined ? {} : { ms }),\n },\n };\n }\n case \"pres\": {\n const refusal = needChannel();\n if (refusal !== undefined) return refusal;\n return { ok: true, frame: { t: \"pres\", c: c as string, s: (raw[\"s\"] ?? null) as Json } };\n }\n default:\n return bad(\"unknown frame type\", n);\n }\n}\n\n/** Frames a server sends, as a client reads them. @since 1.0.0 */\nexport type ServerFrame =\n | {\n readonly t: \"ready\";\n readonly v: number;\n readonly id: string;\n readonly hb: number;\n readonly max: number;\n }\n | {\n readonly t: \"subbed\";\n readonly n: number;\n readonly c: string;\n readonly head: number;\n readonly gap?: boolean;\n readonly reset?: boolean;\n }\n | { readonly t: \"unsubbed\"; readonly c: string; readonly why?: string }\n | {\n readonly t: \"msg\";\n readonly c: string;\n readonly s: number;\n readonly e: string;\n readonly d: Json;\n readonly p?: string;\n readonly u?: string;\n readonly r?: 1;\n }\n | { readonly t: \"ack\"; readonly n: number; readonly s: number; readonly dup?: boolean }\n | { readonly t: \"rep\"; readonly n: number; readonly d: Json }\n | {\n readonly t: \"err\";\n readonly n?: number;\n readonly c?: string;\n readonly code: ErrorCode;\n readonly msg?: string;\n }\n | {\n readonly t: \"pres\";\n readonly c: string;\n readonly op: \"sync\" | \"join\" | \"set\" | \"leave\";\n readonly m: Member | readonly Member[];\n }\n | { readonly t: \"pong\"; readonly n?: number }\n | { readonly t: \"bye\"; readonly code: number; readonly why: string };\n\n/**\n * Decodes one server frame (client side). Returns `undefined` for anything that is not a known, well-formed frame, so a\n * client ignores what a newer server may send and never acts on a malformed frame.\n *\n * @param text The frame text.\n * @returns The frame, or undefined.\n * @example\n * import { decodeServerFrame } from \"@shadow-js/realtime\";\n *\n * decodeServerFrame('{\"t\":\"pong\"}'); // { t: \"pong\" }\n * decodeServerFrame(\"not json\"); // undefined\n * @since 1.0.0\n */\nexport function decodeServerFrame(text: string): ServerFrame | undefined {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const str = (key: string): string | undefined => {\n const item = raw[key];\n return typeof item === \"string\" ? item : undefined;\n };\n const num = (key: string): number | undefined => {\n const item = raw[key];\n return isCount(item) ? item : undefined;\n };\n switch (raw[\"t\"]) {\n case \"ready\": {\n const v = num(\"v\");\n const id = str(\"id\");\n if (v === undefined || id === undefined) return undefined;\n return { t: \"ready\", v, id, hb: num(\"hb\") ?? 30_000, max: num(\"max\") ?? 65_536 };\n }\n case \"subbed\": {\n const n = num(\"n\");\n const c = str(\"c\");\n const head = num(\"head\");\n if (n === undefined || c === undefined || head === undefined) return undefined;\n return {\n t: \"subbed\",\n n,\n c,\n head,\n ...(raw[\"gap\"] === true ? { gap: true } : {}),\n ...(raw[\"reset\"] === true ? { reset: true } : {}),\n };\n }\n case \"unsubbed\": {\n const c = str(\"c\");\n if (c === undefined) return undefined;\n const why = str(\"why\");\n return why === undefined ? { t: \"unsubbed\", c } : { t: \"unsubbed\", c, why };\n }\n case \"msg\": {\n const c = str(\"c\");\n const s = num(\"s\");\n const e = str(\"e\");\n if (c === undefined || s === undefined || e === undefined || s < 1) return undefined;\n const p = str(\"p\");\n const u = str(\"u\");\n return {\n t: \"msg\",\n c,\n s,\n e,\n d: raw[\"d\"] as Json,\n ...(p === undefined ? {} : { p }),\n ...(u === undefined ? {} : { u }),\n ...(raw[\"r\"] === 1 ? { r: 1 as const } : {}),\n };\n }\n case \"ack\": {\n const n = num(\"n\");\n const s = num(\"s\");\n if (n === undefined || s === undefined) return undefined;\n return { t: \"ack\", n, s, ...(raw[\"dup\"] === true ? { dup: true } : {}) };\n }\n case \"rep\": {\n const n = num(\"n\");\n if (n === undefined) return undefined;\n return { t: \"rep\", n, d: raw[\"d\"] as Json };\n }\n case \"err\": {\n const code = str(\"code\");\n if (code === undefined) return undefined;\n const n = num(\"n\");\n const c = str(\"c\");\n const msg = str(\"msg\");\n return {\n t: \"err\",\n code: code as ErrorCode,\n ...(n === undefined ? {} : { n }),\n ...(c === undefined ? {} : { c }),\n ...(msg === undefined ? {} : { msg }),\n };\n }\n case \"pres\": {\n const c = str(\"c\");\n const op = raw[\"op\"];\n const m = raw[\"m\"];\n if (c === undefined || (op !== \"sync\" && op !== \"join\" && op !== \"set\" && op !== \"leave\"))\n return undefined;\n const member = (item: unknown): Member | undefined => {\n if (item === null || typeof item !== \"object\") return undefined;\n const entry = item as Record<string, unknown>;\n if (typeof entry[\"id\"] !== \"string\" || !isCount(entry[\"v\"])) return undefined;\n return {\n id: entry[\"id\"],\n v: entry[\"v\"],\n s: (entry[\"s\"] ?? null) as Json,\n ...(typeof entry[\"u\"] === \"string\" ? { u: entry[\"u\"] } : {}),\n };\n };\n if (op === \"sync\") {\n if (!Array.isArray(m)) return undefined;\n const list = m.map(member);\n if (list.some((item) => item === undefined)) return undefined;\n return { t: \"pres\", c, op, m: list as Member[] };\n }\n const one = member(m);\n return one === undefined ? undefined : { t: \"pres\", c, op, m: one };\n }\n case \"pong\": {\n const n = num(\"n\");\n return n === undefined ? { t: \"pong\" } : { t: \"pong\", n };\n }\n case \"bye\": {\n const code = num(\"code\");\n return code === undefined ? undefined : { t: \"bye\", code, why: str(\"why\") ?? \"\" };\n }\n default:\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(){let e=/* @__PURE__ */ new Set;return{add(t){return e.add(t),()=>{e.delete(t)}},fire(...t){for(let n of[...e])try{n(...t)}catch{}}}}function t(t){return{sendCommand:e=>{let[n,...r]=e;return t.call(n??``,...r)},createSubscriber:()=>{let n=t.duplicate(),r=/* @__PURE__ */ new Map,i=e(),a=e(),o=0;return n.on(`message`,((e,t)=>{r.get(e)?.(t)})),n.on(`ready`,(()=>{o+=1,o>1&&i.fire()})),n.on(`error`,(e=>a.fire(e))),{async subscribe(e,t){r.set(e,t),await n.subscribe(e)},async unsubscribe(e){r.delete(e),await n.unsubscribe(e)},onReconnect:e=>i.add(e),onError:e=>a.add(e),async close(){r.clear();try{await n.quit()}catch{n.disconnect?.()}}}}}}function n(t){return{sendCommand:e=>t.sendCommand([...e]),createSubscriber:async()=>{let n=t.duplicate(),r=/* @__PURE__ */ new Map,i=(e,t)=>{r.get(t)?.(e)},a=e(),o=e(),s=0;n.on(`ready`,(()=>{s+=1,s>1&&a.fire()})),n.on(`error`,(e=>o.fire(e))),await n.connect();let c=/* @__PURE__ */ new Set;return{async subscribe(e,t){if(r.set(e,t),!c.has(e)){c.add(e);try{await n.subscribe(e,i)}catch(t){throw c.delete(e),r.delete(e),t}}},async unsubscribe(e){r.delete(e),c.delete(e)&&await n.unsubscribe(e)},onReconnect:e=>a.add(e),onError:e=>o.add(e),async close(){r.clear(),c.clear(),n.close===void 0?n.quit===void 0?await n.disconnect?.():await n.quit():await n.close()}}}}}export{t as fromIoredis,n as fromNodeRedis};
|
|
2
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["handlers","set","handler","args","fromIoredis","command","rest","client","connection","topics","reconnects","errors","ready","topic","message","error","listener","fromNodeRedis","dispatch","registered"],"sources":["../../../src/redis/client.ts"],"sourcesContent":["/**\n * What the Redis adapter needs from a Redis library, and the two wrappers that adapt the common ones. The adapter has no\n * dependency on a Redis package: it is written against these small interfaces, which ioredis and node-redis satisfy through\n * {@link fromIoredis} and {@link fromNodeRedis}, and which anything else (Upstash over TCP, a mock, your own pool) can implement.\n */\n\n/**\n * A connection that runs commands. Commands issued without waiting for the previous one are pipelined by the library, which the\n * adapter relies on for throughput.\n *\n * @example\n * import type { RedisClient } from \"@shadow-js/realtime/redis\";\n *\n * declare function send(args: string[]): Promise<unknown>;\n * const client: RedisClient = { sendCommand: (args) => send([...args]) };\n * @since 1.0.0\n */\nexport interface RedisClient {\n /**\n * Runs one command (`[\"EVALSHA\", sha, \"1\", key, ...]`) and resolves with its reply: a string (or bytes) for bulk and status\n * replies, a number for integers, `null` for nil, an array for multi-bulk. Rejects with the server's error (its message starts\n * with the error code, for example `NOSCRIPT`) or with the transport's error.\n */\n sendCommand(args: readonly string[]): Promise<unknown>;\n /**\n * Opens a dedicated connection for pub/sub, used when {@link RedisAdapterOptions.subscriber} is not given. The adapter owns it\n * and closes it with {@link RedisSubscriber.close}.\n */\n createSubscriber?(): Promise<RedisSubscriber> | RedisSubscriber;\n}\n\n/**\n * A connection in subscriber mode.\n *\n * @example\n * import type { RedisSubscriber } from \"@shadow-js/realtime/redis\";\n *\n * declare const subscriber: RedisSubscriber;\n * await subscriber.subscribe(\"rt:{room}:c\", (message) => message.length);\n * await subscriber.unsubscribe(\"rt:{room}:c\");\n * @since 1.0.0\n */\nexport interface RedisSubscriber {\n /**\n * Subscribes to a topic. Resolves once the server confirmed. Calling it again for a topic already subscribed replaces the\n * listener and does not deliver twice.\n */\n subscribe(topic: string, listener: (message: string) => void): Promise<void>;\n /** Stops listening to a topic. The listener is removed before the returned promise settles. */\n unsubscribe(topic: string): Promise<void>;\n /**\n * Registers a function to call whenever the connection was lost and came back (not for the first connection). The adapter\n * subscribes its topics again and recovers what was published in between from history. Returns a function that removes the\n * registration.\n */\n onReconnect(handler: () => void): () => void;\n /** Registers a function that receives the connection's errors, so none goes unhandled. Returns a function that removes it. */\n onError?(handler: (error: unknown) => void): () => void;\n /** Closes the connection. Called only for a subscriber the adapter created itself. */\n close?(): Promise<void>;\n}\n\n/**\n * The part of an ioredis connection that {@link fromIoredis} uses. The real `Redis` and `Cluster` classes fit.\n * @since 1.0.0\n */\nexport interface IoredisLike {\n call(command: string, ...args: string[]): Promise<unknown>;\n duplicate(): IoredisSubscriberLike;\n}\n\n/**\n * The part of a duplicated ioredis connection that {@link fromIoredis} uses in subscriber mode.\n * @since 1.0.0\n */\nexport interface IoredisSubscriberLike {\n subscribe(...topics: string[]): Promise<unknown>;\n unsubscribe(...topics: string[]): Promise<unknown>;\n on(event: string, listener: (...args: never[]) => void): unknown;\n off?(event: string, listener: (...args: never[]) => void): unknown;\n removeListener?(event: string, listener: (...args: never[]) => void): unknown;\n quit(): Promise<unknown>;\n disconnect?(): void;\n}\n\n/**\n * The part of a node-redis (v4 or v5) client that {@link fromNodeRedis} uses.\n * @since 1.0.0\n */\nexport interface NodeRedisLike {\n sendCommand(args: string[]): Promise<unknown>;\n duplicate(): NodeRedisSubscriberLike;\n}\n\n/**\n * The part of a duplicated node-redis client that {@link fromNodeRedis} uses in subscriber mode.\n * @since 1.0.0\n */\nexport interface NodeRedisSubscriberLike {\n connect(): Promise<unknown>;\n subscribe(topic: string, listener: (message: string, topic: string) => void): Promise<unknown>;\n unsubscribe(topic: string): Promise<unknown>;\n on(event: string, listener: (...args: never[]) => void): unknown;\n off?(event: string, listener: (...args: never[]) => void): unknown;\n removeListener?(event: string, listener: (...args: never[]) => void): unknown;\n close?(): Promise<unknown>;\n quit?(): Promise<unknown>;\n disconnect?(): Promise<unknown>;\n}\n\n/** A set of handlers with add-and-remove, shared by the two wrappers. */\nfunction handlers<A extends unknown[]>(): {\n add(handler: (...args: A) => void): () => void;\n fire(...args: A): void;\n} {\n const set = new Set<(...args: A) => void>();\n return {\n add(handler) {\n set.add(handler);\n return () => {\n set.delete(handler);\n };\n },\n fire(...args) {\n for (const handler of [...set]) {\n try {\n handler(...args);\n } catch {\n // A handler's failure is its own.\n }\n }\n },\n };\n}\n\n/**\n * Adapts an ioredis connection (`Redis` or `Cluster`). Commands go through `call`; the subscriber is a `duplicate()` of the\n * connection, which ioredis re-subscribes by itself after a reconnect (the adapter then recovers what was missed).\n *\n * @param client The ioredis connection.\n * @returns A {@link RedisClient} for `redisAdapter`.\n * @example\n * import { Redis } from \"ioredis\";\n * import { fromIoredis, redisAdapter } from \"@shadow-js/realtime/redis\";\n *\n * const adapter = redisAdapter({ client: fromIoredis(new Redis(process.env.REDIS_URL ?? \"\")) });\n * await adapter.close();\n * @since 1.0.0\n */\nexport function fromIoredis(client: IoredisLike): RedisClient {\n return {\n sendCommand: (args) => {\n const [command, ...rest] = args;\n return client.call(command ?? \"\", ...rest);\n },\n createSubscriber: (): RedisSubscriber => {\n const connection = client.duplicate();\n const topics = new Map<string, (message: string) => void>();\n const reconnects = handlers<[]>();\n const errors = handlers<[unknown]>();\n let ready = 0;\n connection.on(\"message\", ((topic: string, message: string) => {\n topics.get(topic)?.(message);\n }) as (...args: never[]) => void);\n connection.on(\"ready\", (() => {\n ready += 1;\n if (ready > 1) reconnects.fire();\n }) as (...args: never[]) => void);\n connection.on(\"error\", ((error: unknown) => errors.fire(error)) as (\n ...args: never[]\n ) => void);\n return {\n async subscribe(topic, listener) {\n topics.set(topic, listener);\n await connection.subscribe(topic);\n },\n async unsubscribe(topic) {\n topics.delete(topic);\n await connection.unsubscribe(topic);\n },\n onReconnect: (handler) => reconnects.add(handler),\n onError: (handler) => errors.add(handler),\n async close() {\n topics.clear();\n try {\n await connection.quit();\n } catch {\n connection.disconnect?.();\n }\n },\n };\n },\n };\n}\n\n/**\n * Adapts a node-redis (v4 or v5) client. Commands go through `sendCommand`; the subscriber is a `duplicate()` that is connected\n * here and that node-redis re-subscribes by itself after a reconnect (the adapter then recovers what was missed). Pass a\n * standalone client; node-redis cluster clients route by key and need their own wrapper.\n *\n * @param client The node-redis client (already connected).\n * @returns A {@link RedisClient} for `redisAdapter`.\n * @example\n * import { createClient } from \"redis\";\n * import { fromNodeRedis, redisAdapter } from \"@shadow-js/realtime/redis\";\n *\n * const client = createClient({ url: process.env.REDIS_URL ?? \"\" });\n * await client.connect();\n * const adapter = redisAdapter({ client: fromNodeRedis(client) });\n * await adapter.close();\n * @since 1.0.0\n */\nexport function fromNodeRedis(client: NodeRedisLike): RedisClient {\n return {\n sendCommand: (args) => client.sendCommand([...args]),\n createSubscriber: async (): Promise<RedisSubscriber> => {\n const connection = client.duplicate();\n const topics = new Map<string, (message: string) => void>();\n const dispatch = (message: string, topic: string): void => {\n topics.get(topic)?.(message);\n };\n const reconnects = handlers<[]>();\n const errors = handlers<[unknown]>();\n let ready = 0;\n connection.on(\"ready\", (() => {\n ready += 1;\n if (ready > 1) reconnects.fire();\n }) as (...args: never[]) => void);\n // node-redis throws an unhandled \"error\" event as an exception: it is always handled here.\n connection.on(\"error\", ((error: unknown) => errors.fire(error)) as (\n ...args: never[]\n ) => void);\n await connection.connect();\n const registered = new Set<string>();\n return {\n async subscribe(topic, listener) {\n topics.set(topic, listener);\n if (registered.has(topic)) return;\n registered.add(topic);\n try {\n await connection.subscribe(topic, dispatch);\n } catch (error) {\n registered.delete(topic);\n topics.delete(topic);\n throw error;\n }\n },\n async unsubscribe(topic) {\n topics.delete(topic);\n if (!registered.delete(topic)) return;\n await connection.unsubscribe(topic);\n },\n onReconnect: (handler) => reconnects.add(handler),\n onError: (handler) => errors.add(handler),\n async close() {\n topics.clear();\n registered.clear();\n if (connection.close !== undefined) await connection.close();\n else if (connection.quit !== undefined) await connection.quit();\n else await connection.disconnect?.();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
function e(e,t){return(e<<t|e>>>32-t)>>>0}function t(t){let n=new TextEncoder().encode(t),r=n.length+9+63>>6<<6,i=new Uint8Array(r);i.set(n),i[n.length]=128;let a=new DataView(i.buffer),o=n.length*8;a.setUint32(r-8,Math.floor(o/2**32)),a.setUint32(r-4,o>>>0);let s=1732584193,c=4023233417,l=2562383102,u=271733878,d=3285377520,f=/* @__PURE__ */ new Uint32Array(80);for(let t=0;t<r;t+=64){for(let e=0;e<16;e+=1)f[e]=a.getUint32(t+e*4);for(let t=16;t<80;t+=1)f[t]=e(f[t-3]^f[t-8]^f[t-14]^f[t-16],1);let n=s,r=c,i=l,o=u,p=d;for(let t=0;t<80;t+=1){let a,s;t<20?(a=r&i|~r&o,s=1518500249):t<40?(a=r^i^o,s=1859775393):t<60?(a=r&i|r&o|i&o,s=2400959708):(a=r^i^o,s=3395469782);let c=e(n,5)+(a>>>0)+p+s+f[t]>>>0;p=o,o=i,i=e(r,30),r=n,n=c}s=s+n>>>0,c=c+r>>>0,l=l+i>>>0,u=u+o>>>0,d=d+p>>>0}return[s,c,l,u,d].map(e=>e.toString(16).padStart(8,`0`)).join(``)}function n(e){return{source:e,sha:t(e)}}const r=`
|
|
2
|
+
local function now_ms()
|
|
3
|
+
local t = redis.call('TIME')
|
|
4
|
+
return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
5
|
+
end
|
|
6
|
+
local function num(n) return string.format('%.0f', n) end
|
|
7
|
+
`,i=n(`${r}
|
|
8
|
+
local meta, log, tix, idem = KEYS[1], KEYS[2], KEYS[3], KEYS[4]
|
|
9
|
+
if idem then
|
|
10
|
+
local prev = redis.call('GET', idem)
|
|
11
|
+
if prev then return {tonumber(prev), 1} end
|
|
12
|
+
end
|
|
13
|
+
local history = tonumber(ARGV[1])
|
|
14
|
+
local histTtl = tonumber(ARGV[2])
|
|
15
|
+
local idemTtl = tonumber(ARGV[3])
|
|
16
|
+
local chanTtl = tonumber(ARGV[4])
|
|
17
|
+
local at = now_ms()
|
|
18
|
+
local seq = redis.call('HINCRBY', meta, 's', 1)
|
|
19
|
+
if history > 0 then
|
|
20
|
+
local newest = redis.call('ZREVRANGE', tix, 0, 0, 'WITHSCORES')
|
|
21
|
+
if newest[2] and tonumber(newest[2]) > at then at = tonumber(newest[2]) end
|
|
22
|
+
end
|
|
23
|
+
local msg = '{"s":' .. num(seq) .. ',"a":' .. num(at) .. ',' .. ARGV[6]
|
|
24
|
+
if history > 0 then
|
|
25
|
+
local pad = string.format('%016.0f', seq)
|
|
26
|
+
redis.call('ZADD', log, seq, msg)
|
|
27
|
+
redis.call('ZADD', tix, at, pad)
|
|
28
|
+
local n = redis.call('ZCARD', log)
|
|
29
|
+
if n ~= redis.call('ZCARD', tix) then
|
|
30
|
+
redis.call('DEL', log, tix)
|
|
31
|
+
redis.call('ZADD', log, seq, msg)
|
|
32
|
+
redis.call('ZADD', tix, at, pad)
|
|
33
|
+
else
|
|
34
|
+
local drop = n - history
|
|
35
|
+
local old = redis.call('ZCOUNT', tix, '-inf', '(' .. num(at - histTtl))
|
|
36
|
+
if old > drop then drop = old end
|
|
37
|
+
if drop > 0 then
|
|
38
|
+
redis.call('ZREMRANGEBYRANK', log, 0, drop - 1)
|
|
39
|
+
redis.call('ZREMRANGEBYRANK', tix, 0, drop - 1)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
redis.call('PEXPIRE', log, histTtl)
|
|
43
|
+
redis.call('PEXPIRE', tix, histTtl)
|
|
44
|
+
else
|
|
45
|
+
redis.call('DEL', log, tix)
|
|
46
|
+
end
|
|
47
|
+
redis.call('HSET', meta, 't', ARGV[2])
|
|
48
|
+
local keep = chanTtl
|
|
49
|
+
if histTtl > keep then keep = histTtl end
|
|
50
|
+
if idem and idemTtl > keep then keep = idemTtl end
|
|
51
|
+
redis.call('PEXPIRE', meta, keep)
|
|
52
|
+
if idem then redis.call('SET', idem, num(seq), 'PX', idemTtl) end
|
|
53
|
+
redis.call('PUBLISH', ARGV[5], msg)
|
|
54
|
+
return {seq, 0}
|
|
55
|
+
`),a=n(`${r}
|
|
56
|
+
local meta, log, tix = KEYS[1], KEYS[2], KEYS[3]
|
|
57
|
+
local head = tonumber(redis.call('HGET', meta, 's') or '0')
|
|
58
|
+
local ttl = redis.call('HGET', meta, 't')
|
|
59
|
+
local low = tonumber(ARGV[1])
|
|
60
|
+
local limit = tonumber(ARGV[2])
|
|
61
|
+
local skip = 0
|
|
62
|
+
if ttl then
|
|
63
|
+
skip = redis.call('ZCOUNT', tix, '-inf', '(' .. num(now_ms() - tonumber(ttl)))
|
|
64
|
+
end
|
|
65
|
+
local oldest = 0
|
|
66
|
+
local first = redis.call('ZRANGE', tix, skip, skip)
|
|
67
|
+
if first[1] then oldest = tonumber(first[1]) end
|
|
68
|
+
if skip > 0 then
|
|
69
|
+
local expired = tonumber(redis.call('ZRANGE', tix, skip - 1, skip - 1)[1])
|
|
70
|
+
if expired and expired > low then low = expired end
|
|
71
|
+
end
|
|
72
|
+
local out = {head, oldest}
|
|
73
|
+
if limit > 0 and oldest > 0 then
|
|
74
|
+
local rows = redis.call('ZRANGEBYSCORE', log, '(' .. num(low), '+inf', 'LIMIT', 0, limit)
|
|
75
|
+
for i = 1, #rows do out[#out + 1] = rows[i] end
|
|
76
|
+
end
|
|
77
|
+
return out
|
|
78
|
+
`),o=n(`
|
|
79
|
+
local ms = tonumber(ARGV[1])
|
|
80
|
+
local left = redis.call('PTTL', KEYS[1])
|
|
81
|
+
if left >= 0 and left < ms then redis.call('PEXPIRE', KEYS[1], ms) end
|
|
82
|
+
return 1
|
|
83
|
+
`),s=`
|
|
84
|
+
local function extend(keep)
|
|
85
|
+
for i = 1, 2 do
|
|
86
|
+
local left = redis.call('PTTL', KEYS[i])
|
|
87
|
+
if left == -1 or (left >= 0 and left < keep) then redis.call('PEXPIRE', KEYS[i], keep) end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
`,c=n(`${r}${s}
|
|
91
|
+
local id, version, ttl = ARGV[1], tonumber(ARGV[2]), tonumber(ARGV[3])
|
|
92
|
+
local cur = redis.call('HGET', KEYS[1], id)
|
|
93
|
+
local op = nil
|
|
94
|
+
if cur then
|
|
95
|
+
local known = tonumber(string.match(cur, '^{"v":(%d+),'))
|
|
96
|
+
if known and known > version then return 0 end
|
|
97
|
+
if known == nil or known < version then op = 'set' end
|
|
98
|
+
else
|
|
99
|
+
op = 'join'
|
|
100
|
+
end
|
|
101
|
+
redis.call('HSET', KEYS[1], id, ARGV[5])
|
|
102
|
+
redis.call('ZADD', KEYS[2], num(now_ms() + ttl), id)
|
|
103
|
+
extend(ttl + tonumber(ARGV[7]))
|
|
104
|
+
if op then redis.call('PUBLISH', ARGV[4], '{"o":"' .. op .. '","m":' .. ARGV[6] .. '}') end
|
|
105
|
+
return 1
|
|
106
|
+
`),l=n(`
|
|
107
|
+
local cur = redis.call('HGET', KEYS[1], ARGV[1])
|
|
108
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
109
|
+
if not cur then return 0 end
|
|
110
|
+
redis.call('HDEL', KEYS[1], ARGV[1])
|
|
111
|
+
local member = string.match(cur, '^{"v":%d+,"m":(.*)}$')
|
|
112
|
+
if member then redis.call('PUBLISH', ARGV[2], '{"o":"leave","m":' .. member .. '}') end
|
|
113
|
+
return 1
|
|
114
|
+
`),u=n(`${r}${s}
|
|
115
|
+
local ttl = tonumber(ARGV[1])
|
|
116
|
+
local expires = num(now_ms() + ttl)
|
|
117
|
+
local missing = {}
|
|
118
|
+
local found = false
|
|
119
|
+
for i = 3, #ARGV do
|
|
120
|
+
if redis.call('HEXISTS', KEYS[1], ARGV[i]) == 1 then
|
|
121
|
+
redis.call('ZADD', KEYS[2], expires, ARGV[i])
|
|
122
|
+
found = true
|
|
123
|
+
else
|
|
124
|
+
missing[#missing + 1] = ARGV[i]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
if found then extend(ttl + tonumber(ARGV[2])) end
|
|
128
|
+
return missing
|
|
129
|
+
`),d=n(`${r}
|
|
130
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', num(now_ms()), 'LIMIT', 0, tonumber(ARGV[2]))
|
|
131
|
+
local out = {}
|
|
132
|
+
for i = 1, #ids do
|
|
133
|
+
local cur = redis.call('HGET', KEYS[1], ids[i])
|
|
134
|
+
redis.call('ZREM', KEYS[2], ids[i])
|
|
135
|
+
if cur then
|
|
136
|
+
redis.call('HDEL', KEYS[1], ids[i])
|
|
137
|
+
out[#out + 1] = cur
|
|
138
|
+
local member = string.match(cur, '^{"v":%d+,"m":(.*)}$')
|
|
139
|
+
if member then redis.call('PUBLISH', ARGV[1], '{"o":"leave","m":' .. member .. '}') end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
return out
|
|
143
|
+
`),f=n(`${r}
|
|
144
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[2], '(' .. num(now_ms()), '+inf')
|
|
145
|
+
local out = {}
|
|
146
|
+
local step = 400
|
|
147
|
+
for from = 1, #ids, step do
|
|
148
|
+
local to = from + step - 1
|
|
149
|
+
if to > #ids then to = #ids end
|
|
150
|
+
local values = redis.call('HMGET', KEYS[1], unpack(ids, from, to))
|
|
151
|
+
for i = 1, #values do
|
|
152
|
+
if values[i] then out[#out + 1] = values[i] end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
return out
|
|
156
|
+
`);export{a as HISTORY,o as KEEPALIVE,f as PRESENCE_LIST,d as PRESENCE_REAP,l as PRESENCE_REMOVE,c as PRESENCE_SET,u as PRESENCE_TOUCH,i as PUBLISH,t as sha1};
|
|
157
|
+
//# sourceMappingURL=scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.js","names":["rotateLeft","value","bits","sha1","input","text","total","padded","view","h0","h1","h2","h3","h4","w","offset","i","a","b","c","d","e","f","k","next","word","script","source","CLOCK","PUBLISH","HISTORY","KEEPALIVE","EXTEND","PRESENCE_SET","PRESENCE_REMOVE","PRESENCE_TOUCH","PRESENCE_REAP","PRESENCE_LIST"],"sources":["../../../src/redis/scripts.ts"],"sourcesContent":["/**\n * The Lua scripts of the Redis adapter, and the SHA-1 that names them on the server. Every script touches only keys of one\n * channel (one hash slot) and reads the clock from the server (`TIME`), so servers with skewed clocks agree.\n */\n\n/** A script with its source and the SHA-1 `EVALSHA` uses. */\nexport interface Script {\n readonly source: string;\n readonly sha: string;\n}\n\nfunction rotateLeft(value: number, bits: number): number {\n return ((value << bits) | (value >>> (32 - bits))) >>> 0;\n}\n\n/**\n * SHA-1 of an ASCII or UTF-8 text, as lowercase hex. Pure TypeScript, so the adapter needs neither `node:crypto` nor a\n * round trip to `SCRIPT LOAD`.\n */\nexport function sha1(text: string): string {\n const input = new TextEncoder().encode(text);\n const total = ((input.length + 9 + 63) >> 6) << 6;\n const padded = new Uint8Array(total);\n padded.set(input);\n padded[input.length] = 0x80;\n const view = new DataView(padded.buffer);\n const bits = input.length * 8;\n view.setUint32(total - 8, Math.floor(bits / 2 ** 32));\n view.setUint32(total - 4, bits >>> 0);\n let h0 = 0x67452301;\n let h1 = 0xefcdab89;\n let h2 = 0x98badcfe;\n let h3 = 0x10325476;\n let h4 = 0xc3d2e1f0;\n const w = new Uint32Array(80);\n for (let offset = 0; offset < total; offset += 64) {\n for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);\n for (let i = 16; i < 80; i += 1)\n w[i] = rotateLeft(\n (w[i - 3] as number) ^ (w[i - 8] as number) ^ (w[i - 14] as number) ^ (w[i - 16] as number),\n 1,\n );\n let a = h0;\n let b = h1;\n let c = h2;\n let d = h3;\n let e = h4;\n for (let i = 0; i < 80; i += 1) {\n let f: number;\n let k: number;\n if (i < 20) {\n f = (b & c) | (~b & d);\n k = 0x5a827999;\n } else if (i < 40) {\n f = b ^ c ^ d;\n k = 0x6ed9eba1;\n } else if (i < 60) {\n f = (b & c) | (b & d) | (c & d);\n k = 0x8f1bbcdc;\n } else {\n f = b ^ c ^ d;\n k = 0xca62c1d6;\n }\n const next = (rotateLeft(a, 5) + (f >>> 0) + e + k + (w[i] as number)) >>> 0;\n e = d;\n d = c;\n c = rotateLeft(b, 30);\n b = a;\n a = next;\n }\n h0 = (h0 + a) >>> 0;\n h1 = (h1 + b) >>> 0;\n h2 = (h2 + c) >>> 0;\n h3 = (h3 + d) >>> 0;\n h4 = (h4 + e) >>> 0;\n }\n return [h0, h1, h2, h3, h4].map((word) => word.toString(16).padStart(8, \"0\")).join(\"\");\n}\n\nfunction script(source: string): Script {\n return { source, sha: sha1(source) };\n}\n\n/** Lua helpers shared by the scripts that read the clock. */\nconst CLOCK = `\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function num(n) return string.format('%.0f', n) end\n`;\n\n/**\n * Publish. KEYS: meta hash, log zset (score = seq, member = the message), time zset (score = accepted-at, member = zero-padded\n * seq), optional idempotency key. ARGV: history, historyTtlMs, idempotencyTtlMs, channelTtlMs, topic, message tail.\n * Returns `{seq, duplicate}`.\n */\nexport const PUBLISH: Script = script(`${CLOCK}\nlocal meta, log, tix, idem = KEYS[1], KEYS[2], KEYS[3], KEYS[4]\nif idem then\n local prev = redis.call('GET', idem)\n if prev then return {tonumber(prev), 1} end\nend\nlocal history = tonumber(ARGV[1])\nlocal histTtl = tonumber(ARGV[2])\nlocal idemTtl = tonumber(ARGV[3])\nlocal chanTtl = tonumber(ARGV[4])\nlocal at = now_ms()\nlocal seq = redis.call('HINCRBY', meta, 's', 1)\nif history > 0 then\n local newest = redis.call('ZREVRANGE', tix, 0, 0, 'WITHSCORES')\n if newest[2] and tonumber(newest[2]) > at then at = tonumber(newest[2]) end\nend\nlocal msg = '{\"s\":' .. num(seq) .. ',\"a\":' .. num(at) .. ',' .. ARGV[6]\nif history > 0 then\n local pad = string.format('%016.0f', seq)\n redis.call('ZADD', log, seq, msg)\n redis.call('ZADD', tix, at, pad)\n local n = redis.call('ZCARD', log)\n if n ~= redis.call('ZCARD', tix) then\n redis.call('DEL', log, tix)\n redis.call('ZADD', log, seq, msg)\n redis.call('ZADD', tix, at, pad)\n else\n local drop = n - history\n local old = redis.call('ZCOUNT', tix, '-inf', '(' .. num(at - histTtl))\n if old > drop then drop = old end\n if drop > 0 then\n redis.call('ZREMRANGEBYRANK', log, 0, drop - 1)\n redis.call('ZREMRANGEBYRANK', tix, 0, drop - 1)\n end\n end\n redis.call('PEXPIRE', log, histTtl)\n redis.call('PEXPIRE', tix, histTtl)\nelse\n redis.call('DEL', log, tix)\nend\nredis.call('HSET', meta, 't', ARGV[2])\nlocal keep = chanTtl\nif histTtl > keep then keep = histTtl end\nif idem and idemTtl > keep then keep = idemTtl end\nredis.call('PEXPIRE', meta, keep)\nif idem then redis.call('SET', idem, num(seq), 'PX', idemTtl) end\nredis.call('PUBLISH', ARGV[5], msg)\nreturn {seq, 0}\n`);\n\n/**\n * History. KEYS: meta, log, time. ARGV: after, limit. Returns `{head, oldest, message...}`; a message older than the channel's\n * history lifetime is not returned even when no publish has trimmed it yet.\n */\nexport const HISTORY: Script = script(`${CLOCK}\nlocal meta, log, tix = KEYS[1], KEYS[2], KEYS[3]\nlocal head = tonumber(redis.call('HGET', meta, 's') or '0')\nlocal ttl = redis.call('HGET', meta, 't')\nlocal low = tonumber(ARGV[1])\nlocal limit = tonumber(ARGV[2])\nlocal skip = 0\nif ttl then\n skip = redis.call('ZCOUNT', tix, '-inf', '(' .. num(now_ms() - tonumber(ttl)))\nend\nlocal oldest = 0\nlocal first = redis.call('ZRANGE', tix, skip, skip)\nif first[1] then oldest = tonumber(first[1]) end\nif skip > 0 then\n local expired = tonumber(redis.call('ZRANGE', tix, skip - 1, skip - 1)[1])\n if expired and expired > low then low = expired end\nend\nlocal out = {head, oldest}\nif limit > 0 and oldest > 0 then\n local rows = redis.call('ZRANGEBYSCORE', log, '(' .. num(low), '+inf', 'LIMIT', 0, limit)\n for i = 1, #rows do out[#out + 1] = rows[i] end\nend\nreturn out\n`);\n\n/**\n * Keeps the numbering of a channel that has listeners alive. KEYS: meta. ARGV: channelTtlMs. Only ever extends the lifetime.\n */\nexport const KEEPALIVE: Script = script(`\nlocal ms = tonumber(ARGV[1])\nlocal left = redis.call('PTTL', KEYS[1])\nif left >= 0 and left < ms then redis.call('PEXPIRE', KEYS[1], ms) end\nreturn 1\n`);\n\n/** Lua: extends the lifetime of both presence keys to `keep` milliseconds, never shortens it. */\nconst EXTEND = `\nlocal function extend(keep)\n for i = 1, 2 do\n local left = redis.call('PTTL', KEYS[i])\n if left == -1 or (left >= 0 and left < keep) then redis.call('PEXPIRE', KEYS[i], keep) end\n end\nend\n`;\n\n/**\n * Presence set. KEYS: members hash (id -> stored), expiry zset (id -> expires). ARGV: id, version, ttlMs, topic, stored, member\n * JSON, graceMs. Returns 0 for an ignored older version, else 1. Publishes join (new) or set (higher version).\n */\nexport const PRESENCE_SET: Script = script(`${CLOCK}${EXTEND}\nlocal id, version, ttl = ARGV[1], tonumber(ARGV[2]), tonumber(ARGV[3])\nlocal cur = redis.call('HGET', KEYS[1], id)\nlocal op = nil\nif cur then\n local known = tonumber(string.match(cur, '^{\"v\":(%d+),'))\n if known and known > version then return 0 end\n if known == nil or known < version then op = 'set' end\nelse\n op = 'join'\nend\nredis.call('HSET', KEYS[1], id, ARGV[5])\nredis.call('ZADD', KEYS[2], num(now_ms() + ttl), id)\nextend(ttl + tonumber(ARGV[7]))\nif op then redis.call('PUBLISH', ARGV[4], '{\"o\":\"' .. op .. '\",\"m\":' .. ARGV[6] .. '}') end\nreturn 1\n`);\n\n/**\n * Presence remove. KEYS: hash, zset. ARGV: id, topic. Returns 1 only for the call that removed the member (which alone\n * publishes the leave), else 0.\n */\nexport const PRESENCE_REMOVE: Script = script(`\nlocal cur = redis.call('HGET', KEYS[1], ARGV[1])\nredis.call('ZREM', KEYS[2], ARGV[1])\nif not cur then return 0 end\nredis.call('HDEL', KEYS[1], ARGV[1])\nlocal member = string.match(cur, '^{\"v\":%d+,\"m\":(.*)}$')\nif member then redis.call('PUBLISH', ARGV[2], '{\"o\":\"leave\",\"m\":' .. member .. '}') end\nreturn 1\n`);\n\n/**\n * Presence touch. KEYS: hash, zset. ARGV: ttlMs, graceMs, id... Refreshes the expiry of the members that exist and returns the\n * ids that do not.\n */\nexport const PRESENCE_TOUCH: Script = script(`${CLOCK}${EXTEND}\nlocal ttl = tonumber(ARGV[1])\nlocal expires = num(now_ms() + ttl)\nlocal missing = {}\nlocal found = false\nfor i = 3, #ARGV do\n if redis.call('HEXISTS', KEYS[1], ARGV[i]) == 1 then\n redis.call('ZADD', KEYS[2], expires, ARGV[i])\n found = true\n else\n missing[#missing + 1] = ARGV[i]\n end\nend\nif found then extend(ttl + tonumber(ARGV[2])) end\nreturn missing\n`);\n\n/**\n * Presence reap. KEYS: hash, zset. ARGV: topic, limit. Removes up to `limit` expired members, publishing one leave for each,\n * and returns their stored values.\n */\nexport const PRESENCE_REAP: Script = script(`${CLOCK}\nlocal ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', num(now_ms()), 'LIMIT', 0, tonumber(ARGV[2]))\nlocal out = {}\nfor i = 1, #ids do\n local cur = redis.call('HGET', KEYS[1], ids[i])\n redis.call('ZREM', KEYS[2], ids[i])\n if cur then\n redis.call('HDEL', KEYS[1], ids[i])\n out[#out + 1] = cur\n local member = string.match(cur, '^{\"v\":%d+,\"m\":(.*)}$')\n if member then redis.call('PUBLISH', ARGV[1], '{\"o\":\"leave\",\"m\":' .. member .. '}') end\n end\nend\nreturn out\n`);\n\n/** Presence list. KEYS: hash, zset. Returns the stored values of the members that have not expired. */\nexport const PRESENCE_LIST: Script = script(`${CLOCK}\nlocal ids = redis.call('ZRANGEBYSCORE', KEYS[2], '(' .. num(now_ms()), '+inf')\nlocal out = {}\nlocal step = 400\nfor from = 1, #ids, step do\n local to = from + step - 1\n if to > #ids then to = #ids end\n local values = redis.call('HMGET', KEYS[1], unpack(ids, from, to))\n for i = 1, #values do\n if values[i] then out[#out + 1] = values[i] end\n end\nend\nreturn out\n`);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=[50,250,1e3],t=5e3;function n(e){return new Promise(t=>setTimeout(t,e))}function r(e,t){let n=e.get(t.member.id);return t.op===`leave`?n!==void 0&&e.delete(t.member.id):n!==void 0&&n.v>=t.member.v?!1:(e.set(t.member.id,t.member),!0)}function i(i){let a=/* @__PURE__ */ new Map,o=!1,s,c=!1,l=!1,u,d=[];function f(e,t,n){i.report(e,{where:n,channel:t.channel})}function p(e,t){if(!(e.dead||t.seq<=e.last)){e.last=t.seq;for(let n of[...e.listeners])try{n(t)}catch(t){f(t,e,`listener`)}}}function m(e,t){let n=i.parseMessage(t,e.channel);n===void 0||e.dead||(e.catching||e.last>0&&n.seq>e.last+1?(e.buffer.length>=1e3?e.overflow=!0:e.buffer.push(n),e.catching||v(e)):p(e,n))}function h(e,t){for(let n of[...e.listeners])try{n(t)}catch(t){f(t,e,`listener`)}}function g(e,n){let a=i.parsePresence(n,e.channel);a===void 0||e.dead||(e.pending===void 0?(!e.tracked||r(e.members,a))&&h(e,a):(e.pending.length<t?e.pending.push(a):e.tracked=!1,h(e,a)))}async function _(e){for(;;){let t=!0;for(;t;){let n=await i.history(e.channel,e.last,500);if(e.dead)return;for(let t of n)p(e,t);t=n.length>=500}if(!e.overflow)return;e.overflow=!1,e.buffer.length=0}}async function v(t){if(t.catching)t.rerun=!0;else{t.catching=!0;try{let r=0;for(;!t.dead&&!o;){t.rerun=!1;try{if(await _(t),!t.rerun)return}catch(i){f(i,t,`recover`);let a=e[r];if(r+=1,a===void 0)return;await n(a)}}}finally{t.catching=!1,t.rerun=!1;let e=t.buffer.splice(0);for(let n of e)p(t,n)}}}async function y(e){if(!e.tracked)return;let n=e.members;e.pending=[];let a;try{a=await i.members(e.channel)}catch(t){let i=e.pending;e.pending=void 0;for(let t of i)r(n,t)&&h(e,t);throw t}let o=e.pending;if(e.pending=void 0,e.dead)return;let s=new Map(a.map(e=>[e.id,e]));for(let e of o)r(s,e);let c=[];for(let[t,r]of n)s.has(t)||c.push({channel:e.channel,op:`leave`,member:r});for(let[t,r]of s){let i=n.get(t);i===void 0?c.push({channel:e.channel,op:`join`,member:r}):r.v>i.v&&c.push({channel:e.channel,op:`set`,member:r})}e.members=s,s.size>t&&(e.tracked=!1);for(let t of c)h(e,t)}async function b(t){for(let r=0;;r+=1)try{await(await i.subscriber()).subscribe(t.topic,t.handler),t.kind===`message`?t.last>0&&await v(t):await y(t);return}catch(i){f(i,t,`reconnect`);let a=e[r];if(a===void 0||t.dead||o||(await n(a),t.dead||o))return}}async function x(){if(c)l=!0;else{c=!0;try{do{l=!1;let e=[...a.values()].filter(e=>!e.dead);for(let t=0;t<e.length&&!o;t+=64)await Promise.all(e.slice(t,t+64).map(b))}while(l&&!o)}finally{c=!1}}}async function S(){let e=await i.subscriber();if(u!==e){u=e,d.push(e.onReconnect(()=>{x()}));let t=e.onError?.(e=>i.report(e,{where:`subscriber`}));t!==void 0&&d.push(t)}return e}async function C(){let e=[...a.values()].filter(e=>e.kind===`message`&&!e.dead);for(let t=0;t<e.length;t+=64)await Promise.all(e.slice(t,t+64).map(async e=>{try{await i.keepAlive(e.channel)}catch(t){f(t,e,`keepAlive`)}}))}function w(){let e=[...a.values()].some(e=>e.kind===`message`);e&&s===void 0&&!o?(s=setInterval(()=>void C(),i.keepAliveMs),s.unref?.()):!e&&s!==void 0&&(clearInterval(s),s=void 0)}async function T(e){a.set(e.topic,e),w(),e.ready=(async()=>{await(await S()).subscribe(e.topic,e.handler),e.kind===`presence`&&await E(e)})(),e.ready.catch(()=>{e.dead=!0,a.get(e.topic)===e&&a.delete(e.topic),w(),u?.unsubscribe(e.topic).catch(()=>void 0)}),await e.ready}async function E(e){e.pending=[];let n;try{n=await i.members(e.channel)}catch(t){throw e.pending=void 0,t}let a=e.pending;e.pending=void 0;let o=new Map(n.map(e=>[e.id,e]));for(let e of a)r(o,e);e.members=o,o.size>t&&(e.tracked=!1)}function D(e,t){let n=!1;return async()=>{if(!n&&(n=!0,t(),!(e.listeners.size>0||e.dead)&&(e.dead=!0,a.get(e.topic)===e&&a.delete(e.topic),w(),!(o||u===void 0))))try{await u.unsubscribe(e.topic)}catch(t){f(t,e,`unsubscribe`)}}}async function O(e,t,n){if(o)throw i.fail(/* @__PURE__ */ Error(`the adapter is closed.`));let r=e??t(),a=n(r);try{e===void 0?await T(r):await r.ready}catch(e){throw a(),i.fail(e)}if(o||r.dead)throw a(),i.fail(/* @__PURE__ */ Error(`the adapter is closed.`));return a}return{messages(e,t){let n=i.messageTopic(e),r=a.get(n);return O(r?.kind===`message`?r:void 0,()=>{let t={kind:`message`,channel:e,topic:n,listeners:/* @__PURE__ */ new Set,last:0,catching:!1,rerun:!1,buffer:[],overflow:!1,dead:!1,ready:Promise.resolve(),handler:e=>m(t,e)};return t},e=>(e.listeners.add(t),D(e,()=>e.listeners.delete(t))))},presence(e,t){let n=i.presenceTopic(e),r=a.get(n);return O(r?.kind===`presence`?r:void 0,()=>{let t={kind:`presence`,channel:e,topic:n,listeners:/* @__PURE__ */ new Set,members:/* @__PURE__ */ new Map,tracked:!0,pending:void 0,dead:!1,ready:Promise.resolve(),handler:e=>g(t,e)};return t},e=>(e.listeners.add(t),D(e,()=>e.listeners.delete(t))))},async close(){o=!0;for(let e of d.splice(0))e();s!==void 0&&clearInterval(s),s=void 0;let e=[...a.values()];a.clear();for(let t of e)t.dead=!0;if(e.length===0)return;let t=u;t!==void 0&&await Promise.all(e.map(e=>t.unsubscribe(e.topic).catch(t=>{f(t,e,`unsubscribe`)})))},get size(){return a.size}}}export{i as createTopics};
|
|
2
|
+
//# sourceMappingURL=topics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topics.js","names":["RETRY_DELAYS_MS","MAX_TRACKED_MEMBERS","sleep","resolve","ms","apply","known","members","event","createTopics","topics","closed","timer","reconnecting","again","connected","unhook","reportTo","deps","error","topic","emit","message","listener","onMessage","text","heal","dispatchPresence","onPresence","readToPresent","more","page","attempt","delay","held","resync","before","list","next","member","events","id","recover","reconnect","live","from","hook","subscriber","stopErrors","keepAliveRound","channels","syncTimer","needed","open","seed","release","done","remove","attach","found","create","stop","add","key","channel","existing"],"sources":["../../../src/redis/topics.ts"],"sourcesContent":["import type { Unsubscribe } from \"../adapter\";\nimport type { Member, PresenceEvent, StoredMessage } from \"../protocol\";\nimport type { RedisSubscriber } from \"./client\";\n\n/** Where a failure the adapter handled came from. */\nexport interface RedisErrorContext {\n readonly where: string;\n readonly channel?: string;\n}\n\n/** What the subscription manager needs from the adapter that owns it. */\nexport interface TopicsDeps {\n /** The shared subscriber connection (opened on first use). */\n subscriber(): Promise<RedisSubscriber>;\n /** The retained messages after `after`, at most `limit`, oldest first. */\n history(channel: string, after: number, limit: number): Promise<readonly StoredMessage[]>;\n /** The live presence members of a channel. */\n members(channel: string): Promise<readonly Member[]>;\n /** Extends the lifetime of a channel's numbering. */\n keepAlive(channel: string): Promise<void>;\n /** Parses a pub/sub payload; undefined (after reporting) when it is malformed. */\n parseMessage(text: string, channel: string): StoredMessage | undefined;\n parsePresence(text: string, channel: string): PresenceEvent | undefined;\n messageTopic(channel: string): string;\n presenceTopic(channel: string): string;\n report(error: unknown, context: RedisErrorContext): void;\n /** Turns a failure into the error the adapter's callers see. */\n fail(error: unknown): Error;\n /** Milliseconds between keep-alive rounds. */\n keepAliveMs: number;\n}\n\n/** What the manager returns. */\nexport interface Topics {\n messages(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe>;\n presence(channel: string, listener: (event: PresenceEvent) => void): Promise<Unsubscribe>;\n /** Unsubscribes everything and stops the timers. */\n close(): Promise<void>;\n /** The number of topics currently subscribed (for tests and diagnostics). */\n readonly size: number;\n}\n\n/** Live messages held while a recovery reads history; a larger burst falls back to reading history again. */\nconst CATCH_UP_BUFFER = 1000;\n/** Messages read per history page during recovery. */\nconst CATCH_UP_PAGE = 500;\n/** Delays between attempts to recover after a reconnect. */\nconst RETRY_DELAYS_MS: readonly number[] = [50, 250, 1000];\n/** Topics subscribed again at once after a reconnect. */\nconst RESUBSCRIBE_BATCH = 64;\n/** Members tracked per presence channel; a larger channel is delivered unfiltered and not reconciled after a reconnect. */\nconst MAX_TRACKED_MEMBERS = 5000;\n\ntype MessageListener = (message: StoredMessage) => void;\ntype PresenceListener = (event: PresenceEvent) => void;\n\ninterface TopicBase {\n readonly channel: string;\n readonly topic: string;\n readonly handler: (text: string) => void;\n ready: Promise<void>;\n dead: boolean;\n}\n\ninterface MessageTopic extends TopicBase {\n readonly kind: \"message\";\n readonly listeners: Set<MessageListener>;\n /** The highest seq handed to the listeners: nothing at or below it is delivered again. */\n last: number;\n /** True while history is being read to fill a gap: live messages wait in `buffer`. */\n catching: boolean;\n /** Set when a reconnect happened while a recovery was running: it reads once more. */\n rerun: boolean;\n buffer: StoredMessage[];\n overflow: boolean;\n}\n\ninterface PresenceTopic extends TopicBase {\n readonly kind: \"presence\";\n readonly listeners: Set<PresenceListener>;\n /** The members as the listeners know them, by id. */\n members: Map<string, Member>;\n /** False when the channel is too large to track: events are then delivered unfiltered and not reconciled. */\n tracked: boolean;\n /** Events received while the state is being read; applied on top of it when it arrives. */\n pending: PresenceEvent[] | undefined;\n}\n\ntype Topic = MessageTopic | PresenceTopic;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Applies an event to a member map; true when it changed the map (a stale or repeated event does not). */\nfunction apply(members: Map<string, Member>, event: PresenceEvent): boolean {\n const known = members.get(event.member.id);\n if (event.op === \"leave\") return known !== undefined && members.delete(event.member.id);\n if (known !== undefined && known.v >= event.member.v) return false;\n members.set(event.member.id, event.member);\n return true;\n}\n\n/**\n * The shared subscriber connection of one adapter: topics are refcounted (one SUBSCRIBE per topic however many listeners), a\n * listener sees strictly increasing seqs and never a repeat, and after a reconnect the topics are subscribed again and what the\n * connection missed is read back from history (messages) or from the member list (presence).\n */\nexport function createTopics(deps: TopicsDeps): Topics {\n const topics = new Map<string, Topic>();\n let closed = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n let reconnecting = false;\n let again = false;\n let connected: RedisSubscriber | undefined;\n /** Removes the hooks this manager put on the subscriber (a subscriber the caller passed outlives the adapter). */\n const unhook: (() => void)[] = [];\n\n function reportTo(error: unknown, topic: Topic, where: string): void {\n deps.report(error, { where, channel: topic.channel });\n }\n\n // --- delivery ------------------------------------------------------------------------------------------------------\n\n function emit(topic: MessageTopic, message: StoredMessage): void {\n if (topic.dead || message.seq <= topic.last) return;\n topic.last = message.seq;\n for (const listener of [...topic.listeners]) {\n try {\n listener(message);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onMessage(topic: MessageTopic, text: string): void {\n const message = deps.parseMessage(text, topic.channel);\n if (message === undefined || topic.dead) return;\n if (topic.catching || (topic.last > 0 && message.seq > topic.last + 1)) {\n // Either history is being read, or this message jumped ahead of the cursor (a publish this connection missed): hold it\n // back and fill the gap from history first, so the listeners see every retained message in order.\n if (topic.buffer.length >= CATCH_UP_BUFFER) topic.overflow = true;\n else topic.buffer.push(message);\n if (!topic.catching) void heal(topic);\n return;\n }\n emit(topic, message);\n }\n\n function dispatchPresence(topic: PresenceTopic, event: PresenceEvent): void {\n for (const listener of [...topic.listeners]) {\n try {\n listener(event);\n } catch (error) {\n reportTo(error, topic, \"listener\");\n }\n }\n }\n\n function onPresence(topic: PresenceTopic, text: string): void {\n const event = deps.parsePresence(text, topic.channel);\n if (event === undefined || topic.dead) return;\n if (topic.pending !== undefined) {\n // The state is being read: deliver raw now, and fold the event into the state when it arrives.\n if (topic.pending.length < MAX_TRACKED_MEMBERS) topic.pending.push(event);\n else topic.tracked = false;\n dispatchPresence(topic, event);\n return;\n }\n if (topic.tracked && !apply(topic.members, event)) return;\n dispatchPresence(topic, event);\n }\n\n // --- recovery after a reconnect ------------------------------------------------------------------------------------\n\n /** Reads history from the cursor until it reaches the present, then lets the held live messages through. */\n async function readToPresent(topic: MessageTopic): Promise<void> {\n for (;;) {\n let more = true;\n while (more) {\n const page = await deps.history(topic.channel, topic.last, CATCH_UP_PAGE);\n if (topic.dead) return;\n for (const message of page) emit(topic, message);\n more = page.length >= CATCH_UP_PAGE;\n }\n if (!topic.overflow) return;\n // More live messages arrived than the buffer holds: read history again from the cursor.\n topic.overflow = false;\n topic.buffer.length = 0;\n }\n }\n\n /**\n * Fills a gap of the cursor from history. One at a time per topic; a request that arrives while one runs makes it read once\n * more. Gives up after a few failed attempts (the hub's own recovery is the next net) and lets the held messages through.\n */\n async function heal(topic: MessageTopic): Promise<void> {\n if (topic.catching) {\n topic.rerun = true;\n return;\n }\n topic.catching = true;\n try {\n let attempt = 0;\n while (!topic.dead && !closed) {\n topic.rerun = false;\n try {\n await readToPresent(topic);\n if (!topic.rerun) return;\n } catch (error) {\n reportTo(error, topic, \"recover\");\n const delay = RETRY_DELAYS_MS[attempt];\n attempt += 1;\n if (delay === undefined) return;\n await sleep(delay);\n }\n }\n } finally {\n topic.catching = false;\n topic.rerun = false;\n const held = topic.buffer.splice(0);\n for (const message of held) emit(topic, message);\n }\n }\n\n async function resync(topic: PresenceTopic): Promise<void> {\n if (!topic.tracked) return;\n const before = topic.members;\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n const held = topic.pending;\n topic.pending = undefined;\n for (const event of held) if (apply(before, event)) dispatchPresence(topic, event);\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n if (topic.dead) return;\n const next = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(next, event);\n const events: PresenceEvent[] = [];\n for (const [id, member] of before)\n if (!next.has(id)) events.push({ channel: topic.channel, op: \"leave\", member });\n for (const [id, member] of next) {\n const known = before.get(id);\n if (known === undefined) events.push({ channel: topic.channel, op: \"join\", member });\n else if (member.v > known.v) events.push({ channel: topic.channel, op: \"set\", member });\n }\n topic.members = next;\n if (next.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n for (const event of events) dispatchPresence(topic, event);\n }\n\n async function recover(topic: Topic): Promise<void> {\n for (let attempt = 0; ; attempt += 1) {\n try {\n const subscriber = await deps.subscriber();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"message\") {\n if (topic.last > 0) await heal(topic);\n } else {\n await resync(topic);\n }\n return;\n } catch (error) {\n reportTo(error, topic, \"reconnect\");\n const delay = RETRY_DELAYS_MS[attempt];\n if (delay === undefined || topic.dead || closed) return;\n await sleep(delay);\n if (topic.dead || closed) return;\n }\n }\n }\n\n async function reconnect(): Promise<void> {\n if (reconnecting) {\n again = true;\n return;\n }\n reconnecting = true;\n try {\n do {\n again = false;\n const live = [...topics.values()].filter((topic) => !topic.dead);\n for (let from = 0; from < live.length && !closed; from += RESUBSCRIBE_BATCH)\n await Promise.all(live.slice(from, from + RESUBSCRIBE_BATCH).map(recover));\n } while (again && !closed);\n } finally {\n reconnecting = false;\n }\n }\n\n async function hook(): Promise<RedisSubscriber> {\n const subscriber = await deps.subscriber();\n if (connected !== subscriber) {\n connected = subscriber;\n unhook.push(\n subscriber.onReconnect(() => {\n void reconnect();\n }),\n );\n const stopErrors = subscriber.onError?.((error) =>\n deps.report(error, { where: \"subscriber\" }),\n );\n if (stopErrors !== undefined) unhook.push(stopErrors);\n }\n return subscriber;\n }\n\n // --- keeping numbering alive ---------------------------------------------------------------------------------------\n\n async function keepAliveRound(): Promise<void> {\n const channels = [...topics.values()].filter(\n (topic): topic is MessageTopic => topic.kind === \"message\" && !topic.dead,\n );\n for (let from = 0; from < channels.length; from += RESUBSCRIBE_BATCH)\n await Promise.all(\n channels.slice(from, from + RESUBSCRIBE_BATCH).map(async (topic) => {\n try {\n await deps.keepAlive(topic.channel);\n } catch (error) {\n reportTo(error, topic, \"keepAlive\");\n }\n }),\n );\n }\n\n function syncTimer(): void {\n const needed = [...topics.values()].some((topic) => topic.kind === \"message\");\n if (needed && timer === undefined && !closed) {\n timer = setInterval(() => void keepAliveRound(), deps.keepAliveMs);\n (timer as { unref?: () => void }).unref?.();\n } else if (!needed && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n }\n\n // --- subscribing and releasing -------------------------------------------------------------------------------------\n\n async function open<T extends Topic>(topic: T): Promise<void> {\n topics.set(topic.topic, topic);\n syncTimer();\n topic.ready = (async () => {\n const subscriber = await hook();\n await subscriber.subscribe(topic.topic, topic.handler);\n if (topic.kind === \"presence\") await seed(topic);\n })();\n topic.ready.catch(() => {\n // The first caller's await reports it; here the half-open topic is dropped.\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n connected?.unsubscribe(topic.topic).catch(() => undefined);\n });\n await topic.ready;\n }\n\n async function seed(topic: PresenceTopic): Promise<void> {\n topic.pending = [];\n let list: readonly Member[];\n try {\n list = await deps.members(topic.channel);\n } catch (error) {\n topic.pending = undefined;\n throw error;\n }\n const held = topic.pending;\n topic.pending = undefined;\n const members = new Map<string, Member>(list.map((member) => [member.id, member]));\n for (const event of held) apply(members, event);\n topic.members = members;\n if (members.size > MAX_TRACKED_MEMBERS) topic.tracked = false;\n }\n\n /**\n * Returns the function that removes one listener. The last listener out drops the topic and sends UNSUBSCRIBE at once (not\n * after an await), so a subscribe of the same topic issued next is sent after it and wins.\n */\n function release(topic: Topic, remove: () => void): Unsubscribe {\n let done = false;\n return async () => {\n if (done) return;\n done = true;\n remove();\n if (topic.listeners.size > 0 || topic.dead) return;\n topic.dead = true;\n if (topics.get(topic.topic) === topic) topics.delete(topic.topic);\n syncTimer();\n if (closed || connected === undefined) return;\n try {\n await connected.unsubscribe(topic.topic);\n } catch (error) {\n reportTo(error, topic, \"unsubscribe\");\n }\n };\n }\n\n async function attach<T extends Topic>(\n topic: T | undefined,\n create: () => T,\n add: (topic: T) => Unsubscribe,\n ): Promise<Unsubscribe> {\n if (closed) throw deps.fail(new Error(\"the adapter is closed.\"));\n const found = topic ?? create();\n const stop = add(found);\n try {\n if (topic === undefined) await open(found);\n else await found.ready;\n } catch (error) {\n void stop();\n throw deps.fail(error);\n }\n if (closed || found.dead) {\n void stop();\n throw deps.fail(new Error(\"the adapter is closed.\"));\n }\n return stop;\n }\n\n return {\n messages(channel, listener) {\n const key = deps.messageTopic(channel);\n const existing = topics.get(key);\n return attach<MessageTopic>(\n existing?.kind === \"message\" ? existing : undefined,\n () => {\n const topic: MessageTopic = {\n kind: \"message\",\n channel,\n topic: key,\n listeners: new Set(),\n last: 0,\n catching: false,\n rerun: false,\n buffer: [],\n overflow: false,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onMessage(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n presence(channel, listener) {\n const key = deps.presenceTopic(channel);\n const existing = topics.get(key);\n return attach<PresenceTopic>(\n existing?.kind === \"presence\" ? existing : undefined,\n () => {\n const topic: PresenceTopic = {\n kind: \"presence\",\n channel,\n topic: key,\n listeners: new Set(),\n members: new Map(),\n tracked: true,\n pending: undefined,\n dead: false,\n ready: Promise.resolve(),\n handler: (text) => onPresence(topic, text),\n };\n return topic;\n },\n (topic) => {\n topic.listeners.add(listener);\n return release(topic, () => topic.listeners.delete(listener));\n },\n );\n },\n async close() {\n closed = true;\n for (const stop of unhook.splice(0)) stop();\n if (timer !== undefined) clearInterval(timer);\n timer = undefined;\n const open = [...topics.values()];\n topics.clear();\n for (const topic of open) topic.dead = true;\n if (open.length === 0) return;\n const subscriber = connected;\n if (subscriber === undefined) return;\n await Promise.all(\n open.map((topic) =>\n subscriber.unsubscribe(topic.topic).catch((error: unknown) => {\n reportTo(error, topic, \"unsubscribe\");\n }),\n ),\n );\n },\n get size() {\n return topics.size;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{RealtimeError as e}from"./errors.js";import{HISTORY as t,KEEPALIVE as n,PRESENCE_LIST as r,PRESENCE_REAP as i,PRESENCE_REMOVE as a,PRESENCE_SET as o,PRESENCE_TOUCH as s,PUBLISH as c}from"./redis/scripts.js";import{createTopics as l}from"./redis/topics.js";import{fromIoredis as u,fromNodeRedis as d}from"./redis/client.js";function f(e,t){if(!Number.isFinite(t)||t<1)throw RangeError(`${e} must be a positive number of milliseconds.`);return Math.ceil(t)}function p(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function m(e){if(typeof e==`string`)return e;if(e instanceof Uint8Array)return new TextDecoder().decode(e);if(typeof e==`number`)return String(e);throw TypeError(`Redis answered with an unexpected reply.`)}function h(e){let t=typeof e==`number`?e:Number(m(e));if(!Number.isSafeInteger(t))throw TypeError(`Redis answered with an unexpected number.`);return t}function g(e){if(!Array.isArray(e))throw TypeError(`Redis answered with an unexpected reply.`);return e}function _(e){return(e instanceof Error?e.message:String(e)).includes(`NOSCRIPT`)}async function v(e,t){for(let n=0;n<e.length;n+=64)await Promise.all(e.slice(n,n+64).map(t))}function y(u){let{client:d}=u;if(typeof d?.sendCommand!=`function`)throw TypeError(`redisAdapter needs a client with sendCommand (see fromIoredis, fromNodeRedis).`);if(u.subscriber===void 0&&d.createSubscriber===void 0)throw TypeError(`redisAdapter needs a subscriber: pass one, or a client that can create one (fromIoredis, fromNodeRedis).`);let y=u.prefix??`rt`;if(!/^[^\s{}]{1,128}$/.test(y))throw RangeError(`prefix must be 1 to 128 characters without braces or whitespace.`);let b=f(`channelTtlMs`,u.channelTtlMs??864e5),x=f(`presenceGraceMs`,u.presenceGraceMs??6e4),S=u.onError,C=!1,w;function T(e,t){try{S?.(e,t)}catch{}}function E(t){return t instanceof e?t:new e(`unavailable`,C?`the Redis adapter is closed.`:`the Redis adapter could not complete the command.`,{cause:t})}function D(){if(C)throw new e(`unavailable`,`the Redis adapter is closed.`)}function O(t){if(t.length===0||t.includes(`{`)||t.includes(`}`))throw new e(`bad_channel`,`a channel name is empty or contains a brace.`);return`${y}:{${t}}`}let k=e=>{let t=O(e);return{meta:`${t}:m`,log:`${t}:l`,time:`${t}:t`,idem:e=>`${t}:k:${e}`,members:`${t}:p`,expiry:`${t}:x`,messages:`${t}:c`,events:`${t}:e`}};async function A(e,t,n){D();let r=[String(t.length),...t,...n];try{try{return await d.sendCommand([`EVALSHA`,e.sha,...r])}catch(t){if(!_(t))throw t;return await d.sendCommand([`EVAL`,e.source,...r])}}catch(e){throw E(e)}}function j(e,t){try{let t=JSON.parse(e);if(p(t)){let e=t.s,n=t.a,r=t.e,i=t.d,a=t.p,o=t.u;if(typeof e==`number`&&Number.isSafeInteger(e)&&e>=1&&typeof n==`number`&&typeof r==`string`&&typeof i==`string`&&(a===void 0||typeof a==`string`)&&(o===void 0||typeof o==`string`))return{seq:e,at:n,event:r,data:i,...a===void 0?{}:{publisher:a},...o===void 0?{}:{user:o}}}}catch(e){T(e,{where:`parse`,channel:t});return}T(/* @__PURE__ */ TypeError(`a stored message has an unexpected shape.`),{where:`parse`,channel:t})}function M(e){if(!p(e))return;let t=e.id,n=e.u,r=e.v;if(typeof t==`string`&&typeof r==`number`&&Number.isSafeInteger(r)&&`s`in e&&(n===void 0||typeof n==`string`))return{id:t,...n===void 0?{}:{u:n},s:e.s,v:r}}function N(e,t){try{let t=JSON.parse(m(e)),n=p(t)?M(t.m):void 0;if(n!==void 0)return n}catch(e){T(e,{where:`parse`,channel:t});return}T(/* @__PURE__ */ TypeError(`a stored presence member has an unexpected shape.`),{where:`parse`,channel:t})}function P(e,t){try{let n=JSON.parse(e);if(p(n)){let e=n.o,r=M(n.m);if((e===`join`||e===`set`||e===`leave`)&&r!==void 0)return{channel:t,op:e,member:r}}}catch(e){T(e,{where:`parse`,channel:t});return}T(/* @__PURE__ */ TypeError(`a presence event has an unexpected shape.`),{where:`parse`,channel:t})}async function F(e,n,r){let i=k(e),a=Number.isFinite(n)?Math.min(Math.max(0,Math.floor(n)),2**53-1):0,o=Number.isFinite(r)?Math.min(Math.max(0,Math.floor(r)),1e4):0,s=g(await A(t,[i.meta,i.log,i.time],[String(a),String(o)])),c=[];try{for(let t of s.slice(2)){let n=j(m(t),e);n!==void 0&&c.push(n)}return{messages:c,head:h(s[0]),oldest:h(s[1])}}catch(e){throw E(e)}}async function I(e){let t=k(e),n=g(await A(r,[t.members,t.expiry],[])),i=[];for(let t of n){let n=N(t,e);n!==void 0&&i.push(n)}return i}function L(){if(D(),u.subscriber!==void 0)return Promise.resolve(u.subscriber);if(w===void 0){let e=d.createSubscriber;if(e===void 0)return Promise.reject(E(/* @__PURE__ */ TypeError(`no subscriber.`)));w=(async()=>e.call(d))(),w.catch(()=>{w=void 0})}return w.catch(e=>{throw E(e)})}let R=l({subscriber:L,history:async(e,t,n)=>(await F(e,t,n)).messages,members:I,keepAlive:async e=>{await A(n,[k(e).meta],[String(b)])},parseMessage:j,parsePresence:P,messageTopic:e=>k(e).messages,presenceTopic:e=>k(e).events,report:T,fail:E,keepAliveMs:Math.max(100,Math.floor(b/3))});return{async publish(t,n,r){let i=k(t);if(typeof n.data!=`string`)throw TypeError(`a stored entry's data is the JSON text of the value.`);let a=Math.max(0,Math.floor(r.history)),o=f(`historyTtlMs`,r.historyTtlMs),s=f(`idempotencyTtlMs`,r.idempotencyTtlMs),l=r.key;if(l!==void 0&&(l.length===0||l.length>1024))throw new e(`bad_frame`,`an idempotency key is empty or too long.`);let u=JSON.stringify({e:n.event,d:n.data,...n.publisher===void 0?{}:{p:n.publisher},...n.user===void 0?{}:{u:n.user}}).slice(1),d=g(await A(c,[i.meta,i.log,i.time,...l===void 0?[]:[i.idem(l)]],[String(a),String(o),String(s),String(b),i.messages,u]));try{return{seq:h(d[0]),duplicate:h(d[1])===1}}catch(e){throw E(e)}},subscribe(e,t){try{D(),O(e)}catch(e){return Promise.reject(E(e))}return R.messages(e,t)},history:F,async presenceSet(e,t,n){let r=k(e);if(!Number.isSafeInteger(t.v)||t.v<0)throw RangeError(`a member version must be a non-negative integer.`);let i=JSON.stringify({id:t.id,...t.u===void 0?{}:{u:t.u},s:t.s===void 0?null:t.s,v:t.v});await A(o,[r.members,r.expiry],[t.id,String(t.v),String(f(`ttlMs`,n)),r.events,`{"v":${t.v},"m":${i}}`,i,String(x)])},async presenceRemove(e,t){let n=k(e);return h(await A(a,[n.members,n.expiry],[t,n.events]))===1},async presenceList(e){return I(e)},async presenceTouch(e,t){let n=String(f(`ttlMs`,t)),r=/* @__PURE__ */ new Map;for(let t of e){let e=r.get(t.channel);e===void 0?r.set(t.channel,[t.id]):e.push(t.id)}let i=[];for(let[e,t]of r)for(let n=0;n<t.length;n+=500)i.push({channel:e,ids:t.slice(n,n+500)});let a=[];return await v(i,async({channel:e,ids:t})=>{let r=k(e),i=g(await A(s,[r.members,r.expiry],[n,String(x),...t]));for(let t of i)a.push({channel:e,id:m(t)})}),a},async presenceReap(e){let t=k(e),n=[];for(;;){let r=g(await A(i,[t.members,t.expiry],[t.events,`500`]));for(let t of r){let r=N(t,e);r!==void 0&&n.push(r)}if(r.length<500)return n}},presenceSubscribe(e,t){try{D(),O(e)}catch(e){return Promise.reject(E(e))}return R.presence(e,t)},async close(){if(C)return;C=!0,await R.close();let e=w;if(w=void 0,e!==void 0)try{await(await e).close?.()}catch(e){T(e,{where:`close`})}}}}export{u as fromIoredis,d as fromNodeRedis,y as redisAdapter};
|
|
2
|
+
//# sourceMappingURL=redis.js.map
|