@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":"memory.js","names":[],"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,211 @@
|
|
|
1
|
+
import { ErrorCode } from "./errors.js";
|
|
2
|
+
//#region src/protocol.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).
|
|
5
|
+
* @example
|
|
6
|
+
* import { PROTOCOL_VERSION } from "@shadow-js/realtime";
|
|
7
|
+
*
|
|
8
|
+
* const hello = JSON.stringify({ t: "hello", v: PROTOCOL_VERSION });
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
export declare const PROTOCOL_VERSION: 1;
|
|
12
|
+
/**
|
|
13
|
+
* Any JSON value.
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
export type Json = null | boolean | number | string | readonly Json[] | {
|
|
17
|
+
readonly [key: string]: Json;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One member of a channel's presence set: the connection that joined, its user, its state, and the version of that state.
|
|
21
|
+
* @since 1.0.0
|
|
22
|
+
*/
|
|
23
|
+
export interface Member<State = Json> {
|
|
24
|
+
/** The connection id. */
|
|
25
|
+
readonly id: string;
|
|
26
|
+
/** The user of the connection's identity, when it has one. */
|
|
27
|
+
readonly u?: string;
|
|
28
|
+
/** The member's state. */
|
|
29
|
+
readonly s: State;
|
|
30
|
+
/** A counter that rises with every state change of this member, from 1. */
|
|
31
|
+
readonly v: number;
|
|
32
|
+
}
|
|
33
|
+
/** A message as stored by an adapter: the data is its JSON text. @since 1.0.0 */
|
|
34
|
+
export interface StoredMessage {
|
|
35
|
+
readonly seq: number;
|
|
36
|
+
readonly event: string;
|
|
37
|
+
/** The JSON text of the data. */
|
|
38
|
+
readonly data: string;
|
|
39
|
+
/** The publishing connection id, when a connection published it. */
|
|
40
|
+
readonly publisher?: string;
|
|
41
|
+
/** The publishing user, when it has one. */
|
|
42
|
+
readonly user?: string;
|
|
43
|
+
/** When the adapter accepted it, in epoch milliseconds. */
|
|
44
|
+
readonly at: number;
|
|
45
|
+
}
|
|
46
|
+
/** What a publisher supplies; the adapter adds `seq` and `at`. @since 1.0.0 */
|
|
47
|
+
export type StoredEntry = Omit<StoredMessage, "seq" | "at">;
|
|
48
|
+
/** A change of a presence set, as adapters deliver it. @since 1.0.0 */
|
|
49
|
+
export interface PresenceEvent {
|
|
50
|
+
readonly channel: string;
|
|
51
|
+
readonly op: "join" | "set" | "leave";
|
|
52
|
+
readonly member: Member;
|
|
53
|
+
}
|
|
54
|
+
/** Frames a client sends. @since 1.0.0 */
|
|
55
|
+
export type ClientFrame = {
|
|
56
|
+
readonly t: "hello";
|
|
57
|
+
readonly v: number;
|
|
58
|
+
} | {
|
|
59
|
+
readonly t: "sub";
|
|
60
|
+
readonly n: number;
|
|
61
|
+
readonly c: string;
|
|
62
|
+
readonly after?: number;
|
|
63
|
+
readonly echo?: boolean;
|
|
64
|
+
readonly presence?: Json;
|
|
65
|
+
readonly hasPresence: boolean;
|
|
66
|
+
readonly watch?: boolean;
|
|
67
|
+
} | {
|
|
68
|
+
readonly t: "unsub";
|
|
69
|
+
readonly n?: number;
|
|
70
|
+
readonly c: string;
|
|
71
|
+
} | {
|
|
72
|
+
readonly t: "pub";
|
|
73
|
+
readonly c: string;
|
|
74
|
+
readonly e: string;
|
|
75
|
+
readonly d: string;
|
|
76
|
+
readonly n?: number;
|
|
77
|
+
readonly k?: string;
|
|
78
|
+
} | {
|
|
79
|
+
readonly t: "req";
|
|
80
|
+
readonly n: number;
|
|
81
|
+
readonly c: string;
|
|
82
|
+
readonly e: string;
|
|
83
|
+
readonly d: Json;
|
|
84
|
+
readonly ms?: number;
|
|
85
|
+
} | {
|
|
86
|
+
readonly t: "pres";
|
|
87
|
+
readonly c: string;
|
|
88
|
+
readonly s: Json;
|
|
89
|
+
} | {
|
|
90
|
+
readonly t: "ping";
|
|
91
|
+
readonly n?: number;
|
|
92
|
+
};
|
|
93
|
+
/** What decoding a client frame yields. @since 1.0.0 */
|
|
94
|
+
export type DecodedClientFrame = {
|
|
95
|
+
readonly ok: true;
|
|
96
|
+
readonly frame: ClientFrame;
|
|
97
|
+
} | {
|
|
98
|
+
readonly ok: false;
|
|
99
|
+
readonly code: ErrorCode;
|
|
100
|
+
readonly n?: number;
|
|
101
|
+
readonly why: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.
|
|
105
|
+
* @param value The candidate.
|
|
106
|
+
* @returns True for a valid name.
|
|
107
|
+
* @example
|
|
108
|
+
* import { isChannelName } from "@shadow-js/realtime";
|
|
109
|
+
*
|
|
110
|
+
* isChannelName("chat:lobby"); // true
|
|
111
|
+
* isChannelName("chat lobby"); // false
|
|
112
|
+
* @since 1.0.0
|
|
113
|
+
*/
|
|
114
|
+
export declare function isChannelName(value: unknown): value is string;
|
|
115
|
+
/**
|
|
116
|
+
* Whether `value` is a valid event name: 1..64 characters of `A-Za-z0-9_.:@-`.
|
|
117
|
+
* @param value The candidate.
|
|
118
|
+
* @returns True for a valid name.
|
|
119
|
+
* @example
|
|
120
|
+
* import { isEventName } from "@shadow-js/realtime";
|
|
121
|
+
*
|
|
122
|
+
* isEventName("message"); // true
|
|
123
|
+
* isEventName("a/b"); // false
|
|
124
|
+
* @since 1.0.0
|
|
125
|
+
*/
|
|
126
|
+
export declare function isEventName(value: unknown): value is string;
|
|
127
|
+
/**
|
|
128
|
+
* Decodes and validates one client frame (server side). Never throws: any text yields a frame or a refusal.
|
|
129
|
+
*
|
|
130
|
+
* @param text The frame text.
|
|
131
|
+
* @param maxBytes The largest accepted frame, in UTF-8 bytes.
|
|
132
|
+
* @returns The frame, or the code and reason to refuse it with.
|
|
133
|
+
* @example
|
|
134
|
+
* import { decodeClientFrame } from "@shadow-js/realtime";
|
|
135
|
+
*
|
|
136
|
+
* const result = decodeClientFrame('{"t":"ping"}', 65_536);
|
|
137
|
+
* result.ok; // true
|
|
138
|
+
* @since 1.0.0
|
|
139
|
+
*/
|
|
140
|
+
export declare function decodeClientFrame(text: string, maxBytes: number): DecodedClientFrame;
|
|
141
|
+
/** Frames a server sends, as a client reads them. @since 1.0.0 */
|
|
142
|
+
export type ServerFrame = {
|
|
143
|
+
readonly t: "ready";
|
|
144
|
+
readonly v: number;
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly hb: number;
|
|
147
|
+
readonly max: number;
|
|
148
|
+
} | {
|
|
149
|
+
readonly t: "subbed";
|
|
150
|
+
readonly n: number;
|
|
151
|
+
readonly c: string;
|
|
152
|
+
readonly head: number;
|
|
153
|
+
readonly gap?: boolean;
|
|
154
|
+
readonly reset?: boolean;
|
|
155
|
+
} | {
|
|
156
|
+
readonly t: "unsubbed";
|
|
157
|
+
readonly c: string;
|
|
158
|
+
readonly why?: string;
|
|
159
|
+
} | {
|
|
160
|
+
readonly t: "msg";
|
|
161
|
+
readonly c: string;
|
|
162
|
+
readonly s: number;
|
|
163
|
+
readonly e: string;
|
|
164
|
+
readonly d: Json;
|
|
165
|
+
readonly p?: string;
|
|
166
|
+
readonly u?: string;
|
|
167
|
+
readonly r?: 1;
|
|
168
|
+
} | {
|
|
169
|
+
readonly t: "ack";
|
|
170
|
+
readonly n: number;
|
|
171
|
+
readonly s: number;
|
|
172
|
+
readonly dup?: boolean;
|
|
173
|
+
} | {
|
|
174
|
+
readonly t: "rep";
|
|
175
|
+
readonly n: number;
|
|
176
|
+
readonly d: Json;
|
|
177
|
+
} | {
|
|
178
|
+
readonly t: "err";
|
|
179
|
+
readonly n?: number;
|
|
180
|
+
readonly c?: string;
|
|
181
|
+
readonly code: ErrorCode;
|
|
182
|
+
readonly msg?: string;
|
|
183
|
+
} | {
|
|
184
|
+
readonly t: "pres";
|
|
185
|
+
readonly c: string;
|
|
186
|
+
readonly op: "sync" | "join" | "set" | "leave";
|
|
187
|
+
readonly m: Member | readonly Member[];
|
|
188
|
+
} | {
|
|
189
|
+
readonly t: "pong";
|
|
190
|
+
readonly n?: number;
|
|
191
|
+
} | {
|
|
192
|
+
readonly t: "bye";
|
|
193
|
+
readonly code: number;
|
|
194
|
+
readonly why: string;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Decodes one server frame (client side). Returns `undefined` for anything that is not a known, well-formed frame, so a
|
|
198
|
+
* client ignores what a newer server may send and never acts on a malformed frame.
|
|
199
|
+
*
|
|
200
|
+
* @param text The frame text.
|
|
201
|
+
* @returns The frame, or undefined.
|
|
202
|
+
* @example
|
|
203
|
+
* import { decodeServerFrame } from "@shadow-js/realtime";
|
|
204
|
+
*
|
|
205
|
+
* decodeServerFrame('{"t":"pong"}'); // { t: "pong" }
|
|
206
|
+
* decodeServerFrame("not json"); // undefined
|
|
207
|
+
* @since 1.0.0
|
|
208
|
+
*/
|
|
209
|
+
export declare function decodeServerFrame(text: string): ServerFrame | undefined;
|
|
210
|
+
//#endregion
|
|
211
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//#region src/protocol.ts
|
|
2
|
+
/**
|
|
3
|
+
* The wire protocol version this package speaks (https://realtime.shadowjs.net/guides/realtime-protocol/).
|
|
4
|
+
* @example
|
|
5
|
+
* import { PROTOCOL_VERSION } from "@shadow-js/realtime";
|
|
6
|
+
*
|
|
7
|
+
* const hello = JSON.stringify({ t: "hello", v: PROTOCOL_VERSION });
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
const PROTOCOL_VERSION = 1;
|
|
11
|
+
const CHANNEL = /^[A-Za-z0-9_.:/@-]{1,128}$/;
|
|
12
|
+
const EVENT = /^[A-Za-z0-9_.:@-]{1,64}$/;
|
|
13
|
+
const KEY = /^[A-Za-z0-9_.:@-]{1,64}$/;
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
/**
|
|
16
|
+
* Whether `value` is a valid channel name: 1..128 characters of `A-Za-z0-9_.:/@-`, not starting with the reserved `$`.
|
|
17
|
+
* @param value The candidate.
|
|
18
|
+
* @returns True for a valid name.
|
|
19
|
+
* @example
|
|
20
|
+
* import { isChannelName } from "@shadow-js/realtime";
|
|
21
|
+
*
|
|
22
|
+
* isChannelName("chat:lobby"); // true
|
|
23
|
+
* isChannelName("chat lobby"); // false
|
|
24
|
+
* @since 1.0.0
|
|
25
|
+
*/
|
|
26
|
+
function isChannelName(value) {
|
|
27
|
+
return typeof value === "string" && CHANNEL.test(value);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether `value` is a valid event name: 1..64 characters of `A-Za-z0-9_.:@-`.
|
|
31
|
+
* @param value The candidate.
|
|
32
|
+
* @returns True for a valid name.
|
|
33
|
+
* @example
|
|
34
|
+
* import { isEventName } from "@shadow-js/realtime";
|
|
35
|
+
*
|
|
36
|
+
* isEventName("message"); // true
|
|
37
|
+
* isEventName("a/b"); // false
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
*/
|
|
40
|
+
function isEventName(value) {
|
|
41
|
+
return typeof value === "string" && EVENT.test(value);
|
|
42
|
+
}
|
|
43
|
+
/** Whether `value` is a valid idempotency key. @internal */
|
|
44
|
+
function isIdempotencyKey(value) {
|
|
45
|
+
return typeof value === "string" && KEY.test(value);
|
|
46
|
+
}
|
|
47
|
+
/** The number of bytes of `text` in UTF-8, without encoding the common cases. @internal */
|
|
48
|
+
function byteLength(text) {
|
|
49
|
+
if (text.length * 3 <= 2147483647) {
|
|
50
|
+
let ascii = true;
|
|
51
|
+
for (let at = 0; at < text.length; at += 1) if (text.charCodeAt(at) > 127) {
|
|
52
|
+
ascii = false;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (ascii) return text.length;
|
|
56
|
+
}
|
|
57
|
+
return encoder.encode(text).length;
|
|
58
|
+
}
|
|
59
|
+
function isCount(value) {
|
|
60
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
61
|
+
}
|
|
62
|
+
function bad(why, n) {
|
|
63
|
+
return isCount(n) ? {
|
|
64
|
+
ok: false,
|
|
65
|
+
code: "bad_frame",
|
|
66
|
+
n,
|
|
67
|
+
why
|
|
68
|
+
} : {
|
|
69
|
+
ok: false,
|
|
70
|
+
code: "bad_frame",
|
|
71
|
+
why
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decodes and validates one client frame (server side). Never throws: any text yields a frame or a refusal.
|
|
76
|
+
*
|
|
77
|
+
* @param text The frame text.
|
|
78
|
+
* @param maxBytes The largest accepted frame, in UTF-8 bytes.
|
|
79
|
+
* @returns The frame, or the code and reason to refuse it with.
|
|
80
|
+
* @example
|
|
81
|
+
* import { decodeClientFrame } from "@shadow-js/realtime";
|
|
82
|
+
*
|
|
83
|
+
* const result = decodeClientFrame('{"t":"ping"}', 65_536);
|
|
84
|
+
* result.ok; // true
|
|
85
|
+
* @since 1.0.0
|
|
86
|
+
*/
|
|
87
|
+
function decodeClientFrame(text, maxBytes) {
|
|
88
|
+
if (byteLength(text) > maxBytes) return {
|
|
89
|
+
ok: false,
|
|
90
|
+
code: "too_large",
|
|
91
|
+
why: "the frame is larger than the limit"
|
|
92
|
+
};
|
|
93
|
+
let value;
|
|
94
|
+
try {
|
|
95
|
+
value = JSON.parse(text);
|
|
96
|
+
} catch {
|
|
97
|
+
return bad("the frame is not JSON");
|
|
98
|
+
}
|
|
99
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return bad("the frame is not an object");
|
|
100
|
+
const raw = value;
|
|
101
|
+
const type = raw["t"];
|
|
102
|
+
const n = raw["n"];
|
|
103
|
+
if (n !== void 0 && !isCount(n)) return bad("n must be a non-negative integer");
|
|
104
|
+
const c = raw["c"];
|
|
105
|
+
const needChannel = () => isChannelName(c) ? void 0 : {
|
|
106
|
+
...bad("c is not a valid channel name", n),
|
|
107
|
+
code: "bad_channel"
|
|
108
|
+
};
|
|
109
|
+
switch (type) {
|
|
110
|
+
case "hello": {
|
|
111
|
+
const v = raw["v"];
|
|
112
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) return bad("v must be a version");
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
frame: {
|
|
116
|
+
t: "hello",
|
|
117
|
+
v
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
case "ping": return {
|
|
122
|
+
ok: true,
|
|
123
|
+
frame: n === void 0 ? { t: "ping" } : {
|
|
124
|
+
t: "ping",
|
|
125
|
+
n
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
case "sub": {
|
|
129
|
+
const refusal = needChannel();
|
|
130
|
+
if (refusal !== void 0) return refusal;
|
|
131
|
+
if (n === void 0) return bad("sub needs n");
|
|
132
|
+
const after = raw["after"];
|
|
133
|
+
if (after !== void 0 && !isCount(after)) return bad("after must be a non-negative integer", n);
|
|
134
|
+
const echo = raw["echo"];
|
|
135
|
+
if (echo !== void 0 && typeof echo !== "boolean") return bad("echo must be a boolean", n);
|
|
136
|
+
const watch = raw["watch"];
|
|
137
|
+
if (watch !== void 0 && typeof watch !== "boolean") return bad("watch must be a boolean", n);
|
|
138
|
+
const hasPresence = "presence" in raw;
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
frame: {
|
|
142
|
+
t: "sub",
|
|
143
|
+
n,
|
|
144
|
+
c,
|
|
145
|
+
hasPresence,
|
|
146
|
+
...after === void 0 ? {} : { after },
|
|
147
|
+
...echo === void 0 ? {} : { echo },
|
|
148
|
+
...watch === void 0 ? {} : { watch },
|
|
149
|
+
...hasPresence ? { presence: raw["presence"] } : {}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
case "unsub": {
|
|
154
|
+
const refusal = needChannel();
|
|
155
|
+
if (refusal !== void 0) return refusal;
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
frame: n === void 0 ? {
|
|
159
|
+
t: "unsub",
|
|
160
|
+
c
|
|
161
|
+
} : {
|
|
162
|
+
t: "unsub",
|
|
163
|
+
n,
|
|
164
|
+
c
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
case "pub": {
|
|
169
|
+
const refusal = needChannel();
|
|
170
|
+
if (refusal !== void 0) return refusal;
|
|
171
|
+
const e = raw["e"];
|
|
172
|
+
if (!isEventName(e)) return bad("e is not a valid event name", n);
|
|
173
|
+
const k = raw["k"];
|
|
174
|
+
if (k !== void 0 && !isIdempotencyKey(k)) return bad("k is not a valid key", n);
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
frame: {
|
|
178
|
+
t: "pub",
|
|
179
|
+
c,
|
|
180
|
+
e,
|
|
181
|
+
d: JSON.stringify(raw["d"] ?? null),
|
|
182
|
+
...n === void 0 ? {} : { n },
|
|
183
|
+
...k === void 0 ? {} : { k }
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
case "req": {
|
|
188
|
+
const refusal = needChannel();
|
|
189
|
+
if (refusal !== void 0) return refusal;
|
|
190
|
+
if (n === void 0) return bad("req needs n");
|
|
191
|
+
const e = raw["e"];
|
|
192
|
+
if (!isEventName(e)) return bad("e is not a valid event name", n);
|
|
193
|
+
const ms = raw["ms"];
|
|
194
|
+
if (ms !== void 0 && !(typeof ms === "number" && Number.isFinite(ms) && ms > 0)) return bad("ms must be a positive number", n);
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
frame: {
|
|
198
|
+
t: "req",
|
|
199
|
+
n,
|
|
200
|
+
c,
|
|
201
|
+
e,
|
|
202
|
+
d: raw["d"] ?? null,
|
|
203
|
+
...ms === void 0 ? {} : { ms }
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
case "pres": {
|
|
208
|
+
const refusal = needChannel();
|
|
209
|
+
if (refusal !== void 0) return refusal;
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
frame: {
|
|
213
|
+
t: "pres",
|
|
214
|
+
c,
|
|
215
|
+
s: raw["s"] ?? null
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
default: return bad("unknown frame type", n);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Decodes one server frame (client side). Returns `undefined` for anything that is not a known, well-formed frame, so a
|
|
224
|
+
* client ignores what a newer server may send and never acts on a malformed frame.
|
|
225
|
+
*
|
|
226
|
+
* @param text The frame text.
|
|
227
|
+
* @returns The frame, or undefined.
|
|
228
|
+
* @example
|
|
229
|
+
* import { decodeServerFrame } from "@shadow-js/realtime";
|
|
230
|
+
*
|
|
231
|
+
* decodeServerFrame('{"t":"pong"}'); // { t: "pong" }
|
|
232
|
+
* decodeServerFrame("not json"); // undefined
|
|
233
|
+
* @since 1.0.0
|
|
234
|
+
*/
|
|
235
|
+
function decodeServerFrame(text) {
|
|
236
|
+
let value;
|
|
237
|
+
try {
|
|
238
|
+
value = JSON.parse(text);
|
|
239
|
+
} catch {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
243
|
+
const raw = value;
|
|
244
|
+
const str = (key) => {
|
|
245
|
+
const item = raw[key];
|
|
246
|
+
return typeof item === "string" ? item : void 0;
|
|
247
|
+
};
|
|
248
|
+
const num = (key) => {
|
|
249
|
+
const item = raw[key];
|
|
250
|
+
return isCount(item) ? item : void 0;
|
|
251
|
+
};
|
|
252
|
+
switch (raw["t"]) {
|
|
253
|
+
case "ready": {
|
|
254
|
+
const v = num("v");
|
|
255
|
+
const id = str("id");
|
|
256
|
+
if (v === void 0 || id === void 0) return void 0;
|
|
257
|
+
return {
|
|
258
|
+
t: "ready",
|
|
259
|
+
v,
|
|
260
|
+
id,
|
|
261
|
+
hb: num("hb") ?? 3e4,
|
|
262
|
+
max: num("max") ?? 65536
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case "subbed": {
|
|
266
|
+
const n = num("n");
|
|
267
|
+
const c = str("c");
|
|
268
|
+
const head = num("head");
|
|
269
|
+
if (n === void 0 || c === void 0 || head === void 0) return void 0;
|
|
270
|
+
return {
|
|
271
|
+
t: "subbed",
|
|
272
|
+
n,
|
|
273
|
+
c,
|
|
274
|
+
head,
|
|
275
|
+
...raw["gap"] === true ? { gap: true } : {},
|
|
276
|
+
...raw["reset"] === true ? { reset: true } : {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
case "unsubbed": {
|
|
280
|
+
const c = str("c");
|
|
281
|
+
if (c === void 0) return void 0;
|
|
282
|
+
const why = str("why");
|
|
283
|
+
return why === void 0 ? {
|
|
284
|
+
t: "unsubbed",
|
|
285
|
+
c
|
|
286
|
+
} : {
|
|
287
|
+
t: "unsubbed",
|
|
288
|
+
c,
|
|
289
|
+
why
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
case "msg": {
|
|
293
|
+
const c = str("c");
|
|
294
|
+
const s = num("s");
|
|
295
|
+
const e = str("e");
|
|
296
|
+
if (c === void 0 || s === void 0 || e === void 0 || s < 1) return void 0;
|
|
297
|
+
const p = str("p");
|
|
298
|
+
const u = str("u");
|
|
299
|
+
return {
|
|
300
|
+
t: "msg",
|
|
301
|
+
c,
|
|
302
|
+
s,
|
|
303
|
+
e,
|
|
304
|
+
d: raw["d"],
|
|
305
|
+
...p === void 0 ? {} : { p },
|
|
306
|
+
...u === void 0 ? {} : { u },
|
|
307
|
+
...raw["r"] === 1 ? { r: 1 } : {}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "ack": {
|
|
311
|
+
const n = num("n");
|
|
312
|
+
const s = num("s");
|
|
313
|
+
if (n === void 0 || s === void 0) return void 0;
|
|
314
|
+
return {
|
|
315
|
+
t: "ack",
|
|
316
|
+
n,
|
|
317
|
+
s,
|
|
318
|
+
...raw["dup"] === true ? { dup: true } : {}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
case "rep": {
|
|
322
|
+
const n = num("n");
|
|
323
|
+
if (n === void 0) return void 0;
|
|
324
|
+
return {
|
|
325
|
+
t: "rep",
|
|
326
|
+
n,
|
|
327
|
+
d: raw["d"]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
case "err": {
|
|
331
|
+
const code = str("code");
|
|
332
|
+
if (code === void 0) return void 0;
|
|
333
|
+
const n = num("n");
|
|
334
|
+
const c = str("c");
|
|
335
|
+
const msg = str("msg");
|
|
336
|
+
return {
|
|
337
|
+
t: "err",
|
|
338
|
+
code,
|
|
339
|
+
...n === void 0 ? {} : { n },
|
|
340
|
+
...c === void 0 ? {} : { c },
|
|
341
|
+
...msg === void 0 ? {} : { msg }
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case "pres": {
|
|
345
|
+
const c = str("c");
|
|
346
|
+
const op = raw["op"];
|
|
347
|
+
const m = raw["m"];
|
|
348
|
+
if (c === void 0 || op !== "sync" && op !== "join" && op !== "set" && op !== "leave") return void 0;
|
|
349
|
+
const member = (item) => {
|
|
350
|
+
if (item === null || typeof item !== "object") return void 0;
|
|
351
|
+
const entry = item;
|
|
352
|
+
if (typeof entry["id"] !== "string" || !isCount(entry["v"])) return void 0;
|
|
353
|
+
return {
|
|
354
|
+
id: entry["id"],
|
|
355
|
+
v: entry["v"],
|
|
356
|
+
s: entry["s"] ?? null,
|
|
357
|
+
...typeof entry["u"] === "string" ? { u: entry["u"] } : {}
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
if (op === "sync") {
|
|
361
|
+
if (!Array.isArray(m)) return void 0;
|
|
362
|
+
const list = m.map(member);
|
|
363
|
+
if (list.some((item) => item === void 0)) return void 0;
|
|
364
|
+
return {
|
|
365
|
+
t: "pres",
|
|
366
|
+
c,
|
|
367
|
+
op,
|
|
368
|
+
m: list
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const one = member(m);
|
|
372
|
+
return one === void 0 ? void 0 : {
|
|
373
|
+
t: "pres",
|
|
374
|
+
c,
|
|
375
|
+
op,
|
|
376
|
+
m: one
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case "pong": {
|
|
380
|
+
const n = num("n");
|
|
381
|
+
return n === void 0 ? { t: "pong" } : {
|
|
382
|
+
t: "pong",
|
|
383
|
+
n
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
case "bye": {
|
|
387
|
+
const code = num("code");
|
|
388
|
+
return code === void 0 ? void 0 : {
|
|
389
|
+
t: "bye",
|
|
390
|
+
code,
|
|
391
|
+
why: str("why") ?? ""
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
default: return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
export { PROTOCOL_VERSION, byteLength, decodeClientFrame, decodeServerFrame, isChannelName, isEventName, isIdempotencyKey };
|
|
399
|
+
|
|
400
|
+
//# sourceMappingURL=protocol.js.map
|