@dudousxd/nestjs-notifications-sse 0.2.0 → 0.3.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.
@@ -0,0 +1,27 @@
1
+ /** A serializable SSE message routed through an {@link SseBackplane}. */
2
+ export interface SseBackplaneMessage {
3
+ data: unknown;
4
+ /** SSE event `type`. */
5
+ event?: string;
6
+ }
7
+ /**
8
+ * Cross-process fan-out for {@link import('./sse.hub').SseHub}.
9
+ *
10
+ * The default hub is **in-process** — a Subject per stream key — so a publish only reaches
11
+ * subscribers connected to the SAME node. In a multi-pod deployment, the code that publishes a
12
+ * notification and the node holding the user's SSE connection are often different processes (e.g. a
13
+ * worker writes, an API pod streams). Supply a backplane (e.g. {@link RedisSseBackplane}) and a
14
+ * publish on any node is broadcast to the SSE connections on every node.
15
+ */
16
+ export interface SseBackplane {
17
+ /** Broadcast `message` for `key` to all nodes (including this one). */
18
+ publish(key: string, message: SseBackplaneMessage): void | Promise<void>;
19
+ /**
20
+ * Register the handler the hub uses to deliver inbound (cross-node) messages to its local
21
+ * subscribers. Called once when the hub initializes.
22
+ */
23
+ subscribe(handler: (key: string, message: SseBackplaneMessage) => void): void | Promise<void>;
24
+ /** Tear down any connections/subscriptions. Called on module destroy. */
25
+ close?(): void | Promise<void>;
26
+ }
27
+ //# sourceMappingURL=backplane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backplane.d.ts","sourceRoot":"","sources":["../src/backplane.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9F,yEAAyE;IACzE,KAAK,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=backplane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backplane.js","sourceRoot":"","sources":["../src/backplane.ts"],"names":[],"mappings":""}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { Sse, SseChannel, type SseChannelOptions, type SseNotification, } from './sse.channel';
2
2
  export { SseHub } from './sse.hub';
3
3
  export { SseChannelModule, type SseChannelModuleOptions } from './sse.module';
4
- export { SSE_OPTIONS } from './tokens';
4
+ export { SSE_OPTIONS, SSE_BACKPLANE } from './tokens';
5
5
  export { sseKey } from './sse-key';
6
+ export type { SseBackplane, SseBackplaneMessage } from './backplane';
7
+ export { RedisSseBackplane, type RedisSseBackplaneOptions, type RedisPubSubClient, } from './redis.backplane';
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sseKey = exports.SSE_OPTIONS = exports.SseChannelModule = exports.SseHub = exports.SseChannel = exports.Sse = void 0;
3
+ exports.RedisSseBackplane = exports.sseKey = exports.SSE_BACKPLANE = exports.SSE_OPTIONS = exports.SseChannelModule = exports.SseHub = exports.SseChannel = exports.Sse = void 0;
4
4
  var sse_channel_1 = require("./sse.channel");
5
5
  Object.defineProperty(exports, "Sse", { enumerable: true, get: function () { return sse_channel_1.Sse; } });
6
6
  Object.defineProperty(exports, "SseChannel", { enumerable: true, get: function () { return sse_channel_1.SseChannel; } });
@@ -10,6 +10,9 @@ var sse_module_1 = require("./sse.module");
10
10
  Object.defineProperty(exports, "SseChannelModule", { enumerable: true, get: function () { return sse_module_1.SseChannelModule; } });
11
11
  var tokens_1 = require("./tokens");
12
12
  Object.defineProperty(exports, "SSE_OPTIONS", { enumerable: true, get: function () { return tokens_1.SSE_OPTIONS; } });
13
+ Object.defineProperty(exports, "SSE_BACKPLANE", { enumerable: true, get: function () { return tokens_1.SSE_BACKPLANE; } });
13
14
  var sse_key_1 = require("./sse-key");
14
15
  Object.defineProperty(exports, "sseKey", { enumerable: true, get: function () { return sse_key_1.sseKey; } });
