@kavo/sse 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @kavo/sse
2
+
3
+ The first `RealtimeTransport` implementation (`@kavo/core`, ADR-0023):
4
+ plain HTTP `text/event-stream`, no client or server library required —
5
+ SSE is one-directional, so there is no socket to open and nothing to
6
+ depend on beyond Node's own `http` types.
7
+
8
+ **May depend on:** `@kavo/core` only, no peer. **Never on:** `@kavo/nest`
9
+ or any other framework — same rule `packages/orms/*` follows.
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createTransport } from "@kavo/sse";
15
+ import { createKavo } from "@kavo/core";
16
+
17
+ // Filled in below, after `createCrud` — the callback only runs once a
18
+ // subscribe request actually arrives, so the forward reference is fine.
19
+ let bookService: ReturnType<typeof kavo.createCrud<Book>>;
20
+
21
+ const sse = createTransport({
22
+ subscribableFields: (entityName) => (entityName === "Book" ? ["title", "status", "price"] : undefined),
23
+ // Enables subscribe-time filtering (issue #160) for an entity — omit an
24
+ // entry and a `filter[...]` query param on that entity is rejected with
25
+ // 400 rather than silently ignored. Typically `service.engine.metadata`/
26
+ // `service.engine.config` off the `createCrud` service already returned.
27
+ filterableEntities: (entityName) =>
28
+ entityName === "Book" ? { metadata: bookService.engine.metadata, config: bookService.engine.config } : undefined,
29
+ });
30
+
31
+ const kavo = createKavo({
32
+ infrastructure,
33
+ realtimeTransports: [sse],
34
+ defaults: {
35
+ realtime: { enabled: true, events: { created: true, updated: true, patched: true, deleted: true } },
36
+ },
37
+ });
38
+
39
+ bookService = kavo.createCrud(Book);
40
+ ```
41
+
42
+ `@kavo/sse` has **no authentication of its own** — `handleRequest` accepts
43
+ any subscribe request that otherwise validates. A deployment that needs to
44
+ gate who may open a stream does so in front of it: a reverse proxy, or the
45
+ host framework's own guard/middleware on the mounted route, since
46
+ `handleRequest` is just an ordinary `(req, res)` handler.
47
+
48
+ Mount `sse.handleRequest` on a plain Node HTTP route (or any host
49
+ framework's request/response — Express, Nest, Fastify's raw
50
+ req/res, … — since `IncomingMessage`/`ServerResponse` is what all of
51
+ them extend or wrap):
52
+
53
+ ```ts
54
+ http.createServer((req, res) => {
55
+ if (req.url?.startsWith("/realtime")) {
56
+ void sse.handleRequest(req, res);
57
+ return;
58
+ }
59
+ // ... the rest of the app's routing
60
+ });
61
+ ```
62
+
63
+ A client subscribes to one entity/id, or to the whole entity, with
64
+ `EventSource` (or any HTTP client that reads a chunked `text/event-stream`
65
+ body):
66
+
67
+ ```js
68
+ // Item channel: every event for Book id 42.
69
+ const one = new EventSource("/realtime?channel=Book.42");
70
+
71
+ // Collection channel: every event for every Book (issue #160).
72
+ const all = new EventSource("/realtime?channel=Book");
73
+
74
+ // Collection channel, scoped with the same filter grammar REST uses.
75
+ const published = new EventSource("/realtime?channel=Book&filter[status][eq]=published");
76
+
77
+ all.addEventListener("updated", (message) => {
78
+ const event = JSON.parse(message.data); // RealtimeEventDto
79
+ });
80
+ ```
81
+
82
+ `channel` is required: `<entity>.<id>` for an item-level subscription, or
83
+ the bare `<entity>` for a collection-level one (every event for that
84
+ entity). A collection-channel subscribe request may add a `filter` query
85
+ string in the exact `filter[field][operator]=value` grammar REST list
86
+ requests use (doc 18) — evaluated in memory per event, no DB round trip.
87
+ Filtering is opt-in per entity via `filterableEntities`; an entity with no
88
+ entry there rejects any `filter[...]` param with `400` rather than
89
+ silently ignoring it. See doc 18 §4.3 for what a filtered subscriber
90
+ receives when a write moves a row across the filter boundary, and for the
91
+ unconditional `"deleted"`-event bypass.
92
+
93
+ Once `subscribableFields` is configured for an entity, it bounds every
94
+ outgoing `item` **unconditionally** — not only when a subscriber names
95
+ `fields` — the same way `allowlists.selectable`
96
+ bounds a REST response whether or not the caller asked for a subset. An
97
+ optional `fields` query param (comma-separated) narrows further within
98
+ that bound; a field outside it (or outside `subscribableFields`, when no
99
+ `fields` param is given) gets a `400` the same way `allowlists.selectable`
100
+ rejects an unlisted field over REST.
101
+
102
+ A connection that cannot keep up with its publish rate (the writable
103
+ buffer on its response exceeds `bufferLimitBytes`, default 64 KiB) is
104
+ closed rather than left to block delivery to every other subscriber.
105
+
106
+ ## Known limitations
107
+
108
+ - **No resume-on-reconnect.** Every SSE frame carries an `id:`, but
109
+ nothing reads `Last-Event-ID` yet — a dropped connection means missed
110
+ events, not replayed ones. This matters more for SSE than it will for
111
+ `@kavo/websocket`: browsers' native `EventSource` **auto-reconnects by
112
+ default** on a dropped connection, with no application code asking for
113
+ it, so a client silently starts receiving only new events after a gap it
114
+ never signaled. Building resume via the `since` cursor is a future issue.
115
+ - **No multi-node fan-out.** The channel registry is one process's
116
+ in-memory `Map` — a subscriber connected to one instance of a
117
+ horizontally-scaled app never sees a write handled by another instance.
118
+ - **No "leave" event on an ordinary write.** A write that makes a row stop
119
+ matching a filtered subscriber's filter is not delivered at all — only a
120
+ genuine `"deleted"` reliably tells that subscriber a row is gone. See
121
+ doc 18 §4.3.
122
+ - **No filtering by which fields changed** — a subscribe-time filter
123
+ matches row data (`RealtimeEventDto.item`), not the write's diff
124
+ (`RealtimeEventDto.changed`).
125
+ - **No authentication or authorization.** `@kavo/sse` accepts any subscribe
126
+ request that otherwise validates — gating who may open a stream is the
127
+ host app's job (a reverse proxy, or a guard on the mounted route), and
128
+ row/tenant-level subscriber scoping is a future issue (`authorize`, out
129
+ of scope here — see `RealtimeTransport`'s own doc). A `filter` narrows
130
+ _which_ events a subscriber receives, not _whether_ they were authorized
131
+ to receive them.
@@ -0,0 +1,2 @@
1
+ export { createTransport, type FilterableEntity, type SseTransport, type SseTransportOptions, } from "./sse-transport.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createTransport, } from "./sse-transport.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,GAIhB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,99 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { EntityMetadata, RealtimeFieldSelector, RealtimeTransport, ResolvedEntityConfig } from "@kavo/core";
3
+ /**
4
+ * What a subscribe request needs to parse and validate a `filter` query
5
+ * string for one entity — the same two pieces `DefaultFilterParser` and
6
+ * `Filter.parse` already need for REST: `metadata` for column-kind-aware
7
+ * value coercion, `config` for the `filterable` allowlist and the
8
+ * `query.maxFilterDepth`/`maxInValues` limits. `@kavo/sse` has no config
9
+ * resolution of its own (same reason `subscribableFields` is a callback,
10
+ * not a lookup this package performs itself), so the host app supplies
11
+ * both — typically `service.engine.metadata`/`service.engine.config` off
12
+ * the `DefaultKavoService` `createCrud` already returned for that entity.
13
+ * That property is concretely typed per entity (`ResolvedEntityConfig<
14
+ * Book>`), while this interface — serving every entity through one
15
+ * callback — is not; assigning the former to the latter needs the same
16
+ * erasure cast `kavo.ts`'s own `catalog.register` already uses internally
17
+ * (`config as unknown as ResolvedEntityConfig`), not a sign of a type
18
+ * mismatch.
19
+ */
20
+ export interface FilterableEntity {
21
+ readonly metadata: EntityMetadata;
22
+ readonly config: ResolvedEntityConfig;
23
+ }
24
+ /**
25
+ * What `handleRequest` needs to scope one subscription. `@kavo/sse` has no
26
+ * authentication of its own — every subscribe request that otherwise
27
+ * validates is accepted. A deployment that needs to gate who may open a
28
+ * stream does so in front of `handleRequest` (a reverse proxy, a host
29
+ * framework's own guard/middleware on the mounted route, …) — see
30
+ * `RealtimeTransport`'s doc comment on why subscriber-level access control
31
+ * is out of this seam entirely.
32
+ */
33
+ export interface SseTransportOptions {
34
+ /**
35
+ * Per-entity `RealtimeSettings.subscribableFields`, the same allowlist an
36
+ * app already configured via `createCrud` — this package has no config
37
+ * resolution of its own, so the caller supplies the lookup. A request
38
+ * naming a `fields` query param outside the allowlist is rejected with
39
+ * `400` before the stream opens, the same way `allowlists.selectable`
40
+ * rejects an unlisted field over REST. Returning `undefined` (including
41
+ * when the callback itself is omitted) means no allowlist is configured
42
+ * for that entity, so any requested field is accepted.
43
+ *
44
+ * Once configured, this allowlist is also enforced **unconditionally** on
45
+ * every outgoing `item` for that entity — not only when a subscriber
46
+ * names `fields` — the same way `allowlists.selectable` bounds a REST
47
+ * response whether or not the caller asked for a subset. A `fields`
48
+ * query param narrows further *within* that bound; it can never widen
49
+ * past it.
50
+ */
51
+ subscribableFields?(entityName: string): RealtimeFieldSelector | undefined;
52
+ /**
53
+ * Enables subscribe-time filtering (`filter[field][operator]=value`,
54
+ * issue #160) for one entity. Returning `undefined` (including when the
55
+ * callback itself is omitted) means that entity does not support
56
+ * subscribe-time filtering — a request naming any `filter[...]`/`filter`
57
+ * query param for it is rejected with `400` before the stream opens,
58
+ * rather than silently ignoring the filter and delivering everything.
59
+ */
60
+ filterableEntities?(entityName: string): FilterableEntity | undefined;
61
+ /** See `DEFAULT_BUFFER_LIMIT_BYTES`. */
62
+ bufferLimitBytes?: number;
63
+ }
64
+ /** A `RealtimeTransport` plus the HTTP entry point a host wires a route to. */
65
+ export interface SseTransport extends RealtimeTransport {
66
+ /**
67
+ * Handles one incoming SSE subscribe request:
68
+ * `GET ?channel=<entity>.<id>` (item channel) or `GET ?channel=<entity>`
69
+ * (collection channel, issue #160) with `Accept: text/event-stream`.
70
+ * Host-framework-agnostic — takes Node's own
71
+ * `IncomingMessage`/`ServerResponse`, which every Node HTTP framework's
72
+ * request/response either extends or wraps directly. Resolves once the
73
+ * response is settled (an error status was written, or the stream was
74
+ * opened) — the connection itself then lives for as long as the client
75
+ * keeps it open, torn down by the request's own `close`.
76
+ */
77
+ handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
78
+ /** Number of currently open subscriptions, across every channel. */
79
+ readonly connectionCount: number;
80
+ /** Ends every open connection and forgets them. For graceful shutdown and tests. */
81
+ close(): void;
82
+ }
83
+ /**
84
+ * The first real `RealtimeTransport` implementation (issue #155, over the
85
+ * seam #154 added; issue #160 / ADR-0024 added collection and filtered subscriptions):
86
+ * plain HTTP `text/event-stream`, no client or server library required.
87
+ * The channel a connection opens is either the item-level `<entity>.<id>`
88
+ * string `RealtimeEventDto.channel` carries, or the bare `<entity>` name
89
+ * (`RealtimeEventDto.entity`) for every event on that entity — `publish`
90
+ * fans an event out to every connection registered under either string, so
91
+ * one write reaches both an item-level and a collection-level subscriber
92
+ * from the same call.
93
+ *
94
+ * One process, one in-memory channel registry: a subscriber connected to
95
+ * this instance never sees a write handled by another instance of a
96
+ * horizontally-scaled app (see the package README's "Known limitations").
97
+ */
98
+ export declare function createTransport(options: SseTransportOptions): SseTransport;
99
+ //# sourceMappingURL=sse-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-transport.d.ts","sourceRoot":"","sources":["../src/sse-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EACV,cAAc,EAId,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAcpB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,kBAAkB,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;IAC3E;;;;;;;OAOG;IACH,kBAAkB,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACtE,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAa,SAAQ,iBAAiB;IACrD;;;;;;;;;;OAUG;IACH,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,oFAAoF;IACpF,KAAK,IAAI,IAAI,CAAC;CACf;AA0HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAiM1E"}
@@ -0,0 +1,315 @@
1
+ import { DefaultFilterParser, QueryValidationException, evaluateFilter } from "@kavo/core";
2
+ /**
3
+ * Bytes buffered in a connection's underlying socket before it is dropped
4
+ * rather than blocking `publish` for every other subscriber. `res.
5
+ * writableLength` (Node's `http.ServerResponse` is a `stream.Writable`) is
6
+ * the amount queued but not yet flushed to the OS — a slow reader grows
7
+ * this, a healthy one keeps it near zero. 64 KiB is generous for
8
+ * `RealtimeEventDto`-sized JSON frames while still catching a genuinely
9
+ * stuck client quickly.
10
+ */
11
+ const DEFAULT_BUFFER_LIMIT_BYTES = 64 * 1024;
12
+ /**
13
+ * Same array-or-`exclude` semantics `resolveFieldSelector` (core,
14
+ * internal) applies for REST's `selectable`/`filterable`/`sortable` — but
15
+ * with no "base" field list to fall back on, because `RealtimeFieldSelector`
16
+ * carries none (`settings.ts`'s own doc on why: this schema has no `Entity`
17
+ * type parameter to check a field name against). An explicit array is a
18
+ * positive allowlist; `{ exclude }` is checked negatively instead — "not
19
+ * excluded" rather than "in some base set minus excluded" — since there is
20
+ * no base set here to subtract from.
21
+ */
22
+ function isFieldAllowed(selector, field) {
23
+ if (selector === undefined)
24
+ return true;
25
+ // `"exclude" in selector`, not `Array.isArray` — `Array.isArray`'s guard is
26
+ // `arg is any[]`, and a `readonly string[]` is not assignable to `any[]`,
27
+ // so it fails to narrow the union in the negative branch. Same reason
28
+ // core's own `resolveFieldSelector` (resolve-entity-config.ts) checks it
29
+ // this way.
30
+ if ("exclude" in selector)
31
+ return !selector.exclude.includes(field);
32
+ return selector.includes(field);
33
+ }
34
+ /**
35
+ * Bounds an outgoing `item` to `selector` (unconditional, once configured)
36
+ * and then further to `fields` (a subscriber-requested subset, already
37
+ * validated ⊆ the allowlist at subscribe time). `undefined` for both means
38
+ * no narrowing — the item is delivered whole, today's behavior for an
39
+ * entity that never configured `subscribableFields`.
40
+ */
41
+ function narrowItem(item, selector, fields) {
42
+ if (item === null || typeof item !== "object")
43
+ return item;
44
+ const record = item;
45
+ let allowed;
46
+ if (fields !== undefined) {
47
+ allowed = fields;
48
+ }
49
+ else if (selector !== undefined) {
50
+ const keys = Object.keys(record);
51
+ allowed = "exclude" in selector ? keys.filter((key) => !selector.exclude.includes(key)) : selector;
52
+ }
53
+ else {
54
+ return item;
55
+ }
56
+ const narrowed = {};
57
+ for (const key of allowed)
58
+ if (key in record)
59
+ narrowed[key] = record[key];
60
+ return narrowed;
61
+ }
62
+ function sendJson(res, status, body) {
63
+ const payload = JSON.stringify(body);
64
+ res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) });
65
+ res.end(payload);
66
+ }
67
+ /**
68
+ * `id:` set even though nothing consumes it yet (resume-on-reconnect is a
69
+ * future issue) — a monotonically increasing counter, shared across every
70
+ * channel on this transport instance, so it stays meaningful the day resume
71
+ * is built instead of forcing a wire-format change then. Allocated once per
72
+ * `publish` call (not once per connection/frame) so every subscriber of one
73
+ * logical write sees the same `id:`, even though the `data:` they receive
74
+ * may differ once field-narrowing is applied.
75
+ */
76
+ function frame(id, event) {
77
+ return `id: ${id}\nevent: ${event.event}\ndata: ${JSON.stringify(event)}\n\n`;
78
+ }
79
+ /**
80
+ * Every `filter[...]`/`filter` query param, flattened the way
81
+ * `DefaultFilterParser` expects: one entry per literal key, repeated keys
82
+ * (the `filter[status][in][]=a&filter[status][in][]=b` form) collapsed
83
+ * into one array value under that key. Every other query param
84
+ * (`channel`, `fields`, …) rides along harmlessly — the parser only ever
85
+ * reads keys starting with `filter[` or the bare `filter` key.
86
+ */
87
+ function collectRawParams(searchParams) {
88
+ const raw = {};
89
+ for (const key of new Set(searchParams.keys())) {
90
+ const values = searchParams.getAll(key);
91
+ raw[key] = values.length > 1 ? values : values[0];
92
+ }
93
+ return raw;
94
+ }
95
+ function hasFilterParams(searchParams) {
96
+ for (const key of searchParams.keys()) {
97
+ if (key === "filter" || key.startsWith("filter["))
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ /** Every field name a condition in `expression` compares against, deduplicated. */
103
+ function collectConditionFields(expression) {
104
+ const fields = new Set();
105
+ const visit = (node) => {
106
+ if (node.kind === "condition") {
107
+ fields.add(node.field);
108
+ return;
109
+ }
110
+ for (const child of node.children)
111
+ visit(child);
112
+ };
113
+ visit(expression);
114
+ return [...fields];
115
+ }
116
+ /**
117
+ * The first real `RealtimeTransport` implementation (issue #155, over the
118
+ * seam #154 added; issue #160 / ADR-0024 added collection and filtered subscriptions):
119
+ * plain HTTP `text/event-stream`, no client or server library required.
120
+ * The channel a connection opens is either the item-level `<entity>.<id>`
121
+ * string `RealtimeEventDto.channel` carries, or the bare `<entity>` name
122
+ * (`RealtimeEventDto.entity`) for every event on that entity — `publish`
123
+ * fans an event out to every connection registered under either string, so
124
+ * one write reaches both an item-level and a collection-level subscriber
125
+ * from the same call.
126
+ *
127
+ * One process, one in-memory channel registry: a subscriber connected to
128
+ * this instance never sees a write handled by another instance of a
129
+ * horizontally-scaled app (see the package README's "Known limitations").
130
+ */
131
+ export function createTransport(options) {
132
+ const bufferLimitBytes = options.bufferLimitBytes ?? DEFAULT_BUFFER_LIMIT_BYTES;
133
+ const channels = new Map();
134
+ let nextEventId = 1;
135
+ function subscribe(connection) {
136
+ let subscribers = channels.get(connection.channel);
137
+ if (!subscribers) {
138
+ subscribers = new Set();
139
+ channels.set(connection.channel, subscribers);
140
+ }
141
+ subscribers.add(connection);
142
+ }
143
+ function unsubscribe(connection) {
144
+ const subscribers = channels.get(connection.channel);
145
+ if (!subscribers)
146
+ return;
147
+ subscribers.delete(connection);
148
+ if (subscribers.size === 0)
149
+ channels.delete(connection.channel);
150
+ }
151
+ /**
152
+ * `"deleted"` bypasses the filter unconditionally — `event.item` is
153
+ * `null`, so there is nothing to evaluate, and the alternative (silently
154
+ * excluding it) leaves a filtered subscriber with a row in its view that
155
+ * is permanently gone. See `RealtimeTransport`'s doc for the
156
+ * confidentiality tradeoff this implies. Every other event id evaluates
157
+ * normally: a write that makes a row start matching a filter is
158
+ * delivered as whatever its real event id is (`"entering" a filtered
159
+ * view reuses the ordinary event, not a synthesized `"created"`); a
160
+ * write that makes it stop matching simply is not delivered — there is
161
+ * no before-image available to detect that transition without an extra
162
+ * read the engine does not already do, so it is a documented limitation
163
+ * rather than a silently-wrong "leave" event.
164
+ */
165
+ function matches(connection, event) {
166
+ if (event.event === "deleted")
167
+ return true;
168
+ return evaluateFilter(connection.filter, (event.item ?? {}));
169
+ }
170
+ function deliverTo(subscribers, event, id) {
171
+ if (!subscribers || subscribers.size === 0)
172
+ return;
173
+ // Direct `for...of` over the live `Set`, not a snapshot copy: deleting
174
+ // the current entry mid-iteration (the `unsubscribe` below, for a
175
+ // connection that can't keep up) is well-defined under the Set
176
+ // iteration protocol — already-visited and about-to-be-visited
177
+ // entries are unaffected.
178
+ for (const connection of subscribers) {
179
+ if (!matches(connection, event))
180
+ continue;
181
+ if (connection.res.writableLength > bufferLimitBytes) {
182
+ // Can't keep up: dropped rather than left to block publish to
183
+ // every other subscriber of this channel.
184
+ unsubscribe(connection);
185
+ connection.res.end();
186
+ continue;
187
+ }
188
+ const outgoing = {
189
+ ...event,
190
+ item: (narrowItem(event.item, connection.selector, connection.fields) ?? null),
191
+ };
192
+ connection.res.write(frame(id, outgoing));
193
+ }
194
+ }
195
+ return {
196
+ name: "sse",
197
+ get connectionCount() {
198
+ let total = 0;
199
+ for (const subscribers of channels.values())
200
+ total += subscribers.size;
201
+ return total;
202
+ },
203
+ async publish(event) {
204
+ const itemSubscribers = channels.get(event.channel);
205
+ const collectionSubscribers = channels.get(event.entity);
206
+ if ((!itemSubscribers || itemSubscribers.size === 0) &&
207
+ (!collectionSubscribers || collectionSubscribers.size === 0)) {
208
+ return;
209
+ }
210
+ // Allocated once per publish, not once per connection — every
211
+ // subscriber of one logical write shares the same `id:` even though
212
+ // the `data:` they receive may differ (field narrowing).
213
+ const id = nextEventId++;
214
+ deliverTo(itemSubscribers, event, id);
215
+ deliverTo(collectionSubscribers, event, id);
216
+ },
217
+ async handleRequest(req, res) {
218
+ if (req.method !== "GET") {
219
+ sendJson(res, 400, { error: "SSE subscription requires GET" });
220
+ return;
221
+ }
222
+ const accept = req.headers.accept;
223
+ if (typeof accept !== "string" || !accept.includes("text/event-stream")) {
224
+ sendJson(res, 400, { error: "requires 'Accept: text/event-stream'" });
225
+ return;
226
+ }
227
+ const url = new URL(req.url ?? "", "http://kavo.invalid");
228
+ const channel = url.searchParams.get("channel");
229
+ if (!channel) {
230
+ sendJson(res, 400, {
231
+ error: "a 'channel' query parameter of the form '<entity>' or '<entity>.<id>' is required",
232
+ });
233
+ return;
234
+ }
235
+ const dot = channel.indexOf(".");
236
+ if (dot === 0 || dot === channel.length - 1) {
237
+ sendJson(res, 400, {
238
+ error: "a 'channel' query parameter of the form '<entity>' or '<entity>.<id>' is required",
239
+ });
240
+ return;
241
+ }
242
+ const entityName = dot === -1 ? channel : channel.slice(0, dot);
243
+ const selector = options.subscribableFields?.(entityName);
244
+ const fieldsParam = url.searchParams.get("fields");
245
+ let fields;
246
+ if (fieldsParam !== null) {
247
+ const requested = fieldsParam
248
+ .split(",")
249
+ .map((field) => field.trim())
250
+ .filter((field) => field.length > 0);
251
+ const disallowed = requested.filter((field) => !isFieldAllowed(selector, field));
252
+ if (disallowed.length > 0) {
253
+ sendJson(res, 400, { error: `field(s) not subscribable: ${disallowed.join(", ")}` });
254
+ return;
255
+ }
256
+ fields = requested;
257
+ }
258
+ let filter = null;
259
+ if (hasFilterParams(url.searchParams)) {
260
+ const filterable = options.filterableEntities?.(entityName);
261
+ if (filterable === undefined) {
262
+ sendJson(res, 400, { error: `entity '${entityName}' does not support subscribe-time filtering` });
263
+ return;
264
+ }
265
+ const parser = new DefaultFilterParser(filterable.metadata);
266
+ let parsed;
267
+ try {
268
+ parsed = parser.parse(collectRawParams(url.searchParams), filterable.config);
269
+ }
270
+ catch (error) {
271
+ if (error instanceof QueryValidationException) {
272
+ sendJson(res, 400, { error: "invalid filter", issues: error.issues });
273
+ return;
274
+ }
275
+ throw error;
276
+ }
277
+ filter = parsed.root;
278
+ if (filter !== null) {
279
+ const knownFields = new Set(filterable.metadata.fields.map((field) => field.name));
280
+ const conditionFields = collectConditionFields(filter);
281
+ const unevaluable = conditionFields.find((field) => !knownFields.has(field));
282
+ if (unevaluable !== undefined) {
283
+ sendJson(res, 400, {
284
+ error: `filter field '${unevaluable}' cannot be evaluated for a realtime subscription (relation and computed fields are not supported — only the entity's own columns)`,
285
+ });
286
+ return;
287
+ }
288
+ const disallowed = conditionFields.filter((field) => !isFieldAllowed(selector, field));
289
+ if (disallowed.length > 0) {
290
+ sendJson(res, 400, { error: `filter field(s) not subscribable: ${disallowed.join(", ")}` });
291
+ return;
292
+ }
293
+ }
294
+ }
295
+ res.writeHead(200, {
296
+ "Content-Type": "text/event-stream",
297
+ "Cache-Control": "no-cache",
298
+ Connection: "keep-alive",
299
+ });
300
+ res.flushHeaders();
301
+ const connection = { res, channel, entityName, filter, fields, selector };
302
+ subscribe(connection);
303
+ req.on("close", () => unsubscribe(connection));
304
+ res.on("error", () => unsubscribe(connection));
305
+ },
306
+ close() {
307
+ for (const subscribers of channels.values()) {
308
+ for (const connection of subscribers)
309
+ connection.res.end();
310
+ }
311
+ channels.clear();
312
+ },
313
+ };
314
+ }
315
+ //# sourceMappingURL=sse-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-transport.js","sourceRoot":"","sources":["../src/sse-transport.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@kavo/sse",
3
+ "version": "0.7.2",
4
+ "description": "Kavo SSE realtime transport — implements @kavo/core's RealtimeTransport over Server-Sent Events, plain HTTP with no required peer library.",
5
+ "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kavo-labs/kavo.git",
12
+ "directory": "packages/realtime/sse"
13
+ },
14
+ "homepage": "https://github.com/kavo-labs/kavo/tree/main/packages/realtime/sse#readme",
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -b"
31
+ },
32
+ "dependencies": {
33
+ "@kavo/core": "workspace:^"
34
+ }
35
+ }