@monlite/realtime 0.1.0 → 0.2.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/README.md +4 -0
- package/dist/client.cjs +17 -10
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +17 -10
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +49 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +49 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -64,6 +64,10 @@ stop(); // unsubscribe
|
|
|
64
64
|
live.close(); // unsubscribe everything
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Client options: `{ token, path, reconnectMs, fetch, onError }`. `onError` (default `console.error`)
|
|
68
|
+
receives any server-sent `{ error }` frame — e.g. a watch that failed server-side — so it is never
|
|
69
|
+
mis-delivered as a snapshot or a `null` document.
|
|
70
|
+
|
|
67
71
|
## How it works
|
|
68
72
|
|
|
69
73
|
- One SSE stream per subscription; the query travels in the URL.
|
package/dist/client.cjs
CHANGED
|
@@ -25,20 +25,27 @@ function connectRealtime(baseUrl, opts = {}) {
|
|
|
25
25
|
const reader = res.body.getReader();
|
|
26
26
|
const decoder = new TextDecoder();
|
|
27
27
|
let buf = "";
|
|
28
|
+
const FRAME_SEP = /\r\n\r\n|\n\n|\r\r/;
|
|
28
29
|
for (; ; ) {
|
|
29
30
|
const { value, done } = await reader.read();
|
|
30
31
|
if (done) break;
|
|
31
32
|
buf += decoder.decode(value, { stream: true });
|
|
32
|
-
let
|
|
33
|
-
while (
|
|
34
|
-
const frame = buf.slice(0,
|
|
35
|
-
buf = buf.slice(
|
|
36
|
-
const data = frame.split(
|
|
37
|
-
if (data)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
let m;
|
|
34
|
+
while (m = FRAME_SEP.exec(buf)) {
|
|
35
|
+
const frame = buf.slice(0, m.index);
|
|
36
|
+
buf = buf.slice(m.index + m[0].length);
|
|
37
|
+
const data = frame.split(/\r\n|\n|\r/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
38
|
+
if (!data) continue;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(data);
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
46
|
+
(opts.onError ?? ((e) => console.error("realtime:", e)))(parsed.error);
|
|
47
|
+
} else {
|
|
48
|
+
onMessage(parsed);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
}
|
package/dist/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.cjs","sourcesContent":["import type {\n Doc,\n LiveEvent,\n WatchArgs,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\nexport interface RealtimeClientOptions {\n /** Bearer token sent as `Authorization` (and `?token=` for EventSource-style auth). */\n token?: string;\n /** Base path on the server. Default `\"/realtime\"`. */\n path?: string;\n /** Reconnect backoff in ms after a dropped stream. Default `1000`. */\n reconnectMs?: number;\n /** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/** Unsubscribe from a live stream. */\nexport type Unsubscribe = () => void;\n\nexport interface QueryBuilder<T = Doc> {\n where(where: WhereInput<T>): QueryBuilder<T>;\n orderBy(orderBy: WatchArgs<T>[\"orderBy\"]): QueryBuilder<T>;\n take(n: number): QueryBuilder<T>;\n skip(n: number): QueryBuilder<T>;\n /** Only emit when one of these fields changes (server-side filter). */\n fields(fields: (keyof T | (string & {}))[]): QueryBuilder<T>;\n /** Subscribe. The callback fires with each {@link LiveEvent} (init, then changes). */\n onSnapshot(cb: (event: LiveEvent<T>) => void): Unsubscribe;\n}\n\nexport interface RealtimeClient {\n /** Live query over a collection. */\n collection<T = Doc>(name: string): QueryBuilder<T>;\n /** Live single-document listener (`null` while absent / on delete). */\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe;\n /** Close all subscriptions. */\n close(): void;\n}\n\n/**\n * Connect to a `@monlite/realtime` server and subscribe to live queries and\n * documents over SSE. Works in the browser and Node ≥ 18 (uses `fetch`).\n *\n * ```ts\n * const live = connectRealtime(\"http://localhost:8080\", { token });\n * const stop = live.collection(\"orders\").where({ status: \"open\" }).onSnapshot(render);\n * const stopDoc = live.doc(\"orders\", \"o-123\", (doc) => render(doc));\n * ```\n */\nexport function connectRealtime(\n baseUrl: string,\n opts: RealtimeClientOptions = {},\n): RealtimeClient {\n const root = baseUrl.replace(/\\/$/, \"\");\n const base = (opts.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const reconnectMs = opts.reconnectMs ?? 1000;\n const doFetch = opts.fetch ?? globalThis.fetch;\n const subs = new Set<Unsubscribe>();\n\n /** Open an auto-reconnecting SSE stream; returns an unsubscribe. */\n function stream(path: string, onMessage: (data: any) => void): Unsubscribe {\n let stopped = false;\n let controller: AbortController | undefined;\n\n const url = new URL(root + base + path);\n if (opts.token) url.searchParams.set(\"token\", opts.token);\n\n (async () => {\n while (!stopped) {\n controller = new AbortController();\n try {\n const res = await doFetch(url.toString(), {\n headers: opts.token\n ? { authorization: `Bearer ${opts.token}` }\n : {},\n signal: controller.signal,\n });\n if (!res.ok || !res.body)\n throw new Error(`realtime HTTP ${res.status}`);\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\n\\n\")) >= 0) {\n const frame = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n const data = frame\n .split(\"\\n\")\n .filter((l) => l.startsWith(\"data:\"))\n .map((l) => l.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (data) {\n try {\n onMessage(JSON.parse(data));\n } catch {\n /* ignore malformed frame */\n }\n }\n }\n }\n } catch {\n /* network/abort — fall through to reconnect */\n }\n if (stopped) break;\n await new Promise((r) => setTimeout(r, reconnectMs));\n }\n })();\n\n const unsub: Unsubscribe = () => {\n stopped = true;\n controller?.abort();\n subs.delete(unsub);\n };\n subs.add(unsub);\n return unsub;\n }\n\n function buildQuery<T>(name: string): QueryBuilder<T> {\n const args: WatchArgs<T> = {};\n const api: QueryBuilder<T> = {\n where(w) {\n args.where = w;\n return api;\n },\n orderBy(o) {\n args.orderBy = o;\n return api;\n },\n take(n) {\n args.take = n;\n return api;\n },\n skip(n) {\n args.skip = n;\n return api;\n },\n fields(f) {\n args.fields = f;\n return api;\n },\n onSnapshot(cb) {\n const qp = `?coll=${encodeURIComponent(name)}&q=${encodeURIComponent(\n JSON.stringify(args),\n )}`;\n return stream(`/query${qp}`, (data) => cb(data as LiveEvent<T>));\n },\n };\n return api;\n }\n\n return {\n collection<T = Doc>(name: string) {\n return buildQuery<T>(name);\n },\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe {\n const qp = `?coll=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`;\n return stream(`/doc${qp}`, (data) =>\n cb((data?.doc ?? null) as WithId<T> | null),\n );\n },\n close() {\n for (const unsub of [...subs]) unsub();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.cjs","sourcesContent":["import type {\n Doc,\n LiveEvent,\n WatchArgs,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\nexport interface RealtimeClientOptions {\n /** Bearer token sent as `Authorization` (and `?token=` for EventSource-style auth). */\n token?: string;\n /** Base path on the server. Default `\"/realtime\"`. */\n path?: string;\n /** Reconnect backoff in ms after a dropped stream. Default `1000`. */\n reconnectMs?: number;\n /** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n /** Called for a server-sent `{ error }` frame. Defaults to `console.error`. */\n onError?: (error: unknown) => void;\n}\n\n/** Unsubscribe from a live stream. */\nexport type Unsubscribe = () => void;\n\nexport interface QueryBuilder<T = Doc> {\n where(where: WhereInput<T>): QueryBuilder<T>;\n orderBy(orderBy: WatchArgs<T>[\"orderBy\"]): QueryBuilder<T>;\n take(n: number): QueryBuilder<T>;\n skip(n: number): QueryBuilder<T>;\n /** Only emit when one of these fields changes (server-side filter). */\n fields(fields: (keyof T | (string & {}))[]): QueryBuilder<T>;\n /** Subscribe. The callback fires with each {@link LiveEvent} (init, then changes). */\n onSnapshot(cb: (event: LiveEvent<T>) => void): Unsubscribe;\n}\n\nexport interface RealtimeClient {\n /** Live query over a collection. */\n collection<T = Doc>(name: string): QueryBuilder<T>;\n /** Live single-document listener (`null` while absent / on delete). */\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe;\n /** Close all subscriptions. */\n close(): void;\n}\n\n/**\n * Connect to a `@monlite/realtime` server and subscribe to live queries and\n * documents over SSE. Works in the browser and Node ≥ 18 (uses `fetch`).\n *\n * ```ts\n * const live = connectRealtime(\"http://localhost:8080\", { token });\n * const stop = live.collection(\"orders\").where({ status: \"open\" }).onSnapshot(render);\n * const stopDoc = live.doc(\"orders\", \"o-123\", (doc) => render(doc));\n * ```\n */\nexport function connectRealtime(\n baseUrl: string,\n opts: RealtimeClientOptions = {},\n): RealtimeClient {\n const root = baseUrl.replace(/\\/$/, \"\");\n const base = (opts.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const reconnectMs = opts.reconnectMs ?? 1000;\n const doFetch = opts.fetch ?? globalThis.fetch;\n const subs = new Set<Unsubscribe>();\n\n /** Open an auto-reconnecting SSE stream; returns an unsubscribe. */\n function stream(path: string, onMessage: (data: any) => void): Unsubscribe {\n let stopped = false;\n let controller: AbortController | undefined;\n\n const url = new URL(root + base + path);\n if (opts.token) url.searchParams.set(\"token\", opts.token);\n\n (async () => {\n while (!stopped) {\n controller = new AbortController();\n try {\n const res = await doFetch(url.toString(), {\n headers: opts.token\n ? { authorization: `Bearer ${opts.token}` }\n : {},\n signal: controller.signal,\n });\n if (!res.ok || !res.body)\n throw new Error(`realtime HTTP ${res.status}`);\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n // Tolerate LF, CRLF and CR frame/line separators (SSE permits all).\n const FRAME_SEP = /\\r\\n\\r\\n|\\n\\n|\\r\\r/;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let m: RegExpExecArray | null;\n while ((m = FRAME_SEP.exec(buf))) {\n const frame = buf.slice(0, m.index);\n buf = buf.slice(m.index + m[0].length);\n const data = frame\n .split(/\\r\\n|\\n|\\r/)\n .filter((l) => l.startsWith(\"data:\"))\n .map((l) => l.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (!data) continue; // comment/heartbeat (\":\" lines) → ignore\n let parsed: any;\n try {\n parsed = JSON.parse(data);\n } catch {\n continue; // malformed frame\n }\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n (\n opts.onError ??\n ((e: unknown) => console.error(\"realtime:\", e))\n )(parsed.error);\n } else {\n onMessage(parsed);\n }\n }\n }\n } catch {\n /* network/abort — fall through to reconnect */\n }\n if (stopped) break;\n await new Promise((r) => setTimeout(r, reconnectMs));\n }\n })();\n\n const unsub: Unsubscribe = () => {\n stopped = true;\n controller?.abort();\n subs.delete(unsub);\n };\n subs.add(unsub);\n return unsub;\n }\n\n function buildQuery<T>(name: string): QueryBuilder<T> {\n const args: WatchArgs<T> = {};\n const api: QueryBuilder<T> = {\n where(w) {\n args.where = w;\n return api;\n },\n orderBy(o) {\n args.orderBy = o;\n return api;\n },\n take(n) {\n args.take = n;\n return api;\n },\n skip(n) {\n args.skip = n;\n return api;\n },\n fields(f) {\n args.fields = f;\n return api;\n },\n onSnapshot(cb) {\n const qp = `?coll=${encodeURIComponent(name)}&q=${encodeURIComponent(\n JSON.stringify(args),\n )}`;\n return stream(`/query${qp}`, (data) => cb(data as LiveEvent<T>));\n },\n };\n return api;\n }\n\n return {\n collection<T = Doc>(name: string) {\n return buildQuery<T>(name);\n },\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe {\n const qp = `?coll=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`;\n return stream(`/doc${qp}`, (data) =>\n cb((data?.doc ?? null) as WithId<T> | null),\n );\n },\n close() {\n for (const unsub of [...subs]) unsub();\n },\n };\n}\n"]}
|
package/dist/client.d.cts
CHANGED
|
@@ -9,6 +9,8 @@ interface RealtimeClientOptions {
|
|
|
9
9
|
reconnectMs?: number;
|
|
10
10
|
/** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */
|
|
11
11
|
fetch?: typeof fetch;
|
|
12
|
+
/** Called for a server-sent `{ error }` frame. Defaults to `console.error`. */
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
12
14
|
}
|
|
13
15
|
/** Unsubscribe from a live stream. */
|
|
14
16
|
type Unsubscribe = () => void;
|
package/dist/client.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ interface RealtimeClientOptions {
|
|
|
9
9
|
reconnectMs?: number;
|
|
10
10
|
/** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */
|
|
11
11
|
fetch?: typeof fetch;
|
|
12
|
+
/** Called for a server-sent `{ error }` frame. Defaults to `console.error`. */
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
12
14
|
}
|
|
13
15
|
/** Unsubscribe from a live stream. */
|
|
14
16
|
type Unsubscribe = () => void;
|
package/dist/client.js
CHANGED
|
@@ -23,20 +23,27 @@ function connectRealtime(baseUrl, opts = {}) {
|
|
|
23
23
|
const reader = res.body.getReader();
|
|
24
24
|
const decoder = new TextDecoder();
|
|
25
25
|
let buf = "";
|
|
26
|
+
const FRAME_SEP = /\r\n\r\n|\n\n|\r\r/;
|
|
26
27
|
for (; ; ) {
|
|
27
28
|
const { value, done } = await reader.read();
|
|
28
29
|
if (done) break;
|
|
29
30
|
buf += decoder.decode(value, { stream: true });
|
|
30
|
-
let
|
|
31
|
-
while (
|
|
32
|
-
const frame = buf.slice(0,
|
|
33
|
-
buf = buf.slice(
|
|
34
|
-
const data = frame.split(
|
|
35
|
-
if (data)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
let m;
|
|
32
|
+
while (m = FRAME_SEP.exec(buf)) {
|
|
33
|
+
const frame = buf.slice(0, m.index);
|
|
34
|
+
buf = buf.slice(m.index + m[0].length);
|
|
35
|
+
const data = frame.split(/\r\n|\n|\r/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
36
|
+
if (!data) continue;
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(data);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
44
|
+
(opts.onError ?? ((e) => console.error("realtime:", e)))(parsed.error);
|
|
45
|
+
} else {
|
|
46
|
+
onMessage(parsed);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":";AAwDO,SAAS,eAAA,CACd,OAAA,EACA,IAAA,GAA8B,EAAC,EACf;AAChB,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AACtC,EAAA,MAAM,QAAQ,IAAA,CAAK,IAAA,IAAQ,WAAA,EAAa,OAAA,CAAQ,OAAO,EAAE,CAAA;AACzD,EAAA,MAAM,WAAA,GAAc,KAAK,WAAA,IAAe,GAAA;AACxC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,IAAS,UAAA,CAAW,KAAA;AACzC,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAiB;AAGlC,EAAA,SAAS,MAAA,CAAO,MAAc,SAAA,EAA6C;AACzE,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,IAAI,UAAA;AAEJ,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,GAAO,OAAO,IAAI,CAAA;AACtC,IAAA,IAAI,KAAK,KAAA,EAAO,GAAA,CAAI,aAAa,GAAA,CAAI,OAAA,EAAS,KAAK,KAAK,CAAA;AAExD,IAAA,CAAC,YAAY;AACX,MAAA,OAAO,CAAC,OAAA,EAAS;AACf,QAAA,UAAA,GAAa,IAAI,eAAA,EAAgB;AACjC,QAAA,IAAI;AACF,UAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,GAAA,CAAI,UAAS,EAAG;AAAA,YACxC,OAAA,EAAS,IAAA,CAAK,KAAA,GACV,EAAE,aAAA,EAAe,UAAU,IAAA,CAAK,KAAK,CAAA,CAAA,EAAG,GACxC,EAAC;AAAA,YACL,QAAQ,UAAA,CAAW;AAAA,WACpB,CAAA;AACD,UAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,CAAC,GAAA,CAAI,IAAA;AAClB,YAAA,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC/C,UAAA,MAAM,MAAA,GAAS,GAAA,CAAI,IAAA,CAAK,SAAA,EAAU;AAClC,UAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,UAAA,IAAI,GAAA,GAAM,EAAA;AACV,UAAA,WAAS;AACP,YAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAK,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,YAAA,IAAI,IAAA,EAAM;AACV,YAAA,GAAA,IAAO,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAC7C,YAAA,IAAI,GAAA;AACJ,YAAA,OAAA,CAAQ,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,MAAM,MAAM,CAAA,EAAG;AACvC,cAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC9B,cAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AACvB,cAAA,MAAM,IAAA,GAAO,KAAA,CACV,KAAA,CAAM,IAAI,CAAA,CACV,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,UAAA,CAAW,OAAO,CAAC,CAAA,CACnC,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,CACvC,IAAA,CAAK,IAAI,CAAA;AACZ,cAAA,IAAI,IAAA,EAAM;AACR,gBAAA,IAAI;AACF,kBAAA,SAAA,CAAU,IAAA,CAAK,KAAA,CAAM,IAAI,CAAC,CAAA;AAAA,gBAC5B,CAAA,CAAA,MAAQ;AAAA,gBAER;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAA,IAAI,OAAA,EAAS;AACb,QAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,WAAW,CAAC,CAAA;AAAA,MACrD;AAAA,IACF,CAAA,GAAG;AAEH,IAAA,MAAM,QAAqB,MAAM;AAC/B,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,UAAA,EAAY,KAAA,EAAM;AAClB,MAAA,IAAA,CAAK,OAAO,KAAK,CAAA;AAAA,IACnB,CAAA;AACA,IAAA,IAAA,CAAK,IAAI,KAAK,CAAA;AACd,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,SAAS,WAAc,IAAA,EAA+B;AACpD,IAAA,MAAM,OAAqB,EAAC;AAC5B,IAAA,MAAM,GAAA,GAAuB;AAAA,MAC3B,MAAM,CAAA,EAAG;AACP,QAAA,IAAA,CAAK,KAAA,GAAQ,CAAA;AACb,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAAA,MACA,QAAQ,CAAA,EAAG;AACT,QAAA,IAAA,CAAK,OAAA,GAAU,CAAA;AACf,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAAA,MACA,KAAK,CAAA,EAAG;AACN,QAAA,IAAA,CAAK,IAAA,GAAO,CAAA;AACZ,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAAA,MACA,KAAK,CAAA,EAAG;AACN,QAAA,IAAA,CAAK,IAAA,GAAO,CAAA;AACZ,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAAA,MACA,OAAO,CAAA,EAAG;AACR,QAAA,IAAA,CAAK,MAAA,GAAS,CAAA;AACd,QAAA,OAAO,GAAA;AAAA,MACT,CAAA;AAAA,MACA,WAAW,EAAA,EAAI;AACb,QAAA,MAAM,EAAA,GAAK,CAAA,MAAA,EAAS,kBAAA,CAAmB,IAAI,CAAC,CAAA,GAAA,EAAM,kBAAA;AAAA,UAChD,IAAA,CAAK,UAAU,IAAI;AAAA,SACpB,CAAA,CAAA;AACD,QAAA,OAAO,MAAA,CAAO,SAAS,EAAE,CAAA,CAAA,EAAI,CAAC,IAAA,KAAS,EAAA,CAAG,IAAoB,CAAC,CAAA;AAAA,MACjE;AAAA,KACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAEA,EAAA,OAAO;AAAA,IACL,WAAoB,IAAA,EAAc;AAChC,MAAA,OAAO,WAAc,IAAI,CAAA;AAAA,IAC3B,CAAA;AAAA,IACA,GAAA,CACE,IAAA,EACA,EAAA,EACA,EAAA,EACa;AACb,MAAA,MAAM,EAAA,GAAK,SAAS,kBAAA,CAAmB,IAAI,CAAC,CAAA,IAAA,EAAO,kBAAA,CAAmB,EAAE,CAAC,CAAA,CAAA;AACzE,MAAA,OAAO,MAAA;AAAA,QAAO,OAAO,EAAE,CAAA,CAAA;AAAA,QAAI,CAAC,IAAA,KAC1B,EAAA,CAAI,IAAA,EAAM,OAAO,IAAyB;AAAA,OAC5C;AAAA,IACF,CAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,KAAA,MAAW,KAAA,IAAS,CAAC,GAAG,IAAI,GAAG,KAAA,EAAM;AAAA,IACvC;AAAA,GACF;AACF","file":"client.js","sourcesContent":["import type {\n Doc,\n LiveEvent,\n WatchArgs,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\nexport interface RealtimeClientOptions {\n /** Bearer token sent as `Authorization` (and `?token=` for EventSource-style auth). */\n token?: string;\n /** Base path on the server. Default `\"/realtime\"`. */\n path?: string;\n /** Reconnect backoff in ms after a dropped stream. Default `1000`. */\n reconnectMs?: number;\n /** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/** Unsubscribe from a live stream. */\nexport type Unsubscribe = () => void;\n\nexport interface QueryBuilder<T = Doc> {\n where(where: WhereInput<T>): QueryBuilder<T>;\n orderBy(orderBy: WatchArgs<T>[\"orderBy\"]): QueryBuilder<T>;\n take(n: number): QueryBuilder<T>;\n skip(n: number): QueryBuilder<T>;\n /** Only emit when one of these fields changes (server-side filter). */\n fields(fields: (keyof T | (string & {}))[]): QueryBuilder<T>;\n /** Subscribe. The callback fires with each {@link LiveEvent} (init, then changes). */\n onSnapshot(cb: (event: LiveEvent<T>) => void): Unsubscribe;\n}\n\nexport interface RealtimeClient {\n /** Live query over a collection. */\n collection<T = Doc>(name: string): QueryBuilder<T>;\n /** Live single-document listener (`null` while absent / on delete). */\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe;\n /** Close all subscriptions. */\n close(): void;\n}\n\n/**\n * Connect to a `@monlite/realtime` server and subscribe to live queries and\n * documents over SSE. Works in the browser and Node ≥ 18 (uses `fetch`).\n *\n * ```ts\n * const live = connectRealtime(\"http://localhost:8080\", { token });\n * const stop = live.collection(\"orders\").where({ status: \"open\" }).onSnapshot(render);\n * const stopDoc = live.doc(\"orders\", \"o-123\", (doc) => render(doc));\n * ```\n */\nexport function connectRealtime(\n baseUrl: string,\n opts: RealtimeClientOptions = {},\n): RealtimeClient {\n const root = baseUrl.replace(/\\/$/, \"\");\n const base = (opts.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const reconnectMs = opts.reconnectMs ?? 1000;\n const doFetch = opts.fetch ?? globalThis.fetch;\n const subs = new Set<Unsubscribe>();\n\n /** Open an auto-reconnecting SSE stream; returns an unsubscribe. */\n function stream(path: string, onMessage: (data: any) => void): Unsubscribe {\n let stopped = false;\n let controller: AbortController | undefined;\n\n const url = new URL(root + base + path);\n if (opts.token) url.searchParams.set(\"token\", opts.token);\n\n (async () => {\n while (!stopped) {\n controller = new AbortController();\n try {\n const res = await doFetch(url.toString(), {\n headers: opts.token\n ? { authorization: `Bearer ${opts.token}` }\n : {},\n signal: controller.signal,\n });\n if (!res.ok || !res.body)\n throw new Error(`realtime HTTP ${res.status}`);\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let idx: number;\n while ((idx = buf.indexOf(\"\\n\\n\")) >= 0) {\n const frame = buf.slice(0, idx);\n buf = buf.slice(idx + 2);\n const data = frame\n .split(\"\\n\")\n .filter((l) => l.startsWith(\"data:\"))\n .map((l) => l.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (data) {\n try {\n onMessage(JSON.parse(data));\n } catch {\n /* ignore malformed frame */\n }\n }\n }\n }\n } catch {\n /* network/abort — fall through to reconnect */\n }\n if (stopped) break;\n await new Promise((r) => setTimeout(r, reconnectMs));\n }\n })();\n\n const unsub: Unsubscribe = () => {\n stopped = true;\n controller?.abort();\n subs.delete(unsub);\n };\n subs.add(unsub);\n return unsub;\n }\n\n function buildQuery<T>(name: string): QueryBuilder<T> {\n const args: WatchArgs<T> = {};\n const api: QueryBuilder<T> = {\n where(w) {\n args.where = w;\n return api;\n },\n orderBy(o) {\n args.orderBy = o;\n return api;\n },\n take(n) {\n args.take = n;\n return api;\n },\n skip(n) {\n args.skip = n;\n return api;\n },\n fields(f) {\n args.fields = f;\n return api;\n },\n onSnapshot(cb) {\n const qp = `?coll=${encodeURIComponent(name)}&q=${encodeURIComponent(\n JSON.stringify(args),\n )}`;\n return stream(`/query${qp}`, (data) => cb(data as LiveEvent<T>));\n },\n };\n return api;\n }\n\n return {\n collection<T = Doc>(name: string) {\n return buildQuery<T>(name);\n },\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe {\n const qp = `?coll=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`;\n return stream(`/doc${qp}`, (data) =>\n cb((data?.doc ?? null) as WithId<T> | null),\n );\n },\n close() {\n for (const unsub of [...subs]) unsub();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["import type {\n Doc,\n LiveEvent,\n WatchArgs,\n WhereInput,\n WithId,\n} from \"@monlite/core\";\n\nexport interface RealtimeClientOptions {\n /** Bearer token sent as `Authorization` (and `?token=` for EventSource-style auth). */\n token?: string;\n /** Base path on the server. Default `\"/realtime\"`. */\n path?: string;\n /** Reconnect backoff in ms after a dropped stream. Default `1000`. */\n reconnectMs?: number;\n /** Custom `fetch` (e.g. for Node < 18 or a proxy). Defaults to global `fetch`. */\n fetch?: typeof fetch;\n /** Called for a server-sent `{ error }` frame. Defaults to `console.error`. */\n onError?: (error: unknown) => void;\n}\n\n/** Unsubscribe from a live stream. */\nexport type Unsubscribe = () => void;\n\nexport interface QueryBuilder<T = Doc> {\n where(where: WhereInput<T>): QueryBuilder<T>;\n orderBy(orderBy: WatchArgs<T>[\"orderBy\"]): QueryBuilder<T>;\n take(n: number): QueryBuilder<T>;\n skip(n: number): QueryBuilder<T>;\n /** Only emit when one of these fields changes (server-side filter). */\n fields(fields: (keyof T | (string & {}))[]): QueryBuilder<T>;\n /** Subscribe. The callback fires with each {@link LiveEvent} (init, then changes). */\n onSnapshot(cb: (event: LiveEvent<T>) => void): Unsubscribe;\n}\n\nexport interface RealtimeClient {\n /** Live query over a collection. */\n collection<T = Doc>(name: string): QueryBuilder<T>;\n /** Live single-document listener (`null` while absent / on delete). */\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe;\n /** Close all subscriptions. */\n close(): void;\n}\n\n/**\n * Connect to a `@monlite/realtime` server and subscribe to live queries and\n * documents over SSE. Works in the browser and Node ≥ 18 (uses `fetch`).\n *\n * ```ts\n * const live = connectRealtime(\"http://localhost:8080\", { token });\n * const stop = live.collection(\"orders\").where({ status: \"open\" }).onSnapshot(render);\n * const stopDoc = live.doc(\"orders\", \"o-123\", (doc) => render(doc));\n * ```\n */\nexport function connectRealtime(\n baseUrl: string,\n opts: RealtimeClientOptions = {},\n): RealtimeClient {\n const root = baseUrl.replace(/\\/$/, \"\");\n const base = (opts.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const reconnectMs = opts.reconnectMs ?? 1000;\n const doFetch = opts.fetch ?? globalThis.fetch;\n const subs = new Set<Unsubscribe>();\n\n /** Open an auto-reconnecting SSE stream; returns an unsubscribe. */\n function stream(path: string, onMessage: (data: any) => void): Unsubscribe {\n let stopped = false;\n let controller: AbortController | undefined;\n\n const url = new URL(root + base + path);\n if (opts.token) url.searchParams.set(\"token\", opts.token);\n\n (async () => {\n while (!stopped) {\n controller = new AbortController();\n try {\n const res = await doFetch(url.toString(), {\n headers: opts.token\n ? { authorization: `Bearer ${opts.token}` }\n : {},\n signal: controller.signal,\n });\n if (!res.ok || !res.body)\n throw new Error(`realtime HTTP ${res.status}`);\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n // Tolerate LF, CRLF and CR frame/line separators (SSE permits all).\n const FRAME_SEP = /\\r\\n\\r\\n|\\n\\n|\\r\\r/;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let m: RegExpExecArray | null;\n while ((m = FRAME_SEP.exec(buf))) {\n const frame = buf.slice(0, m.index);\n buf = buf.slice(m.index + m[0].length);\n const data = frame\n .split(/\\r\\n|\\n|\\r/)\n .filter((l) => l.startsWith(\"data:\"))\n .map((l) => l.slice(5).replace(/^ /, \"\"))\n .join(\"\\n\");\n if (!data) continue; // comment/heartbeat (\":\" lines) → ignore\n let parsed: any;\n try {\n parsed = JSON.parse(data);\n } catch {\n continue; // malformed frame\n }\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n (\n opts.onError ??\n ((e: unknown) => console.error(\"realtime:\", e))\n )(parsed.error);\n } else {\n onMessage(parsed);\n }\n }\n }\n } catch {\n /* network/abort — fall through to reconnect */\n }\n if (stopped) break;\n await new Promise((r) => setTimeout(r, reconnectMs));\n }\n })();\n\n const unsub: Unsubscribe = () => {\n stopped = true;\n controller?.abort();\n subs.delete(unsub);\n };\n subs.add(unsub);\n return unsub;\n }\n\n function buildQuery<T>(name: string): QueryBuilder<T> {\n const args: WatchArgs<T> = {};\n const api: QueryBuilder<T> = {\n where(w) {\n args.where = w;\n return api;\n },\n orderBy(o) {\n args.orderBy = o;\n return api;\n },\n take(n) {\n args.take = n;\n return api;\n },\n skip(n) {\n args.skip = n;\n return api;\n },\n fields(f) {\n args.fields = f;\n return api;\n },\n onSnapshot(cb) {\n const qp = `?coll=${encodeURIComponent(name)}&q=${encodeURIComponent(\n JSON.stringify(args),\n )}`;\n return stream(`/query${qp}`, (data) => cb(data as LiveEvent<T>));\n },\n };\n return api;\n }\n\n return {\n collection<T = Doc>(name: string) {\n return buildQuery<T>(name);\n },\n doc<T = Doc>(\n name: string,\n id: string,\n cb: (doc: WithId<T> | null) => void,\n ): Unsubscribe {\n const qp = `?coll=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`;\n return stream(`/doc${qp}`, (data) =>\n cb((data?.doc ?? null) as WithId<T> | null),\n );\n },\n close() {\n for (const unsub of [...subs]) unsub();\n },\n };\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -41,20 +41,57 @@ function realtime(options) {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
void (async () => {
|
|
44
|
+
const coll = url.searchParams.get("coll");
|
|
45
|
+
if (!coll) {
|
|
46
|
+
fail(res, 400, "missing coll");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const docId = kind === "doc" ? url.searchParams.get("id") : null;
|
|
50
|
+
if (kind === "doc" && !docId) {
|
|
51
|
+
fail(res, 400, "missing id");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let args = {};
|
|
55
|
+
if (kind === "query") {
|
|
56
|
+
const q = url.searchParams.get("q");
|
|
57
|
+
if (q) {
|
|
58
|
+
try {
|
|
59
|
+
args = JSON.parse(q);
|
|
60
|
+
} catch {
|
|
61
|
+
fail(res, 400, "invalid q");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let handle;
|
|
67
|
+
let heartbeat = void 0;
|
|
68
|
+
let cleaned = false;
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
if (cleaned) return;
|
|
71
|
+
cleaned = true;
|
|
72
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
73
|
+
if (handle) {
|
|
74
|
+
handle.stop();
|
|
75
|
+
open.delete(handle);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
req.on("close", cleanup);
|
|
79
|
+
res.on("close", cleanup);
|
|
44
80
|
let ctx;
|
|
45
81
|
try {
|
|
46
82
|
ctx = await resolve(req);
|
|
47
83
|
} catch (err) {
|
|
84
|
+
cleanup();
|
|
48
85
|
fail(res, 401, err?.message ?? "unauthorized");
|
|
49
86
|
return;
|
|
50
87
|
}
|
|
51
88
|
if (!ctx) {
|
|
89
|
+
cleanup();
|
|
52
90
|
fail(res, 401, "unauthorized");
|
|
53
91
|
return;
|
|
54
92
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fail(res, 400, "missing coll");
|
|
93
|
+
if (cleaned || req.destroyed || res.destroyed) {
|
|
94
|
+
cleanup();
|
|
58
95
|
return;
|
|
59
96
|
}
|
|
60
97
|
setCors(res);
|
|
@@ -67,46 +104,30 @@ function realtime(options) {
|
|
|
67
104
|
res.write(": ok\n\n");
|
|
68
105
|
let id = 0;
|
|
69
106
|
const send = (payload) => {
|
|
70
|
-
if (res.writableEnded) return;
|
|
107
|
+
if (res.writableEnded || res.destroyed) return;
|
|
71
108
|
res.write(`id: ${++id}
|
|
72
109
|
data: ${JSON.stringify(payload)}
|
|
73
110
|
|
|
74
111
|
`);
|
|
75
112
|
};
|
|
76
|
-
let handle;
|
|
77
113
|
try {
|
|
78
114
|
const collection = ctx.db.collection(coll);
|
|
79
|
-
|
|
80
|
-
const docId = url.searchParams.get("id");
|
|
81
|
-
if (!docId) {
|
|
82
|
-
fail(res, 400, "missing id");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
handle = collection.watchDoc(docId, (doc) => send({ doc }));
|
|
86
|
-
} else {
|
|
87
|
-
const q = url.searchParams.get("q");
|
|
88
|
-
const args = q ? JSON.parse(q) : {};
|
|
89
|
-
handle = collection.watch(args, (event) => send(event));
|
|
90
|
-
}
|
|
115
|
+
handle = kind === "doc" ? collection.watchDoc(docId, (doc) => send({ doc })) : collection.watch(args, (event) => send(event));
|
|
91
116
|
} catch (err) {
|
|
92
117
|
send({ error: err?.message ?? "watch failed" });
|
|
93
118
|
res.end();
|
|
119
|
+
cleanup();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (cleaned) {
|
|
123
|
+
handle.stop();
|
|
94
124
|
return;
|
|
95
125
|
}
|
|
96
126
|
open.add(handle);
|
|
97
|
-
|
|
98
|
-
if (!res.writableEnded) res.write(": ping\n\n");
|
|
127
|
+
heartbeat = setInterval(() => {
|
|
128
|
+
if (!res.writableEnded && !res.destroyed) res.write(": ping\n\n");
|
|
99
129
|
}, heartbeatMs);
|
|
100
130
|
heartbeat.unref?.();
|
|
101
|
-
const cleanup = () => {
|
|
102
|
-
clearInterval(heartbeat);
|
|
103
|
-
if (handle) {
|
|
104
|
-
handle.stop();
|
|
105
|
-
open.delete(handle);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
req.on("close", cleanup);
|
|
109
|
-
res.on("close", cleanup);
|
|
110
131
|
})();
|
|
111
132
|
};
|
|
112
133
|
return {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["createServer"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport type { Monlite, WatchArgs, WatchHandle } from \"@monlite/core\";\n\n/** What an authorized request resolves to: the database (tenant) to serve. */\nexport interface RealtimeContext {\n db: Monlite;\n}\n\nexport interface RealtimeOptions {\n /**\n * Resolve the database (tenant) for a request and authorize it. Return a\n * context (`{ db }`), or `null`/throw to reject (`401`). Read a token from\n * `req.headers.authorization` / the `?token=` query param. For per-tenant\n * deployments, map the token → that tenant's `Monlite` instance.\n */\n authorize?: (\n req: IncomingMessage,\n ) => RealtimeContext | null | Promise<RealtimeContext | null>;\n /** Single-database shortcut — serve this db for every request (no auth). */\n db?: Monlite;\n /** Base path for the endpoints. Default `\"/realtime\"`. */\n path?: string;\n /** `Access-Control-Allow-Origin` value, or `false` to disable CORS. Default `\"*\"`. */\n cors?: string | false;\n /** Heartbeat comment interval (ms) to keep idle connections alive. Default `25000`. */\n heartbeatMs?: number;\n}\n\nexport interface RealtimeServer {\n /** Node `http` request handler — attach to your own server or framework. */\n handler: (req: IncomingMessage, res: ServerResponse) => void;\n /** Convenience: start a standalone HTTP server. */\n listen: (port: number, cb?: () => void) => Server;\n /** Active subscription count. */\n readonly subscriptions: number;\n /** Stop all subscriptions (does not close the http server). */\n close: () => void;\n}\n\n/**\n * A realtime gateway that streams live queries and documents to remote clients\n * over Server-Sent Events, backed by `@monlite/core`'s `watch()` / change feed.\n * Zero extra dependencies — built on `node:http`.\n *\n * ```ts\n * realtime({ authorize: (req) => ({ db: dbForTenant(tokenOf(req)) }) }).listen(8080);\n * ```\n */\nexport function realtime(options: RealtimeOptions): RealtimeServer {\n const base = (options.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const cors = options.cors === undefined ? \"*\" : options.cors;\n const heartbeatMs = options.heartbeatMs ?? 25_000;\n const open = new Set<WatchHandle<any>>();\n\n const resolve = async (\n req: IncomingMessage,\n ): Promise<RealtimeContext | null> => {\n if (options.authorize) return options.authorize(req);\n if (options.db) return { db: options.db };\n return null;\n };\n\n const setCors = (res: ServerResponse): void => {\n if (cors === false) return;\n res.setHeader(\"Access-Control-Allow-Origin\", cors);\n res.setHeader(\"Access-Control-Allow-Headers\", \"authorization,content-type\");\n };\n\n const fail = (res: ServerResponse, code: number, msg: string): void => {\n setCors(res);\n res.writeHead(code, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: msg }));\n };\n\n const handler = (req: IncomingMessage, res: ServerResponse): void => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (cors !== false && req.method === \"OPTIONS\") {\n setCors(res);\n res.writeHead(204);\n res.end();\n return;\n }\n if (!url.pathname.startsWith(base + \"/\")) {\n fail(res, 404, \"not found\");\n return;\n }\n const kind = url.pathname.slice(base.length + 1); // \"query\" | \"doc\"\n if (kind !== \"query\" && kind !== \"doc\") {\n fail(res, 404, \"not found\");\n return;\n }\n\n void (async () => {\n let ctx: RealtimeContext | null;\n try {\n ctx = await resolve(req);\n } catch (err) {\n fail(res, 401, (err as Error)?.message ?? \"unauthorized\");\n return;\n }\n if (!ctx) {\n fail(res, 401, \"unauthorized\");\n return;\n }\n\n const coll = url.searchParams.get(\"coll\");\n if (!coll) {\n fail(res, 400, \"missing coll\");\n return;\n }\n\n // Open the SSE stream.\n setCors(res);\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache, no-transform\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n });\n res.write(\": ok\\n\\n\");\n let id = 0;\n const send = (payload: unknown): void => {\n if (res.writableEnded) return;\n res.write(`id: ${++id}\\ndata: ${JSON.stringify(payload)}\\n\\n`);\n };\n\n let handle: WatchHandle<any> | undefined;\n try {\n const collection = ctx.db.collection(coll);\n if (kind === \"doc\") {\n const docId = url.searchParams.get(\"id\");\n if (!docId) {\n fail(res, 400, \"missing id\");\n return;\n }\n handle = collection.watchDoc(docId, (doc) => send({ doc }));\n } else {\n const q = url.searchParams.get(\"q\");\n const args: WatchArgs<any> = q ? JSON.parse(q) : {};\n handle = collection.watch(args, (event) => send(event));\n }\n } catch (err) {\n // Headers already sent; surface the error in-band, then close.\n send({ error: (err as Error)?.message ?? \"watch failed\" });\n res.end();\n return;\n }\n open.add(handle);\n\n const heartbeat = setInterval(() => {\n if (!res.writableEnded) res.write(\": ping\\n\\n\");\n }, heartbeatMs);\n (heartbeat as any).unref?.();\n\n const cleanup = (): void => {\n clearInterval(heartbeat);\n if (handle) {\n handle.stop();\n open.delete(handle);\n }\n };\n req.on(\"close\", cleanup);\n res.on(\"close\", cleanup);\n })();\n };\n\n return {\n handler,\n listen(port, cb) {\n const server = createServer(handler);\n server.listen(port, cb);\n return server;\n },\n get subscriptions() {\n return open.size;\n },\n close() {\n for (const h of open) h.stop();\n open.clear();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["createServer"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport type { Monlite, WatchArgs, WatchHandle } from \"@monlite/core\";\n\n/** What an authorized request resolves to: the database (tenant) to serve. */\nexport interface RealtimeContext {\n db: Monlite;\n}\n\nexport interface RealtimeOptions {\n /**\n * Resolve the database (tenant) for a request and authorize it. Return a\n * context (`{ db }`), or `null`/throw to reject (`401`). Read a token from\n * `req.headers.authorization` / the `?token=` query param. For per-tenant\n * deployments, map the token → that tenant's `Monlite` instance.\n */\n authorize?: (\n req: IncomingMessage,\n ) => RealtimeContext | null | Promise<RealtimeContext | null>;\n /** Single-database shortcut — serve this db for every request (no auth). */\n db?: Monlite;\n /** Base path for the endpoints. Default `\"/realtime\"`. */\n path?: string;\n /** `Access-Control-Allow-Origin` value, or `false` to disable CORS. Default `\"*\"`. */\n cors?: string | false;\n /** Heartbeat comment interval (ms) to keep idle connections alive. Default `25000`. */\n heartbeatMs?: number;\n}\n\nexport interface RealtimeServer {\n /** Node `http` request handler — attach to your own server or framework. */\n handler: (req: IncomingMessage, res: ServerResponse) => void;\n /** Convenience: start a standalone HTTP server. */\n listen: (port: number, cb?: () => void) => Server;\n /** Active subscription count. */\n readonly subscriptions: number;\n /** Stop all subscriptions (does not close the http server). */\n close: () => void;\n}\n\n/**\n * A realtime gateway that streams live queries and documents to remote clients\n * over Server-Sent Events, backed by `@monlite/core`'s `watch()` / change feed.\n * Zero extra dependencies — built on `node:http`.\n *\n * ```ts\n * realtime({ authorize: (req) => ({ db: dbForTenant(tokenOf(req)) }) }).listen(8080);\n * ```\n */\nexport function realtime(options: RealtimeOptions): RealtimeServer {\n const base = (options.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const cors = options.cors === undefined ? \"*\" : options.cors;\n const heartbeatMs = options.heartbeatMs ?? 25_000;\n const open = new Set<WatchHandle<any>>();\n\n const resolve = async (\n req: IncomingMessage,\n ): Promise<RealtimeContext | null> => {\n if (options.authorize) return options.authorize(req);\n if (options.db) return { db: options.db };\n return null;\n };\n\n const setCors = (res: ServerResponse): void => {\n if (cors === false) return;\n res.setHeader(\"Access-Control-Allow-Origin\", cors);\n res.setHeader(\"Access-Control-Allow-Headers\", \"authorization,content-type\");\n };\n\n const fail = (res: ServerResponse, code: number, msg: string): void => {\n setCors(res);\n res.writeHead(code, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: msg }));\n };\n\n const handler = (req: IncomingMessage, res: ServerResponse): void => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (cors !== false && req.method === \"OPTIONS\") {\n setCors(res);\n res.writeHead(204);\n res.end();\n return;\n }\n if (!url.pathname.startsWith(base + \"/\")) {\n fail(res, 404, \"not found\");\n return;\n }\n const kind = url.pathname.slice(base.length + 1); // \"query\" | \"doc\"\n if (kind !== \"query\" && kind !== \"doc\") {\n fail(res, 404, \"not found\");\n return;\n }\n\n void (async () => {\n // Validate params BEFORE auth/stream — a bad request is then a clean 400,\n // not an in-band error written onto an already-opened SSE stream.\n const coll = url.searchParams.get(\"coll\");\n if (!coll) {\n fail(res, 400, \"missing coll\");\n return;\n }\n const docId = kind === \"doc\" ? url.searchParams.get(\"id\") : null;\n if (kind === \"doc\" && !docId) {\n fail(res, 400, \"missing id\");\n return;\n }\n let args: WatchArgs<any> = {};\n if (kind === \"query\") {\n const q = url.searchParams.get(\"q\");\n if (q) {\n try {\n args = JSON.parse(q);\n } catch {\n fail(res, 400, \"invalid q\");\n return;\n }\n }\n }\n\n // Attach an idempotent cleanup BEFORE the (possibly async) authorize, so a\n // client that disconnects during auth still tears the subscription down —\n // otherwise the watch handle + heartbeat leak forever (unauthenticated DoS).\n let handle: WatchHandle<any> | undefined;\n // `cleanup` (attached below, before `await`) may read this while it's still\n // unset, so it must be a hoisted let seeded to undefined — not a const.\n let heartbeat: ReturnType<typeof setInterval> | undefined = undefined;\n let cleaned = false;\n const cleanup = (): void => {\n if (cleaned) return;\n cleaned = true;\n if (heartbeat) clearInterval(heartbeat);\n if (handle) {\n handle.stop();\n open.delete(handle);\n }\n };\n req.on(\"close\", cleanup);\n res.on(\"close\", cleanup);\n\n let ctx: RealtimeContext | null;\n try {\n ctx = await resolve(req);\n } catch (err) {\n cleanup();\n fail(res, 401, (err as Error)?.message ?? \"unauthorized\");\n return;\n }\n if (!ctx) {\n cleanup();\n fail(res, 401, \"unauthorized\");\n return;\n }\n // Client may have disconnected during the await — bail before opening.\n if (cleaned || req.destroyed || res.destroyed) {\n cleanup();\n return;\n }\n\n // Open the SSE stream.\n setCors(res);\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache, no-transform\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n });\n res.write(\": ok\\n\\n\");\n let id = 0;\n const send = (payload: unknown): void => {\n if (res.writableEnded || res.destroyed) return;\n res.write(`id: ${++id}\\ndata: ${JSON.stringify(payload)}\\n\\n`);\n };\n\n try {\n const collection = ctx.db.collection(coll);\n handle =\n kind === \"doc\"\n ? collection.watchDoc(docId as string, (doc) => send({ doc }))\n : collection.watch(args, (event) => send(event));\n } catch (err) {\n send({ error: (err as Error)?.message ?? \"watch failed\" });\n res.end();\n cleanup();\n return;\n }\n if (cleaned) {\n // Disconnected during setup — don't leave the handle registered.\n handle.stop();\n return;\n }\n open.add(handle);\n heartbeat = setInterval(() => {\n if (!res.writableEnded && !res.destroyed) res.write(\": ping\\n\\n\");\n }, heartbeatMs);\n (heartbeat as any).unref?.();\n })();\n };\n\n return {\n handler,\n listen(port, cb) {\n const server = createServer(handler);\n server.listen(port, cb);\n return server;\n },\n get subscriptions() {\n return open.size;\n },\n close() {\n for (const h of open) h.stop();\n open.clear();\n },\n };\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -39,20 +39,57 @@ function realtime(options) {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
void (async () => {
|
|
42
|
+
const coll = url.searchParams.get("coll");
|
|
43
|
+
if (!coll) {
|
|
44
|
+
fail(res, 400, "missing coll");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const docId = kind === "doc" ? url.searchParams.get("id") : null;
|
|
48
|
+
if (kind === "doc" && !docId) {
|
|
49
|
+
fail(res, 400, "missing id");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let args = {};
|
|
53
|
+
if (kind === "query") {
|
|
54
|
+
const q = url.searchParams.get("q");
|
|
55
|
+
if (q) {
|
|
56
|
+
try {
|
|
57
|
+
args = JSON.parse(q);
|
|
58
|
+
} catch {
|
|
59
|
+
fail(res, 400, "invalid q");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let handle;
|
|
65
|
+
let heartbeat = void 0;
|
|
66
|
+
let cleaned = false;
|
|
67
|
+
const cleanup = () => {
|
|
68
|
+
if (cleaned) return;
|
|
69
|
+
cleaned = true;
|
|
70
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
71
|
+
if (handle) {
|
|
72
|
+
handle.stop();
|
|
73
|
+
open.delete(handle);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
req.on("close", cleanup);
|
|
77
|
+
res.on("close", cleanup);
|
|
42
78
|
let ctx;
|
|
43
79
|
try {
|
|
44
80
|
ctx = await resolve(req);
|
|
45
81
|
} catch (err) {
|
|
82
|
+
cleanup();
|
|
46
83
|
fail(res, 401, err?.message ?? "unauthorized");
|
|
47
84
|
return;
|
|
48
85
|
}
|
|
49
86
|
if (!ctx) {
|
|
87
|
+
cleanup();
|
|
50
88
|
fail(res, 401, "unauthorized");
|
|
51
89
|
return;
|
|
52
90
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
fail(res, 400, "missing coll");
|
|
91
|
+
if (cleaned || req.destroyed || res.destroyed) {
|
|
92
|
+
cleanup();
|
|
56
93
|
return;
|
|
57
94
|
}
|
|
58
95
|
setCors(res);
|
|
@@ -65,46 +102,30 @@ function realtime(options) {
|
|
|
65
102
|
res.write(": ok\n\n");
|
|
66
103
|
let id = 0;
|
|
67
104
|
const send = (payload) => {
|
|
68
|
-
if (res.writableEnded) return;
|
|
105
|
+
if (res.writableEnded || res.destroyed) return;
|
|
69
106
|
res.write(`id: ${++id}
|
|
70
107
|
data: ${JSON.stringify(payload)}
|
|
71
108
|
|
|
72
109
|
`);
|
|
73
110
|
};
|
|
74
|
-
let handle;
|
|
75
111
|
try {
|
|
76
112
|
const collection = ctx.db.collection(coll);
|
|
77
|
-
|
|
78
|
-
const docId = url.searchParams.get("id");
|
|
79
|
-
if (!docId) {
|
|
80
|
-
fail(res, 400, "missing id");
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
handle = collection.watchDoc(docId, (doc) => send({ doc }));
|
|
84
|
-
} else {
|
|
85
|
-
const q = url.searchParams.get("q");
|
|
86
|
-
const args = q ? JSON.parse(q) : {};
|
|
87
|
-
handle = collection.watch(args, (event) => send(event));
|
|
88
|
-
}
|
|
113
|
+
handle = kind === "doc" ? collection.watchDoc(docId, (doc) => send({ doc })) : collection.watch(args, (event) => send(event));
|
|
89
114
|
} catch (err) {
|
|
90
115
|
send({ error: err?.message ?? "watch failed" });
|
|
91
116
|
res.end();
|
|
117
|
+
cleanup();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (cleaned) {
|
|
121
|
+
handle.stop();
|
|
92
122
|
return;
|
|
93
123
|
}
|
|
94
124
|
open.add(handle);
|
|
95
|
-
|
|
96
|
-
if (!res.writableEnded) res.write(": ping\n\n");
|
|
125
|
+
heartbeat = setInterval(() => {
|
|
126
|
+
if (!res.writableEnded && !res.destroyed) res.write(": ping\n\n");
|
|
97
127
|
}, heartbeatMs);
|
|
98
128
|
heartbeat.unref?.();
|
|
99
|
-
const cleanup = () => {
|
|
100
|
-
clearInterval(heartbeat);
|
|
101
|
-
if (handle) {
|
|
102
|
-
handle.stop();
|
|
103
|
-
open.delete(handle);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
req.on("close", cleanup);
|
|
107
|
-
res.on("close", cleanup);
|
|
108
129
|
})();
|
|
109
130
|
};
|
|
110
131
|
return {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport type { Monlite, WatchArgs, WatchHandle } from \"@monlite/core\";\n\n/** What an authorized request resolves to: the database (tenant) to serve. */\nexport interface RealtimeContext {\n db: Monlite;\n}\n\nexport interface RealtimeOptions {\n /**\n * Resolve the database (tenant) for a request and authorize it. Return a\n * context (`{ db }`), or `null`/throw to reject (`401`). Read a token from\n * `req.headers.authorization` / the `?token=` query param. For per-tenant\n * deployments, map the token → that tenant's `Monlite` instance.\n */\n authorize?: (\n req: IncomingMessage,\n ) => RealtimeContext | null | Promise<RealtimeContext | null>;\n /** Single-database shortcut — serve this db for every request (no auth). */\n db?: Monlite;\n /** Base path for the endpoints. Default `\"/realtime\"`. */\n path?: string;\n /** `Access-Control-Allow-Origin` value, or `false` to disable CORS. Default `\"*\"`. */\n cors?: string | false;\n /** Heartbeat comment interval (ms) to keep idle connections alive. Default `25000`. */\n heartbeatMs?: number;\n}\n\nexport interface RealtimeServer {\n /** Node `http` request handler — attach to your own server or framework. */\n handler: (req: IncomingMessage, res: ServerResponse) => void;\n /** Convenience: start a standalone HTTP server. */\n listen: (port: number, cb?: () => void) => Server;\n /** Active subscription count. */\n readonly subscriptions: number;\n /** Stop all subscriptions (does not close the http server). */\n close: () => void;\n}\n\n/**\n * A realtime gateway that streams live queries and documents to remote clients\n * over Server-Sent Events, backed by `@monlite/core`'s `watch()` / change feed.\n * Zero extra dependencies — built on `node:http`.\n *\n * ```ts\n * realtime({ authorize: (req) => ({ db: dbForTenant(tokenOf(req)) }) }).listen(8080);\n * ```\n */\nexport function realtime(options: RealtimeOptions): RealtimeServer {\n const base = (options.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const cors = options.cors === undefined ? \"*\" : options.cors;\n const heartbeatMs = options.heartbeatMs ?? 25_000;\n const open = new Set<WatchHandle<any>>();\n\n const resolve = async (\n req: IncomingMessage,\n ): Promise<RealtimeContext | null> => {\n if (options.authorize) return options.authorize(req);\n if (options.db) return { db: options.db };\n return null;\n };\n\n const setCors = (res: ServerResponse): void => {\n if (cors === false) return;\n res.setHeader(\"Access-Control-Allow-Origin\", cors);\n res.setHeader(\"Access-Control-Allow-Headers\", \"authorization,content-type\");\n };\n\n const fail = (res: ServerResponse, code: number, msg: string): void => {\n setCors(res);\n res.writeHead(code, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: msg }));\n };\n\n const handler = (req: IncomingMessage, res: ServerResponse): void => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (cors !== false && req.method === \"OPTIONS\") {\n setCors(res);\n res.writeHead(204);\n res.end();\n return;\n }\n if (!url.pathname.startsWith(base + \"/\")) {\n fail(res, 404, \"not found\");\n return;\n }\n const kind = url.pathname.slice(base.length + 1); // \"query\" | \"doc\"\n if (kind !== \"query\" && kind !== \"doc\") {\n fail(res, 404, \"not found\");\n return;\n }\n\n void (async () => {\n let ctx: RealtimeContext | null;\n try {\n ctx = await resolve(req);\n } catch (err) {\n fail(res, 401, (err as Error)?.message ?? \"unauthorized\");\n return;\n }\n if (!ctx) {\n fail(res, 401, \"unauthorized\");\n return;\n }\n\n const coll = url.searchParams.get(\"coll\");\n if (!coll) {\n fail(res, 400, \"missing coll\");\n return;\n }\n\n // Open the SSE stream.\n setCors(res);\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache, no-transform\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n });\n res.write(\": ok\\n\\n\");\n let id = 0;\n const send = (payload: unknown): void => {\n if (res.writableEnded) return;\n res.write(`id: ${++id}\\ndata: ${JSON.stringify(payload)}\\n\\n`);\n };\n\n let handle: WatchHandle<any> | undefined;\n try {\n const collection = ctx.db.collection(coll);\n if (kind === \"doc\") {\n const docId = url.searchParams.get(\"id\");\n if (!docId) {\n fail(res, 400, \"missing id\");\n return;\n }\n handle = collection.watchDoc(docId, (doc) => send({ doc }));\n } else {\n const q = url.searchParams.get(\"q\");\n const args: WatchArgs<any> = q ? JSON.parse(q) : {};\n handle = collection.watch(args, (event) => send(event));\n }\n } catch (err) {\n // Headers already sent; surface the error in-band, then close.\n send({ error: (err as Error)?.message ?? \"watch failed\" });\n res.end();\n return;\n }\n open.add(handle);\n\n const heartbeat = setInterval(() => {\n if (!res.writableEnded) res.write(\": ping\\n\\n\");\n }, heartbeatMs);\n (heartbeat as any).unref?.();\n\n const cleanup = (): void => {\n clearInterval(heartbeat);\n if (handle) {\n handle.stop();\n open.delete(handle);\n }\n };\n req.on(\"close\", cleanup);\n res.on(\"close\", cleanup);\n })();\n };\n\n return {\n handler,\n listen(port, cb) {\n const server = createServer(handler);\n server.listen(port, cb);\n return server;\n },\n get subscriptions() {\n return open.size;\n },\n close() {\n for (const h of open) h.stop();\n open.clear();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport type { Monlite, WatchArgs, WatchHandle } from \"@monlite/core\";\n\n/** What an authorized request resolves to: the database (tenant) to serve. */\nexport interface RealtimeContext {\n db: Monlite;\n}\n\nexport interface RealtimeOptions {\n /**\n * Resolve the database (tenant) for a request and authorize it. Return a\n * context (`{ db }`), or `null`/throw to reject (`401`). Read a token from\n * `req.headers.authorization` / the `?token=` query param. For per-tenant\n * deployments, map the token → that tenant's `Monlite` instance.\n */\n authorize?: (\n req: IncomingMessage,\n ) => RealtimeContext | null | Promise<RealtimeContext | null>;\n /** Single-database shortcut — serve this db for every request (no auth). */\n db?: Monlite;\n /** Base path for the endpoints. Default `\"/realtime\"`. */\n path?: string;\n /** `Access-Control-Allow-Origin` value, or `false` to disable CORS. Default `\"*\"`. */\n cors?: string | false;\n /** Heartbeat comment interval (ms) to keep idle connections alive. Default `25000`. */\n heartbeatMs?: number;\n}\n\nexport interface RealtimeServer {\n /** Node `http` request handler — attach to your own server or framework. */\n handler: (req: IncomingMessage, res: ServerResponse) => void;\n /** Convenience: start a standalone HTTP server. */\n listen: (port: number, cb?: () => void) => Server;\n /** Active subscription count. */\n readonly subscriptions: number;\n /** Stop all subscriptions (does not close the http server). */\n close: () => void;\n}\n\n/**\n * A realtime gateway that streams live queries and documents to remote clients\n * over Server-Sent Events, backed by `@monlite/core`'s `watch()` / change feed.\n * Zero extra dependencies — built on `node:http`.\n *\n * ```ts\n * realtime({ authorize: (req) => ({ db: dbForTenant(tokenOf(req)) }) }).listen(8080);\n * ```\n */\nexport function realtime(options: RealtimeOptions): RealtimeServer {\n const base = (options.path ?? \"/realtime\").replace(/\\/$/, \"\");\n const cors = options.cors === undefined ? \"*\" : options.cors;\n const heartbeatMs = options.heartbeatMs ?? 25_000;\n const open = new Set<WatchHandle<any>>();\n\n const resolve = async (\n req: IncomingMessage,\n ): Promise<RealtimeContext | null> => {\n if (options.authorize) return options.authorize(req);\n if (options.db) return { db: options.db };\n return null;\n };\n\n const setCors = (res: ServerResponse): void => {\n if (cors === false) return;\n res.setHeader(\"Access-Control-Allow-Origin\", cors);\n res.setHeader(\"Access-Control-Allow-Headers\", \"authorization,content-type\");\n };\n\n const fail = (res: ServerResponse, code: number, msg: string): void => {\n setCors(res);\n res.writeHead(code, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ error: msg }));\n };\n\n const handler = (req: IncomingMessage, res: ServerResponse): void => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (cors !== false && req.method === \"OPTIONS\") {\n setCors(res);\n res.writeHead(204);\n res.end();\n return;\n }\n if (!url.pathname.startsWith(base + \"/\")) {\n fail(res, 404, \"not found\");\n return;\n }\n const kind = url.pathname.slice(base.length + 1); // \"query\" | \"doc\"\n if (kind !== \"query\" && kind !== \"doc\") {\n fail(res, 404, \"not found\");\n return;\n }\n\n void (async () => {\n // Validate params BEFORE auth/stream — a bad request is then a clean 400,\n // not an in-band error written onto an already-opened SSE stream.\n const coll = url.searchParams.get(\"coll\");\n if (!coll) {\n fail(res, 400, \"missing coll\");\n return;\n }\n const docId = kind === \"doc\" ? url.searchParams.get(\"id\") : null;\n if (kind === \"doc\" && !docId) {\n fail(res, 400, \"missing id\");\n return;\n }\n let args: WatchArgs<any> = {};\n if (kind === \"query\") {\n const q = url.searchParams.get(\"q\");\n if (q) {\n try {\n args = JSON.parse(q);\n } catch {\n fail(res, 400, \"invalid q\");\n return;\n }\n }\n }\n\n // Attach an idempotent cleanup BEFORE the (possibly async) authorize, so a\n // client that disconnects during auth still tears the subscription down —\n // otherwise the watch handle + heartbeat leak forever (unauthenticated DoS).\n let handle: WatchHandle<any> | undefined;\n // `cleanup` (attached below, before `await`) may read this while it's still\n // unset, so it must be a hoisted let seeded to undefined — not a const.\n let heartbeat: ReturnType<typeof setInterval> | undefined = undefined;\n let cleaned = false;\n const cleanup = (): void => {\n if (cleaned) return;\n cleaned = true;\n if (heartbeat) clearInterval(heartbeat);\n if (handle) {\n handle.stop();\n open.delete(handle);\n }\n };\n req.on(\"close\", cleanup);\n res.on(\"close\", cleanup);\n\n let ctx: RealtimeContext | null;\n try {\n ctx = await resolve(req);\n } catch (err) {\n cleanup();\n fail(res, 401, (err as Error)?.message ?? \"unauthorized\");\n return;\n }\n if (!ctx) {\n cleanup();\n fail(res, 401, \"unauthorized\");\n return;\n }\n // Client may have disconnected during the await — bail before opening.\n if (cleaned || req.destroyed || res.destroyed) {\n cleanup();\n return;\n }\n\n // Open the SSE stream.\n setCors(res);\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache, no-transform\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n });\n res.write(\": ok\\n\\n\");\n let id = 0;\n const send = (payload: unknown): void => {\n if (res.writableEnded || res.destroyed) return;\n res.write(`id: ${++id}\\ndata: ${JSON.stringify(payload)}\\n\\n`);\n };\n\n try {\n const collection = ctx.db.collection(coll);\n handle =\n kind === \"doc\"\n ? collection.watchDoc(docId as string, (doc) => send({ doc }))\n : collection.watch(args, (event) => send(event));\n } catch (err) {\n send({ error: (err as Error)?.message ?? \"watch failed\" });\n res.end();\n cleanup();\n return;\n }\n if (cleaned) {\n // Disconnected during setup — don't leave the handle registered.\n handle.stop();\n return;\n }\n open.add(handle);\n heartbeat = setInterval(() => {\n if (!res.writableEnded && !res.destroyed) res.write(\": ping\\n\\n\");\n }, heartbeatMs);\n (heartbeat as any).unref?.();\n })();\n };\n\n return {\n handler,\n listen(port, cb) {\n const server = createServer(handler);\n server.listen(port, cb);\n return server;\n },\n get subscriptions() {\n return open.size;\n },\n close() {\n for (const h of open) h.stop();\n open.clear();\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monlite/realtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Networked realtime for @monlite/core: stream live queries and documents to remote clients over SSE, backed by the change feed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"node": ">=18"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@monlite/core": "^2.
|
|
62
|
+
"@monlite/core": "^2.8.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@types/node": "^22.10.0",
|