16
+ var redis_backplane_1 = require("./redis.backplane");
17
+ Object.defineProperty(exports, "RedisSseBackplane", { enumerable: true, get: function () { return redis_backplane_1.RedisSseBackplane; } });
15
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAKuB;AAJrB,kGAAA,GAAG,OAAA;AACH,yGAAA,UAAU,OAAA;AAIZ,qCAAmC;AAA1B,iGAAA,MAAM,OAAA;AACf,2CAA8E;AAArE,8GAAA,gBAAgB,OAAA;AACzB,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AACpB,qCAAmC;AAA1B,iGAAA,MAAM,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,6CAKuB;AAJrB,kGAAA,GAAG,OAAA;AACH,yGAAA,UAAU,OAAA;AAIZ,qCAAmC;AAA1B,iGAAA,MAAM,OAAA;AACf,2CAA8E;AAArE,8GAAA,gBAAgB,OAAA;AACzB,mCAAsD;AAA7C,qGAAA,WAAW,OAAA;AAAE,uGAAA,aAAa,OAAA;AACnC,qCAAmC;AAA1B,iGAAA,MAAM,OAAA;AAEf,qDAI2B;AAHzB,oHAAA,iBAAiB,OAAA"}
@@ -0,0 +1,43 @@
1
+ import type { SseBackplane, SseBackplaneMessage } from './backplane';
2
+ /**
3
+ * The slice of an `ioredis`-style client this backplane uses. Pass real `ioredis` instances (or any
4
+ * compatible client) — the package doesn't depend on `ioredis` itself, so you control the version
5
+ * and connection. Use SEPARATE clients for publisher and subscriber: a subscriber connection enters
6
+ * subscribe mode and can't issue regular commands.
7
+ */
8
+ export interface RedisPubSubClient {
9
+ publish(channel: string, message: string): unknown;
10
+ subscribe(channel: string, callback?: (err: Error | null, count: number) => void): unknown;
11
+ on(event: 'message', listener: (channel: string, message: string) => void): unknown;
12
+ quit?(): unknown;
13
+ }
14
+ export interface RedisSseBackplaneOptions {
15
+ /** Client used to publish (regular command mode). */
16
+ publisher: RedisPubSubClient;
17
+ /** Client used to subscribe (enters subscribe mode). Must be a different connection. */
18
+ subscriber: RedisPubSubClient;
19
+ /** Pub/sub channel. Default `nestjs-notifications:sse`. */
20
+ channel?: string;
21
+ }
22
+ /**
23
+ * A Redis pub/sub {@link SseBackplane}. Every publish fans out over one Redis channel to all nodes,
24
+ * so SSE connections on any pod receive notifications written on any other pod. Mirrors the common
25
+ * "writer pod / API pod" split.
26
+ *
27
+ * ```ts
28
+ * import Redis from 'ioredis';
29
+ * SseChannelModule.forRoot({
30
+ * backplane: new RedisSseBackplane({ publisher: new Redis(url), subscriber: new Redis(url) }),
31
+ * });
32
+ * ```
33
+ */
34
+ export declare class RedisSseBackplane implements SseBackplane {
35
+ private readonly publisher;
36
+ private readonly subscriber;
37
+ private readonly channel;
38
+ constructor(options: RedisSseBackplaneOptions);
39
+ publish(key: string, message: SseBackplaneMessage): void;
40
+ subscribe(handler: (key: string, message: SseBackplaneMessage) => void): void;
41
+ close(): void;
42
+ }
43
+ //# sourceMappingURL=redis.backplane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.backplane.d.ts","sourceRoot":"","sources":["../src/redis.backplane.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAErE;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACnD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAC3F,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IACpF,IAAI,CAAC,IAAI,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,qDAAqD;IACrD,SAAS,EAAE,iBAAiB,CAAC;IAC7B,wFAAwF;IACxF,UAAU,EAAE,iBAAiB,CAAC;IAC9B,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,wBAAwB;IAM7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAIxD,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,IAAI;IAa7E,KAAK,IAAI,IAAI;CAId"}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisSseBackplane = void 0;
4
+ const DEFAULT_CHANNEL = 'nestjs-notifications:sse';
5
+ /**
6
+ * A Redis pub/sub {@link SseBackplane}. Every publish fans out over one Redis channel to all nodes,
7
+ * so SSE connections on any pod receive notifications written on any other pod. Mirrors the common
8
+ * "writer pod / API pod" split.
9
+ *
10
+ * ```ts
11
+ * import Redis from 'ioredis';
12
+ * SseChannelModule.forRoot({
13
+ * backplane: new RedisSseBackplane({ publisher: new Redis(url), subscriber: new Redis(url) }),
14
+ * });
15
+ * ```
16
+ */
17
+ class RedisSseBackplane {
18
+ publisher;
19
+ subscriber;
20
+ channel;
21
+ constructor(options) {
22
+ this.publisher = options.publisher;
23
+ this.subscriber = options.subscriber;
24
+ this.channel = options.channel ?? DEFAULT_CHANNEL;
25
+ }
26
+ publish(key, message) {
27
+ this.publisher.publish(this.channel, JSON.stringify({ key, message }));
28
+ }
29
+ subscribe(handler) {
30
+ this.subscriber.subscribe(this.channel);
31
+ this.subscriber.on('message', (channel, raw) => {
32
+ if (channel !== this.channel)
33
+ return;
34
+ try {
35
+ const parsed = JSON.parse(raw);
36
+ handler(parsed.key, parsed.message);
37
+ }
38
+ catch {
39
+ // Ignore malformed payloads — never let a bad message tear down the subscription.
40
+ }
41
+ });
42
+ }
43
+ close() {
44
+ this.publisher.quit?.();
45
+ this.subscriber.quit?.();
46
+ }
47
+ }
48
+ exports.RedisSseBackplane = RedisSseBackplane;
49
+ //# sourceMappingURL=redis.backplane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.backplane.js","sourceRoot":"","sources":["../src/redis.backplane.ts"],"names":[],"mappings":";;;AAwBA,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAa,iBAAiB;IACX,SAAS,CAAoB;IAC7B,UAAU,CAAoB;IAC9B,OAAO,CAAS;IAEjC,YAAY,OAAiC;QAC3C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,eAAe,CAAC;IACpD,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,OAA4B;QAC/C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,SAAS,CAAC,OAA4D;QACpE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;YAC7C,IAAI,OAAO,KAAK,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkD,CAAC;gBAChF,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YACtC,CAAC;YAAC,MAAM,CAAC;gBACP,kFAAkF;YACpF,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;IAC3B,CAAC;CACF;AAhCD,8CAgCC"}
package/dist/sse.hub.d.ts CHANGED
@@ -1,33 +1,40 @@
1
- import { type MessageEvent } from '@nestjs/common';
1
+ import { type MessageEvent, type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
2
2
  import { type Observable } from 'rxjs';
3
+ import type { SseBackplane } from './backplane';
3
4
  /**
4
- * In-memory fan-out for native NestJS Server-Sent Events. Keeps one
5
- * {@link Subject} per stream key; the channel pushes events into it via
6
- * {@link SseHub.publish} and a controller's `@Sse()` endpoint reads from it via
7
- * {@link SseHub.stream}.
5
+ * Fan-out for native NestJS Server-Sent Events. Keeps one {@link Subject} per stream key; the
6
+ * channel pushes events into it via {@link SseHub.publish} and a controller's `@Sse()` endpoint
7
+ * reads from it via {@link SseHub.stream}.
8
8
  *
9
- * Each key may have multiple concurrent subscribers (e.g. a user with several
10
- * open tabs). The Subject is created lazily on first subscribe and torn down
11
- * once the last subscriber unsubscribes.
9
+ * Each key may have multiple concurrent subscribers (e.g. a user with several open tabs). The
10
+ * Subject is created lazily on first subscribe and torn down once the last subscriber unsubscribes.
11
+ *
12
+ * By default delivery is **in-process**. Provide an {@link SseBackplane} (e.g. Redis pub/sub) to
13
+ * fan out across pods: a publish on any node then reaches the SSE connections on every node.
12
14
  */
13
- export declare class SseHub {
15
+ export declare class SseHub implements OnModuleInit, OnModuleDestroy {
16
+ private readonly backplane?;
14
17
  private readonly subjects;
15
18
  private readonly refcounts;
19
+ constructor(backplane?: SseBackplane | undefined);
20
+ onModuleInit(): Promise<void>;
21
+ onModuleDestroy(): Promise<void>;
16
22
  /**
17
- * Return the live stream for `key` as an Observable. Subscribing creates the
18
- * underlying Subject (if absent) and bumps its refcount; unsubscribing
19
- * decrements it and removes the Subject once no subscribers remain. Return
20
- * this directly from a controller's `@Sse()` method.
23
+ * Return the live stream for `key` as an Observable. Subscribing creates the underlying Subject
24
+ * (if absent) and bumps its refcount; unsubscribing decrements it and removes the Subject once no
25
+ * subscribers remain. Return this directly from a controller's `@Sse()` method.
21
26
  */
22
27
  stream(key: string): Observable<MessageEvent>;
23
28
  /**
24
- * Push a `MessageEvent` to every subscriber of `key`. No-ops when nobody is
25
- * listening (no Subject exists), since SSE is fire-and-forget. `options.event`
26
- * sets the SSE event `type`.
29
+ * Push a message to every subscriber of `key`. With a backplane it's broadcast to all nodes
30
+ * (delivered back through the backplane subscription, including to this node); without one it's
31
+ * delivered to local subscribers directly. `options.event` sets the SSE event `type`.
27
32
  */
28
33
  publish(key: string, data: unknown, options?: {
29
34
  event?: string;
30
35
  }): void;
36
+ /** Deliver a message to this node's local subscribers of `key` (no-op when none). */
37
+ private deliverLocal;
31
38
  private subjectFor;
32
39
  }
33
40
  //# sourceMappingURL=sse.hub.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse.hub.d.ts","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,MAAM,CAAC;AAE1D;;;;;;;;;GASG;AACH,qBACa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,YAAY,CAAC;IAkB7C;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAWvE,OAAO,CAAC,UAAU;CAQnB"}
1
+ {"version":3,"file":"sse.hub.d.ts","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,YAAY,EAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,MAAM,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,aAAa,CAAC;AAGrE;;;;;;;;;;GAUG;AACH,qBACa,MAAO,YAAW,YAAY,EAAE,eAAe;IAOxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAN7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;gBAKpC,SAAS,CAAC,EAAE,YAAY,YAAA;IAGrC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAO7B,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,YAAY,CAAC;IAkB7C;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IASvE,qFAAqF;IACrF,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,UAAU;CAQnB"}
package/dist/sse.hub.js CHANGED
@@ -5,28 +5,48 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
8
14
  Object.defineProperty(exports, "__esModule", { value: true });
9
15
  exports.SseHub = void 0;
10
16
  const common_1 = require("@nestjs/common");
11
17
  const rxjs_1 = require("rxjs");
18
+ const tokens_1 = require("./tokens");
12
19
  /**
13
- * In-memory fan-out for native NestJS Server-Sent Events. Keeps one
14
- * {@link Subject} per stream key; the channel pushes events into it via
15
- * {@link SseHub.publish} and a controller's `@Sse()` endpoint reads from it via
16
- * {@link SseHub.stream}.
20
+ * Fan-out for native NestJS Server-Sent Events. Keeps one {@link Subject} per stream key; the
21
+ * channel pushes events into it via {@link SseHub.publish} and a controller's `@Sse()` endpoint
22
+ * reads from it via {@link SseHub.stream}.
23
+ *
24
+ * Each key may have multiple concurrent subscribers (e.g. a user with several open tabs). The
25
+ * Subject is created lazily on first subscribe and torn down once the last subscriber unsubscribes.
17
26
  *
18
- * Each key may have multiple concurrent subscribers (e.g. a user with several
19
- * open tabs). The Subject is created lazily on first subscribe and torn down
20
- * once the last subscriber unsubscribes.
27
+ * By default delivery is **in-process**. Provide an {@link SseBackplane} (e.g. Redis pub/sub) to
28
+ * fan out across pods: a publish on any node then reaches the SSE connections on every node.
21
29
  */
22
30
  let SseHub = class SseHub {
31
+ backplane;
23
32
  subjects = new Map();
24
33
  refcounts = new Map();
34
+ constructor(backplane) {
35
+ this.backplane = backplane;
36
+ }
37
+ async onModuleInit() {
38
+ // Inbound messages from other nodes are delivered to this node's local subscribers.
39
+ if (this.backplane) {
40
+ await this.backplane.subscribe((key, message) => this.deliverLocal(key, message));
41
+ }
42
+ }
43
+ async onModuleDestroy() {
44
+ await this.backplane?.close?.();
45
+ }
25
46
  /**
26
- * Return the live stream for `key` as an Observable. Subscribing creates the
27
- * underlying Subject (if absent) and bumps its refcount; unsubscribing
28
- * decrements it and removes the Subject once no subscribers remain. Return
29
- * this directly from a controller's `@Sse()` method.
47
+ * Return the live stream for `key` as an Observable. Subscribing creates the underlying Subject
48
+ * (if absent) and bumps its refcount; unsubscribing decrements it and removes the Subject once no
49
+ * subscribers remain. Return this directly from a controller's `@Sse()` method.
30
50
  */
31
51
  stream(key) {
32
52
  const subject = this.subjectFor(key);
@@ -44,19 +64,25 @@ let SseHub = class SseHub {
44
64
  }));
45
65
  }
46
66
  /**
47
- * Push a `MessageEvent` to every subscriber of `key`. No-ops when nobody is
48
- * listening (no Subject exists), since SSE is fire-and-forget. `options.event`
49
- * sets the SSE event `type`.
67
+ * Push a message to every subscriber of `key`. With a backplane it's broadcast to all nodes
68
+ * (delivered back through the backplane subscription, including to this node); without one it's
69
+ * delivered to local subscribers directly. `options.event` sets the SSE event `type`.
50
70
  */
51
71
  publish(key, data, options) {
72
+ const message = { data, event: options?.event };
73
+ if (this.backplane) {
74
+ void this.backplane.publish(key, message);
75
+ return;
76
+ }
77
+ this.deliverLocal(key, message);
78
+ }
79
+ /** Deliver a message to this node's local subscribers of `key` (no-op when none). */
80
+ deliverLocal(key, message) {
52
81
  const subject = this.subjects.get(key);
53
82
  if (!subject)
54
83
  return;
55
- const data_ = data;
56
- const message = options?.event
57
- ? { data: data_, type: options.event }
58
- : { data: data_ };
59
- subject.next(message);
84
+ const data = message.data;
85
+ subject.next(message.event ? { data, type: message.event } : { data });
60
86
  }
61
87
  subjectFor(key) {
62
88
  let subject = this.subjects.get(key);
@@ -69,6 +95,9 @@ let SseHub = class SseHub {
69
95
  };
70
96
  exports.SseHub = SseHub;
71
97
  exports.SseHub = SseHub = __decorate([
72
- (0, common_1.Injectable)()
98
+ (0, common_1.Injectable)(),
99
+ __param(0, (0, common_1.Optional)()),
100
+ __param(0, (0, common_1.Inject)(tokens_1.SSE_BACKPLANE)),
101
+ __metadata("design:paramtypes", [Object])
73
102
  ], SseHub);
74
103
  //# sourceMappingURL=sse.hub.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse.hub.js","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA+D;AAC/D,+BAA0D;AAE1D;;;;;;;;;GASG;AAEI,IAAM,MAAM,GAAZ,MAAM,MAAM;IACA,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IACpD,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,GAAW;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAE5D,OAAO,OAAO,CAAC,YAAY,EAAE,CAAC,IAAI,CAChC,IAAA,eAAQ,EAAC,GAAG,EAAE;YACZ,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAChD,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;gBACd,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC1B,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;QACH,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,GAAW,EAAE,IAAa,EAAE,OAA4B;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,MAAM,KAAK,GAAG,IAA4B,CAAC;QAC3C,MAAM,OAAO,GAAiB,OAAO,EAAE,KAAK;YAC1C,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE;YACtC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IAEO,UAAU,CAAC,GAAW;QAC5B,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,IAAI,cAAO,EAAgB,CAAC;YACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AApDY,wBAAM;iBAAN,MAAM;IADlB,IAAA,mBAAU,GAAE;GACA,MAAM,CAoDlB"}
1
+ {"version":3,"file":"sse.hub.js","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,14 @@
1
1
  import { type DynamicModule } from '@nestjs/common';
2
+ import type { SseBackplane } from './backplane';
2
3
  export interface SseChannelModuleOptions {
3
4
  /** SSE event name (`type`) emitted to clients. Defaults to `'notification'`. */
4
5
  event?: string;
6
+ /**
7
+ * Cross-pod fan-out backplane (e.g. {@link import('./redis.backplane').RedisSseBackplane}). Omit
8
+ * for in-process delivery (single node). Required when publishers and SSE connections live on
9
+ * different processes.
10
+ */
11
+ backplane?: SseBackplane;
5
12
  /** Register globally so the channel is discoverable app-wide. Default true. */
6
13
  global?: boolean;
7
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sse.module.d.ts","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,gBAAgB,CAAC;AAK3E,MAAM,WAAW,uBAAuB;IACtC,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,qBACa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,uBAA4B,GAAG,aAAa;CAkBrE"}
1
+ {"version":3,"file":"sse.module.d.ts","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,uBAAuB;IACtC,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,qBACa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,uBAA4B,GAAG,aAAa;CAmBrE"}
@@ -31,6 +31,7 @@ let SseChannelModule = SseChannelModule_1 = class SseChannelModule {
31
31
  };
32
32
  const providers = [
33
33
  { provide: tokens_1.SSE_OPTIONS, useValue: channelOptions },
34
+ { provide: tokens_1.SSE_BACKPLANE, useValue: options.backplane ?? null },
34
35
  sse_hub_1.SseHub,
35
36
  sse_channel_1.SseChannel,
36
37
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"sse.module.js","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAA2E;AAC3E,+CAAmE;AACnE,uCAAmC;AACnC,qCAAuC;AASvC;;;;;;;;;;;GAWG;AAEI,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,UAAmC,EAAE;QAClD,MAAM,cAAc,GAAsB;YACxC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,cAAc;SACvC,CAAC;QAEF,MAAM,SAAS,GAAe;YAC5B,EAAE,OAAO,EAAE,oBAAW,EAAE,QAAQ,EAAE,cAAc,EAAE;YAClD,gBAAM;YACN,wBAAU;SACX,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,kBAAgB;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;YAC9B,SAAS;YACT,OAAO,EAAE,CAAC,gBAAM,EAAE,wBAAU,CAAC;SAC9B,CAAC;IACJ,CAAC;CACF,CAAA;AAnBY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,gBAAgB,CAmB5B"}
1
+ {"version":3,"file":"sse.module.js","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAA2E;AAE3E,+CAAmE;AACnE,uCAAmC;AACnC,qCAAsD;AAetD;;;;;;;;;;;GAWG;AAEI,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,UAAmC,EAAE;QAClD,MAAM,cAAc,GAAsB;YACxC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,cAAc;SACvC,CAAC;QAEF,MAAM,SAAS,GAAe;YAC5B,EAAE,OAAO,EAAE,oBAAW,EAAE,QAAQ,EAAE,cAAc,EAAE;YAClD,EAAE,OAAO,EAAE,sBAAa,EAAE,QAAQ,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE;YAC/D,gBAAM;YACN,wBAAU;SACX,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,kBAAgB;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;YAC9B,SAAS;YACT,OAAO,EAAE,CAAC,gBAAM,EAAE,wBAAU,CAAC;SAC9B,CAAC;IACJ,CAAC;CACF,CAAA;AApBY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,gBAAgB,CAoB5B"}
package/dist/tokens.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  /** DI token for the resolved {@link SseChannelOptions} (event name, etc.). */
2
2
  export declare const SSE_OPTIONS: unique symbol;
3
+ /** DI token for the optional {@link import('./backplane').SseBackplane} (cross-pod fan-out). */
4
+ export declare const SSE_BACKPLANE: unique symbol;
3
5
  //# sourceMappingURL=tokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,WAAW,eAAwB,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,WAAW,eAAwB,CAAC;AAEjD,gGAAgG;AAChG,eAAO,MAAM,aAAa,eAA0B,CAAC"}
package/dist/tokens.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SSE_OPTIONS = void 0;
3
+ exports.SSE_BACKPLANE = exports.SSE_OPTIONS = void 0;
4
4
  /** DI token for the resolved {@link SseChannelOptions} (event name, etc.). */
5
5
  exports.SSE_OPTIONS = Symbol('SSE_OPTIONS');
6
+ /** DI token for the optional {@link import('./backplane').SseBackplane} (cross-pod fan-out). */
7
+ exports.SSE_BACKPLANE = Symbol('SSE_BACKPLANE');
6
8
  //# sourceMappingURL=tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":";;;AAAA,8EAA8E;AACjE,QAAA,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":";;;AAAA,8EAA8E;AACjE,QAAA,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;AAEjD,gGAAgG;AACnF,QAAA,aAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-notifications-sse",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Server-Sent Events channel for nestjs-notifications — push real-time notifications over native NestJS SSE",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",
@@ -21,11 +21,11 @@
21
21
  "rxjs": "^7.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@nestjs/common": "^11.0.0",
25
- "reflect-metadata": "^0.2.2",
26
- "rxjs": "^7.0.0",
27
- "typescript": "^5.7.2",
28
- "@dudousxd/nestjs-notifications-core": "^0.4.0"
24
+ "@nestjs/common": "11.1.26",
25
+ "reflect-metadata": "0.2.2",
26
+ "rxjs": "7.8.2",
27
+ "typescript": "5.9.3",
28
+ "@dudousxd/nestjs-notifications-core": "^0.5.0"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsc -p tsconfig.json",