@dudousxd/nestjs-notifications-sse 0.2.0 → 0.4.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/dist/backplane.d.ts +27 -0
- package/dist/backplane.d.ts.map +1 -0
- package/dist/backplane.js +3 -0
- package/dist/backplane.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/redis.backplane.d.ts +43 -0
- package/dist/redis.backplane.d.ts.map +1 -0
- package/dist/redis.backplane.js +49 -0
- package/dist/redis.backplane.js.map +1 -0
- package/dist/sse.hub.d.ts +23 -16
- package/dist/sse.hub.d.ts.map +1 -1
- package/dist/sse.hub.js +49 -20
- package/dist/sse.hub.js.map +1 -1
- package/dist/sse.module.d.ts +32 -5
- package/dist/sse.module.d.ts.map +1 -1
- package/dist/sse.module.js +44 -7
- package/dist/sse.module.js.map +1 -1
- package/dist/stream-controller.d.ts +43 -0
- package/dist/stream-controller.d.ts.map +1 -0
- package/dist/stream-controller.js +71 -0
- package/dist/stream-controller.js.map +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +3 -1
- package/dist/tokens.js.map +1 -1
- package/package.json +6 -6
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"backplane.js","sourceRoot":"","sources":["../src/backplane.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { Sse, SseChannel, type SseChannelOptions, type SseNotification, } from './sse.channel';
|
|
2
2
|
export { SseHub } from './sse.hub';
|
|
3
|
-
export { SseChannelModule, type SseChannelModuleOptions } from './sse.module';
|
|
4
|
-
export { SSE_OPTIONS } from './tokens';
|
|
3
|
+
export { SseChannelModule, type SseChannelModuleOptions, type SseChannelModuleAsyncOptions, type SseChannelAsyncConfig, } from './sse.module';
|
|
4
|
+
export { SSE_OPTIONS, SSE_BACKPLANE } from './tokens';
|
|
5
5
|
export { sseKey } from './sse-key';
|
|
6
|
+
export { createNotificationsStreamController, type NotificationsStreamControllerOptions, } from './stream-controller';
|
|
7
|
+
export type { SseBackplane, SseBackplaneMessage } from './backplane';
|
|
8
|
+
export { RedisSseBackplane, type RedisSseBackplaneOptions, type RedisPubSubClient, } from './redis.backplane';
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EACL,mCAAmC,EACnC,KAAK,oCAAoC,GAC1C,MAAM,qBAAqB,CAAC;AAC7B,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.createNotificationsStreamController = 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,11 @@ 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 stream_controller_1 = require("./stream-controller");
|
|
17
|
+
Object.defineProperty(exports, "createNotificationsStreamController", { enumerable: true, get: function () { return stream_controller_1.createNotificationsStreamController; } });
|
|
18
|
+
var redis_backplane_1 = require("./redis.backplane");
|
|
19
|
+
Object.defineProperty(exports, "RedisSseBackplane", { enumerable: true, get: function () { return redis_backplane_1.RedisSseBackplane; } });
|
|
15
20
|
//# 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,
|
|
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,2CAKsB;AAJpB,8GAAA,gBAAgB,OAAA;AAKlB,mCAAsD;AAA7C,qGAAA,WAAW,OAAA;AAAE,uGAAA,aAAa,OAAA;AACnC,qCAAmC;AAA1B,iGAAA,MAAM,OAAA;AACf,yDAG6B;AAF3B,wIAAA,mCAAmC,OAAA;AAIrC,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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* {@link SseHub.
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
25
|
-
*
|
|
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
|
package/dist/sse.hub.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.hub.d.ts","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* {@link SseHub.
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
48
|
-
*
|
|
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
|
|
56
|
-
|
|
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
|
package/dist/sse.hub.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.hub.js","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sse.hub.js","sourceRoot":"","sources":["../src/sse.hub.ts"],"names":[],"mappings":"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"}
|
package/dist/sse.module.d.ts
CHANGED
|
@@ -1,23 +1,50 @@
|
|
|
1
|
-
import { type DynamicModule } from '@nestjs/common';
|
|
1
|
+
import { type DynamicModule, type FactoryProvider, type ModuleMetadata } 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;
|
|
12
|
+
/** Register globally so the channel is discoverable app-wide. Default true. */
|
|
13
|
+
global?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** The slice of {@link SseChannelModuleOptions} a {@link SseChannelModuleAsyncOptions} factory returns. */
|
|
16
|
+
export interface SseChannelAsyncConfig {
|
|
17
|
+
event?: string;
|
|
18
|
+
backplane?: SseBackplane;
|
|
19
|
+
}
|
|
20
|
+
/** Async registration for {@link SseChannelModule.forRootAsync} — build options (e.g. the backplane) from DI. */
|
|
21
|
+
export interface SseChannelModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
22
|
+
useFactory: (...args: never[]) => SseChannelAsyncConfig | Promise<SseChannelAsyncConfig>;
|
|
23
|
+
inject?: FactoryProvider['inject'];
|
|
5
24
|
/** Register globally so the channel is discoverable app-wide. Default true. */
|
|
6
25
|
global?: boolean;
|
|
7
26
|
}
|
|
8
27
|
/**
|
|
9
28
|
* Registers the SSE channel and its {@link SseHub}.
|
|
10
29
|
*
|
|
11
|
-
* The streaming endpoint itself is mounted by the consumer in their own
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for the controller example.
|
|
30
|
+
* The streaming endpoint itself is mounted by the consumer in their own controller via NestJS's
|
|
31
|
+
* native `@Sse()` decorator — this module only provides the hub (to read from) and the channel (to
|
|
32
|
+
* push into). See {@link SseChannel} for the controller example.
|
|
15
33
|
*
|
|
16
34
|
* ```ts
|
|
17
35
|
* SseChannelModule.forRoot({ event: 'notification' });
|
|
36
|
+
*
|
|
37
|
+
* // Build the backplane from DI (e.g. a Redis config service):
|
|
38
|
+
* SseChannelModule.forRootAsync({
|
|
39
|
+
* inject: [AppConfig],
|
|
40
|
+
* useFactory: (cfg: AppConfig) => ({
|
|
41
|
+
* backplane: new RedisSseBackplane({ publisher: new Redis(cfg.redis), subscriber: new Redis(cfg.redis) }),
|
|
42
|
+
* }),
|
|
43
|
+
* });
|
|
18
44
|
* ```
|
|
19
45
|
*/
|
|
20
46
|
export declare class SseChannelModule {
|
|
21
47
|
static forRoot(options?: SseChannelModuleOptions): DynamicModule;
|
|
48
|
+
static forRootAsync(options: SseChannelModuleAsyncOptions): DynamicModule;
|
|
22
49
|
}
|
|
23
50
|
//# sourceMappingURL=sse.module.d.ts.map
|
package/dist/sse.module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.module.d.ts","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"sse.module.d.ts","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,eAAe,EAEpB,KAAK,cAAc,EAEpB,MAAM,gBAAgB,CAAC;AACxB,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,2GAA2G;AAC3G,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,iHAAiH;AACjH,MAAM,WAAW,4BAA6B,SAAQ,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;IACnF,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACzF,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,uBAA4B,GAAG,aAAa;IAgBpE,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,4BAA4B,GAAG,aAAa;CAgC1E"}
|
package/dist/sse.module.js
CHANGED
|
@@ -12,31 +12,68 @@ const common_1 = require("@nestjs/common");
|
|
|
12
12
|
const sse_channel_1 = require("./sse.channel");
|
|
13
13
|
const sse_hub_1 = require("./sse.hub");
|
|
14
14
|
const tokens_1 = require("./tokens");
|
|
15
|
+
/** Internal token holding the resolved async config. */
|
|
16
|
+
const SSE_RESOLVED_CONFIG = Symbol('SSE_RESOLVED_CONFIG');
|
|
15
17
|
/**
|
|
16
18
|
* Registers the SSE channel and its {@link SseHub}.
|
|
17
19
|
*
|
|
18
|
-
* The streaming endpoint itself is mounted by the consumer in their own
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* for the controller example.
|
|
20
|
+
* The streaming endpoint itself is mounted by the consumer in their own controller via NestJS's
|
|
21
|
+
* native `@Sse()` decorator — this module only provides the hub (to read from) and the channel (to
|
|
22
|
+
* push into). See {@link SseChannel} for the controller example.
|
|
22
23
|
*
|
|
23
24
|
* ```ts
|
|
24
25
|
* SseChannelModule.forRoot({ event: 'notification' });
|
|
26
|
+
*
|
|
27
|
+
* // Build the backplane from DI (e.g. a Redis config service):
|
|
28
|
+
* SseChannelModule.forRootAsync({
|
|
29
|
+
* inject: [AppConfig],
|
|
30
|
+
* useFactory: (cfg: AppConfig) => ({
|
|
31
|
+
* backplane: new RedisSseBackplane({ publisher: new Redis(cfg.redis), subscriber: new Redis(cfg.redis) }),
|
|
32
|
+
* }),
|
|
33
|
+
* });
|
|
25
34
|
* ```
|
|
26
35
|
*/
|
|
27
36
|
let SseChannelModule = SseChannelModule_1 = class SseChannelModule {
|
|
28
37
|
static forRoot(options = {}) {
|
|
29
|
-
const channelOptions = {
|
|
30
|
-
|
|
38
|
+
const channelOptions = { event: options.event ?? 'notification' };
|
|
39
|
+
return {
|
|
40
|
+
module: SseChannelModule_1,
|
|
41
|
+
global: options.global ?? true,
|
|
42
|
+
providers: [
|
|
43
|
+
{ provide: tokens_1.SSE_OPTIONS, useValue: channelOptions },
|
|
44
|
+
{ provide: tokens_1.SSE_BACKPLANE, useValue: options.backplane ?? null },
|
|
45
|
+
sse_hub_1.SseHub,
|
|
46
|
+
sse_channel_1.SseChannel,
|
|
47
|
+
],
|
|
48
|
+
exports: [sse_hub_1.SseHub, sse_channel_1.SseChannel],
|
|
31
49
|
};
|
|
50
|
+
}
|
|
51
|
+
static forRootAsync(options) {
|
|
32
52
|
const providers = [
|
|
33
|
-
{
|
|
53
|
+
{
|
|
54
|
+
provide: SSE_RESOLVED_CONFIG,
|
|
55
|
+
useFactory: options.useFactory,
|
|
56
|
+
inject: options.inject ?? [],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
provide: tokens_1.SSE_OPTIONS,
|
|
60
|
+
useFactory: (config) => ({
|
|
61
|
+
event: config.event ?? 'notification',
|
|
62
|
+
}),
|
|
63
|
+
inject: [SSE_RESOLVED_CONFIG],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
provide: tokens_1.SSE_BACKPLANE,
|
|
67
|
+
useFactory: (config) => config.backplane ?? null,
|
|
68
|
+
inject: [SSE_RESOLVED_CONFIG],
|
|
69
|
+
},
|
|
34
70
|
sse_hub_1.SseHub,
|
|
35
71
|
sse_channel_1.SseChannel,
|
|
36
72
|
];
|
|
37
73
|
return {
|
|
38
74
|
module: SseChannelModule_1,
|
|
39
75
|
global: options.global ?? true,
|
|
76
|
+
imports: options.imports ?? [],
|
|
40
77
|
providers,
|
|
41
78
|
exports: [sse_hub_1.SseHub, sse_channel_1.SseChannel],
|
|
42
79
|
};
|
package/dist/sse.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.module.js","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"sse.module.js","sourceRoot":"","sources":["../src/sse.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAMwB;AAExB,+CAAmE;AACnE,uCAAmC;AACnC,qCAAsD;AA6BtD,wDAAwD;AACxD,MAAM,mBAAmB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAC3B,MAAM,CAAC,OAAO,CAAC,UAAmC,EAAE;QAClD,MAAM,cAAc,GAAsB,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,cAAc,EAAE,CAAC;QAErF,OAAO;YACL,MAAM,EAAE,kBAAgB;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;YAC9B,SAAS,EAAE;gBACT,EAAE,OAAO,EAAE,oBAAW,EAAE,QAAQ,EAAE,cAAc,EAAE;gBAClD,EAAE,OAAO,EAAE,sBAAa,EAAE,QAAQ,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,EAAE;gBAC/D,gBAAM;gBACN,wBAAU;aACX;YACD,OAAO,EAAE,CAAC,gBAAM,EAAE,wBAAU,CAAC;SAC9B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,OAAqC;QACvD,MAAM,SAAS,GAAe;YAC5B;gBACE,OAAO,EAAE,mBAAmB;gBAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;aAC7B;YACD;gBACE,OAAO,EAAE,oBAAW;gBACpB,UAAU,EAAE,CAAC,MAA6B,EAAqB,EAAE,CAAC,CAAC;oBACjE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,cAAc;iBACtC,CAAC;gBACF,MAAM,EAAE,CAAC,mBAAmB,CAAC;aAC9B;YACD;gBACE,OAAO,EAAE,sBAAa;gBACtB,UAAU,EAAE,CAAC,MAA6B,EAAuB,EAAE,CACjE,MAAM,CAAC,SAAS,IAAI,IAAI;gBAC1B,MAAM,EAAE,CAAC,mBAAmB,CAAC;aAC9B;YACD,gBAAM;YACN,wBAAU;SACX,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,kBAAgB;YACxB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;YAC9B,SAAS;YACT,OAAO,EAAE,CAAC,gBAAM,EAAE,wBAAU,CAAC;SAC9B,CAAC;IACJ,CAAC;CACF,CAAA;AAjDY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,gBAAgB,CAiD5B"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Type } from '@nestjs/common';
|
|
2
|
+
/** Options for {@link createNotificationsStreamController}. */
|
|
3
|
+
export interface NotificationsStreamControllerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the SSE route value from the request — must match what the notifiable returns from
|
|
6
|
+
* `routeNotificationFor('sse')` (typically the user id). E.g. `(req) => req.user.id`.
|
|
7
|
+
*/
|
|
8
|
+
resolveRoute: (req: any) => string | Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Optional tenant resolver — must match the tenant the notification was sent under, so the stream
|
|
11
|
+
* key lines up with what the SSE channel published to.
|
|
12
|
+
*/
|
|
13
|
+
resolveTenant?: (req: any) => string | undefined | Promise<string | undefined>;
|
|
14
|
+
/** Controller base path. Default `'notifications'`. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Sub-path for the `@Sse()` endpoint. Default `'stream'` → `GET {path}/stream`. */
|
|
17
|
+
streamPath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Keep-alive interval (ms) emitting a `{ type: 'heartbeat' }` event so idle connections survive
|
|
20
|
+
* proxy/load-balancer timeouts. Default `25000`; set `0` to disable.
|
|
21
|
+
*/
|
|
22
|
+
heartbeatMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Builds a `@Controller` exposing the Server-Sent Events stream endpoint for in-app notifications —
|
|
26
|
+
* the consumer-side counterpart the SSE channel publishes to. Mounts a native `@Sse()` route that
|
|
27
|
+
* subscribes to {@link SseHub} under the same key the channel uses (`sseKey(tenant, routeValue)`),
|
|
28
|
+
* so apps don't hand-write the streaming endpoint.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const NotificationsStreamController = createNotificationsStreamController({
|
|
32
|
+
* resolveRoute: (req) => String(req.user.id),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* @Module({ controllers: [NotificationsStreamController] })
|
|
36
|
+
* export class InboxModule {}
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Requires `SseChannelModule` (which provides {@link SseHub}) in scope. Pair with a cross-pod
|
|
40
|
+
* `backplane` so a publish on any node reaches connections held by another.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createNotificationsStreamController(options: NotificationsStreamControllerOptions): Type<unknown>;
|
|
43
|
+
//# sourceMappingURL=stream-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-controller.d.ts","sourceRoot":"","sources":["../src/stream-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAKpF,+DAA+D;AAC/D,MAAM,WAAW,oCAAoC;IACnD;;;OAGG;IACH,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/E,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,oCAAoC,GAC5C,IAAI,CAAC,OAAO,CAAC,CA0Bf"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
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
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.createNotificationsStreamController = createNotificationsStreamController;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const rxjs_1 = require("rxjs");
|
|
18
|
+
const sse_key_1 = require("./sse-key");
|
|
19
|
+
const sse_hub_1 = require("./sse.hub");
|
|
20
|
+
/**
|
|
21
|
+
* Builds a `@Controller` exposing the Server-Sent Events stream endpoint for in-app notifications —
|
|
22
|
+
* the consumer-side counterpart the SSE channel publishes to. Mounts a native `@Sse()` route that
|
|
23
|
+
* subscribes to {@link SseHub} under the same key the channel uses (`sseKey(tenant, routeValue)`),
|
|
24
|
+
* so apps don't hand-write the streaming endpoint.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const NotificationsStreamController = createNotificationsStreamController({
|
|
28
|
+
* resolveRoute: (req) => String(req.user.id),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* @Module({ controllers: [NotificationsStreamController] })
|
|
32
|
+
* export class InboxModule {}
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Requires `SseChannelModule` (which provides {@link SseHub}) in scope. Pair with a cross-pod
|
|
36
|
+
* `backplane` so a publish on any node reaches connections held by another.
|
|
37
|
+
*/
|
|
38
|
+
function createNotificationsStreamController(options) {
|
|
39
|
+
const heartbeatMs = options.heartbeatMs ?? 25_000;
|
|
40
|
+
let NotificationsStreamController = class NotificationsStreamController {
|
|
41
|
+
hub;
|
|
42
|
+
constructor(hub) {
|
|
43
|
+
this.hub = hub;
|
|
44
|
+
}
|
|
45
|
+
stream(req) {
|
|
46
|
+
const key$ = (0, rxjs_1.from)((async () => {
|
|
47
|
+
const route = await options.resolveRoute(req);
|
|
48
|
+
const tenant = options.resolveTenant ? await options.resolveTenant(req) : undefined;
|
|
49
|
+
return (0, sse_key_1.sseKey)(tenant, route);
|
|
50
|
+
})());
|
|
51
|
+
const events$ = key$.pipe((0, rxjs_1.switchMap)((key) => this.hub.stream(key)));
|
|
52
|
+
if (!heartbeatMs)
|
|
53
|
+
return events$;
|
|
54
|
+
const heartbeat$ = (0, rxjs_1.interval)(heartbeatMs).pipe((0, rxjs_1.map)(() => ({ data: '', type: 'heartbeat' })));
|
|
55
|
+
return (0, rxjs_1.merge)(events$, heartbeat$);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
__decorate([
|
|
59
|
+
(0, common_1.Sse)(options.streamPath ?? 'stream'),
|
|
60
|
+
__param(0, (0, common_1.Req)()),
|
|
61
|
+
__metadata("design:type", Function),
|
|
62
|
+
__metadata("design:paramtypes", [Object]),
|
|
63
|
+
__metadata("design:returntype", Function)
|
|
64
|
+
], NotificationsStreamController.prototype, "stream", null);
|
|
65
|
+
NotificationsStreamController = __decorate([
|
|
66
|
+
(0, common_1.Controller)(options.path ?? 'notifications'),
|
|
67
|
+
__metadata("design:paramtypes", [sse_hub_1.SseHub])
|
|
68
|
+
], NotificationsStreamController);
|
|
69
|
+
return NotificationsStreamController;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=stream-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-controller.js","sourceRoot":"","sources":["../src/stream-controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AA8CA,kFA4BC;AA1ED,2CAAoF;AACpF,+BAA8E;AAC9E,uCAAmC;AACnC,uCAAmC;AAyBnC;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,mCAAmC,CACjD,OAA6C;IAE7C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC;IAElD,IACM,6BAA6B,GADnC,MACM,6BAA6B;QACJ;QAA7B,YAA6B,GAAW;YAAX,QAAG,GAAH,GAAG,CAAQ;QAAG,CAAC;QAG5C,MAAM,CAAQ,GAAY;YACxB,MAAM,IAAI,GAAG,IAAA,WAAI,EACf,CAAC,KAAK,IAAI,EAAE;gBACV,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;gBAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACpF,OAAO,IAAA,gBAAM,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAC/B,CAAC,CAAC,EAAE,CACL,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAA,gBAAS,EAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACpE,IAAI,CAAC,WAAW;gBAAE,OAAO,OAAO,CAAC;YACjC,MAAM,UAAU,GAAG,IAAA,eAAQ,EAAC,WAAW,CAAC,CAAC,IAAI,CAC3C,IAAA,UAAG,EAAC,GAAiB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAC3D,CAAC;YACF,OAAO,IAAA,YAAK,EAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACpC,CAAC;KACF,CAAA;IAfC;QADC,IAAA,YAAG,EAAC,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC;QAC5B,WAAA,IAAA,YAAG,GAAE,CAAA;;;;+DAcZ;IAlBG,6BAA6B;QADlC,IAAA,mBAAU,EAAC,OAAO,CAAC,IAAI,IAAI,eAAe,CAAC;yCAER,gBAAM;OADpC,6BAA6B,CAmBlC;IAED,OAAO,6BAA6B,CAAC;AACvC,CAAC"}
|
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
|
package/dist/tokens.d.ts.map
CHANGED
|
@@ -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
|
package/dist/tokens.js.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.4.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": "
|
|
25
|
-
"reflect-metadata": "
|
|
26
|
-
"rxjs": "
|
|
27
|
-
"typescript": "
|
|
28
|
-
"@dudousxd/nestjs-notifications-core": "^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",
|