@nextage/nx-frame-be 1.0.50 → 1.0.52

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,145 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const constants_1 = require("../../constants");
13
+ const constants_2 = require("../../constants");
14
+ const base_controller_1 = require("../base.controller");
15
+ const pubsub_manager_1 = require("../../manager/pubsub-manager");
16
+ const enums_1 = require("../../enums");
17
+ const coded_entity_model_1 = require("../../models/coded-entity.model");
18
+ /**
19
+ * DEFERRED CRUD EMISSIONS - what a transaction is allowed to announce, and when.
20
+ *
21
+ * The harness runs a STANDALONE mongodb-memory-server, so the real transactional path cannot be
22
+ * reached here: `supportsTransactions()` answers false and `withTransaction` degrades. These are
23
+ * therefore UNIT tests of the queue itself - detection and the session are stubbed, and what is
24
+ * asserted is the contract the transactional path must keep:
25
+ *
26
+ * nothing leaves before the commit · everything leaves after it · an abort leaves nothing ·
27
+ * a retried transaction emits once · the subscriber never receives the session.
28
+ *
29
+ * The end-to-end proof, against a REAL replica set, lives in the consumer (D1).
30
+ */
31
+ class CeController extends base_controller_1.BaseController {
32
+ }
33
+ const ctrl = new CeController({ model: coded_entity_model_1.ceModel });
34
+ /** A session that records what was asked of it and never talks to a server. */
35
+ function fakeSession(behaviour) {
36
+ let attempts = 0;
37
+ return {
38
+ ended: false,
39
+ attempts: () => attempts,
40
+ withTransaction: (fn) => __awaiter(this, void 0, void 0, function* () {
41
+ attempts++;
42
+ if (behaviour === 'abort') {
43
+ yield fn();
44
+ throw new Error('deliberate abort');
45
+ }
46
+ if (behaviour === 'retry-once' && attempts === 1) {
47
+ yield fn(); // first attempt does its work...
48
+ attempts++;
49
+ return fn(); // ...and is then re-run, as a transient error would cause
50
+ }
51
+ return fn();
52
+ }),
53
+ endSession: function () {
54
+ return __awaiter(this, void 0, void 0, function* () { this.ended = true; });
55
+ }
56
+ };
57
+ }
58
+ describe('withTransaction - emissions wait for the commit', () => {
59
+ let published;
60
+ let session;
61
+ beforeEach(() => {
62
+ published = [];
63
+ constants_1.APP.nxPubSub = {
64
+ publish: (key, data, context) => { published.push({ key, data, context }); }
65
+ };
66
+ jest.spyOn(ctrl, 'supportsTransactions').mockResolvedValue(true);
67
+ });
68
+ afterEach(() => jest.restoreAllMocks());
69
+ const startWith = (behaviour) => {
70
+ session = fakeSession(behaviour);
71
+ jest.spyOn(coded_entity_model_1.ceModel.mgModel, 'startSession').mockResolvedValue(session);
72
+ };
73
+ it('holds an emission back until the transaction has committed', () => __awaiter(void 0, void 0, void 0, function* () {
74
+ startWith('commit');
75
+ let seenDuring = -1;
76
+ yield ctrl.withTransaction({ i18n: 'en' }, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
77
+ coded_entity_model_1.ceModel.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: 'x1', model: 'ce' } }, ctx);
78
+ seenDuring = published.length;
79
+ }));
80
+ expect(seenDuring).toBe(0); // nothing announced while the write is uncommitted
81
+ expect(published).toHaveLength(1); // and exactly one announcement after the commit
82
+ expect(published[0].key).toBe(constants_2.MODEL_CRUD_EVENT);
83
+ expect(published[0].data.data.uid).toBe('x1');
84
+ }));
85
+ it('hands the subscriber a context without the session and without the queue', () => __awaiter(void 0, void 0, void 0, function* () {
86
+ startWith('commit');
87
+ yield ctrl.withTransaction({ i18n: 'en' }, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
88
+ expect(ctx.session).toBeDefined(); // the WRITE is in the transaction...
89
+ expect(ctx.deferredEvents).toBeDefined();
90
+ coded_entity_model_1.ceModel.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: 'x2', model: 'ce' } }, ctx);
91
+ }));
92
+ // ...but the SUBSCRIBER is not: it would be joining a transaction it cannot see the end of.
93
+ expect(published[0].context.session).toBeUndefined();
94
+ expect(published[0].context.deferredEvents).toBeUndefined();
95
+ expect(published[0].context.i18n).toBe('en'); // the rest of the context survives
96
+ }));
97
+ it('announces nothing when the transaction aborts', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ startWith('abort');
99
+ yield expect(ctrl.withTransaction({}, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
100
+ coded_entity_model_1.ceModel.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: 'x3', model: 'ce' } }, ctx);
101
+ }))).rejects.toThrow('deliberate abort');
102
+ expect(published).toHaveLength(0); // the write never happened: announcing it would be a lie
103
+ }));
104
+ it('announces once when the transaction is re-run on a transient error', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ startWith('retry-once');
106
+ yield ctrl.withTransaction({}, (ctx) => __awaiter(void 0, void 0, void 0, function* () {
107
+ coded_entity_model_1.ceModel.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: 'x4', model: 'ce' } }, ctx);
108
+ }));
109
+ expect(session.attempts()).toBeGreaterThan(1); // the callback really did run twice
110
+ expect(published).toHaveLength(1); // and the queue kept only the last attempt
111
+ }));
112
+ it('publishes immediately - and without the session - when there is no transaction', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ jest.spyOn(ctrl, 'supportsTransactions').mockResolvedValue(false);
114
+ const handMadeSession = { id: 'by-hand' };
115
+ coded_entity_model_1.ceModel.publishCRUDEvent({ type: enums_1.ModelCrudEventType.create, data: { uid: 'x5', model: 'ce' } }, { i18n: 'it', session: handMadeSession });
116
+ expect(published).toHaveLength(1);
117
+ expect(published[0].context.session).toBeUndefined();
118
+ expect(published[0].context.i18n).toBe('it');
119
+ }));
120
+ });
121
+ describe('PubSubManager - a failing subscriber cannot take the process down', () => {
122
+ it('handles the rejection of an async subscriber instead of leaving it unhandled', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ const pubSub = new pubsub_manager_1.PubSubManager();
124
+ const seen = [];
125
+ const onUnhandled = (err) => seen.push(err);
126
+ process.on('unhandledRejection', onUnhandled);
127
+ const id = pubSub.subscribe('probe.key', () => __awaiter(void 0, void 0, void 0, function* () { throw new Error('subscriber failed'); }));
128
+ pubSub.publish('probe.key', { any: 'payload' }, {});
129
+ // Let the microtask queue run: an unhandled rejection would be reported by now.
130
+ yield new Promise(resolve => setImmediate(resolve));
131
+ yield new Promise(resolve => setImmediate(resolve));
132
+ process.off('unhandledRejection', onUnhandled);
133
+ expect(seen).toHaveLength(0);
134
+ pubSub.unsubscribe(id);
135
+ }));
136
+ it('really detaches the subscriber it registered', () => __awaiter(void 0, void 0, void 0, function* () {
137
+ const pubSub = new pubsub_manager_1.PubSubManager();
138
+ let calls = 0;
139
+ const id = pubSub.subscribe('probe.detach', () => { calls++; });
140
+ pubSub.publish('probe.detach', {}, {});
141
+ pubSub.unsubscribe(id);
142
+ pubSub.publish('probe.detach', {}, {});
143
+ expect(calls).toBe(1); // the second publish must not reach a detached subscriber
144
+ }));
145
+ });
@@ -146,8 +146,33 @@ export declare abstract class BaseController<TAttrs, TDoc extends BaseMongoDoc,
146
146
  * - `session.withTransaction` may re-run `fn` on transient errors, so `fn` MUST
147
147
  * keep non-DB side effects (e.g. publishing) OUTSIDE the transaction.
148
148
  *
149
+ * DEFERRED EMISSIONS. A CRUD publication asked for INSIDE the transaction is held back and
150
+ * released only after the commit. Without this the subscribers were handed a context carrying
151
+ * the live session, their work was never awaited by the caller, and whatever they had in
152
+ * flight met that session the moment `withTransaction` committed and `endSession` closed it -
153
+ * a rejection nobody was listening for, which is an uncaught rejection and, with the usual
154
+ * process handler, a shutdown. Queueing also makes the emission mean what it says: an event
155
+ * announcing a write that the database has not yet accepted is a lie the subscribers cannot
156
+ * detect, and on an abort it would announce a write that never happened.
157
+ *
149
158
  * @param context caller context (may already carry a session)
150
159
  * @param fn work to run; receives the context to pass down to model ops
151
160
  */
152
161
  withTransaction<R>(context: NxContext, fn: (ctx: NxContext) => Promise<R>): Promise<R>;
162
+ /**
163
+ * Releases the emissions a committed transaction held back.
164
+ *
165
+ * Two properties, both deliberate:
166
+ *
167
+ * - the subscribers are handed a context WITHOUT the session (and without the queue). The
168
+ * transaction is over; a subscriber that joined it would be writing into a session that is
169
+ * about to be closed, which is the failure this whole mechanism removes;
170
+ * - a subscriber that throws SYNCHRONOUSLY cannot stop the flush or fail the caller's write,
171
+ * which has already committed. It is logged and the remaining emissions still go out. An
172
+ * asynchronous rejection is guarded one level down, where the subscriber is invoked
173
+ * (`PubSubManager.subscribe`), because that is the only place that can see the promise.
174
+ *
175
+ * @param deferred the queue opened for the transaction that has just committed
176
+ */
177
+ private flushDeferredEvents;
153
178
  }
@@ -8,8 +8,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  Object.defineProperty(exports, "__esModule", { value: true });
12
23
  exports.BaseController = void 0;
24
+ const constants_1 = require("../constants");
13
25
  /**
14
26
  * Merges the options the PARSE decided with the ones the CALLER asked for, leaving both untouched.
15
27
  *
@@ -226,23 +238,42 @@ class BaseController {
226
238
  * - `session.withTransaction` may re-run `fn` on transient errors, so `fn` MUST
227
239
  * keep non-DB side effects (e.g. publishing) OUTSIDE the transaction.
228
240
  *
241
+ * DEFERRED EMISSIONS. A CRUD publication asked for INSIDE the transaction is held back and
242
+ * released only after the commit. Without this the subscribers were handed a context carrying
243
+ * the live session, their work was never awaited by the caller, and whatever they had in
244
+ * flight met that session the moment `withTransaction` committed and `endSession` closed it -
245
+ * a rejection nobody was listening for, which is an uncaught rejection and, with the usual
246
+ * process handler, a shutdown. Queueing also makes the emission mean what it says: an event
247
+ * announcing a write that the database has not yet accepted is a lie the subscribers cannot
248
+ * detect, and on an abort it would announce a write that never happened.
249
+ *
229
250
  * @param context caller context (may already carry a session)
230
251
  * @param fn work to run; receives the context to pass down to model ops
231
252
  */
232
253
  withTransaction(context, fn) {
233
254
  return __awaiter(this, void 0, void 0, function* () {
234
- // already inside a transaction → reuse it (nested call, no new session)
255
+ // already inside a transaction → reuse it (nested call, no new session). The queue of the
256
+ // OUTER call travels with the context, so an inner emission is released by the outer commit.
235
257
  if (context === null || context === void 0 ? void 0 : context.session)
236
258
  return fn(context);
237
- // standalone deployment → no atomicity available, run as today
259
+ // standalone deployment → no atomicity available, run as today. Nothing to wait for either:
260
+ // with no transaction there is no commit to defer to, so emissions leave immediately.
238
261
  if (!(yield this.supportsTransactions()))
239
262
  return fn(context);
240
263
  const session = yield this.model.mgModel.startSession();
264
+ const deferred = [];
241
265
  try {
242
266
  let result;
243
267
  yield session.withTransaction(() => __awaiter(this, void 0, void 0, function* () {
244
- result = yield fn(Object.assign(Object.assign({}, context), { session }));
268
+ // `withTransaction` MAY RE-RUN this callback on a transient error. Anything queued by a
269
+ // previous attempt belongs to a transaction that did not commit, so it is dropped here
270
+ // rather than emitted twice.
271
+ deferred.length = 0;
272
+ result = yield fn(Object.assign(Object.assign({}, context), { session, deferredEvents: deferred }));
245
273
  }));
274
+ // Committed, and only now. An abort never reaches this line: the throw propagates and the
275
+ // queue is discarded with the stack, which is exactly the intended behaviour.
276
+ this.flushDeferredEvents(deferred);
246
277
  return result;
247
278
  }
248
279
  finally {
@@ -250,5 +281,33 @@ class BaseController {
250
281
  }
251
282
  });
252
283
  }
284
+ /**
285
+ * Releases the emissions a committed transaction held back.
286
+ *
287
+ * Two properties, both deliberate:
288
+ *
289
+ * - the subscribers are handed a context WITHOUT the session (and without the queue). The
290
+ * transaction is over; a subscriber that joined it would be writing into a session that is
291
+ * about to be closed, which is the failure this whole mechanism removes;
292
+ * - a subscriber that throws SYNCHRONOUSLY cannot stop the flush or fail the caller's write,
293
+ * which has already committed. It is logged and the remaining emissions still go out. An
294
+ * asynchronous rejection is guarded one level down, where the subscriber is invoked
295
+ * (`PubSubManager.subscribe`), because that is the only place that can see the promise.
296
+ *
297
+ * @param deferred the queue opened for the transaction that has just committed
298
+ */
299
+ flushDeferredEvents(deferred) {
300
+ var _a, _b;
301
+ for (const ev of deferred) {
302
+ const _c = ev.context, { session, deferredEvents } = _c, ctx = __rest(_c, ["session", "deferredEvents"]);
303
+ try {
304
+ (_a = constants_1.APP.nxPubSub) === null || _a === void 0 ? void 0 : _a.publish(ev.key, ev.data, ctx);
305
+ }
306
+ catch (err) {
307
+ (_b = constants_1.APP.logger) === null || _b === void 0 ? void 0 : _b.error(`Deferred event [${ev.key}] failed to publish after commit: ${err}`);
308
+ }
309
+ }
310
+ deferred.length = 0;
311
+ }
253
312
  }
254
313
  exports.BaseController = BaseController;
@@ -85,11 +85,10 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
85
85
  aggregateNativeFiltered(pipeline: mongoose.PipelineStage[], params: FilterParams | undefined, context: NxContext): Promise<mongoose.mongo.AggregationCursor<any> | null>;
86
86
  /**
87
87
  *
88
- * @param {*} pipeline
89
- * @param {*} params
90
- * @param {*} context
88
+ * @param pipeline
89
+ * @param options
91
90
  */
92
- aggregateNative(pipeline: mongoose.PipelineStage[]): mongoose.mongo.AggregationCursor<any>;
91
+ aggregateNative(pipeline: mongoose.PipelineStage[], options?: mongoose.mongo.AggregateOptions): mongoose.mongo.AggregationCursor<any>;
93
92
  /**
94
93
  * Simple Find wrapper
95
94
  * @param {*} params
@@ -442,9 +441,19 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
442
441
  */
443
442
  bulkRemove(ids: string, context: NxContext): Promise<TDoc[]>;
444
443
  /**
444
+ * Announce a CRUD write to the in-process subscribers.
445
445
  *
446
- * @param {*} evt
447
- * @param {*} context
446
+ * WHEN A TRANSACTION IS OPEN THE EMISSION IS QUEUED, not published: the context carries the
447
+ * queue that `BaseController.withTransaction` opened, and the flush happens after the commit.
448
+ * Publishing here would announce a write the database has not accepted yet, and would hand the
449
+ * subscribers a live session that the caller is about to close.
450
+ *
451
+ * OUTSIDE a transaction the emission goes out immediately, as it always has - but the session,
452
+ * if the caller put one on the context by hand, is REMOVED first. A subscriber must never join
453
+ * a transaction whose lifetime it does not control and whose end it cannot see.
454
+ *
455
+ * @param evt the CRUD event to announce
456
+ * @param context the context of the write; decides whether this waits for a commit
448
457
  */
449
458
  publishCRUDEvent(evt: CrudItemEvent, context?: NxContext): void;
450
459
  /**
@@ -175,12 +175,11 @@ class BaseModel extends events_1.EventEmitter {
175
175
  }
176
176
  /**
177
177
  *
178
- * @param {*} pipeline
179
- * @param {*} params
180
- * @param {*} context
178
+ * @param pipeline
179
+ * @param options
181
180
  */
182
- aggregateNative(pipeline) {
183
- return this.mgModel.collection.aggregate(pipeline, { allowDiskUse: true });
181
+ aggregateNative(pipeline, options = {}) {
182
+ return this.mgModel.collection.aggregate(pipeline, Object.assign({ allowDiskUse: true }, options));
184
183
  }
185
184
  /**
186
185
  * Simple Find wrapper
@@ -928,9 +927,19 @@ class BaseModel extends events_1.EventEmitter {
928
927
  return Promise.all(promises);
929
928
  }
930
929
  /**
930
+ * Announce a CRUD write to the in-process subscribers.
931
931
  *
932
- * @param {*} evt
933
- * @param {*} context
932
+ * WHEN A TRANSACTION IS OPEN THE EMISSION IS QUEUED, not published: the context carries the
933
+ * queue that `BaseController.withTransaction` opened, and the flush happens after the commit.
934
+ * Publishing here would announce a write the database has not accepted yet, and would hand the
935
+ * subscribers a live session that the caller is about to close.
936
+ *
937
+ * OUTSIDE a transaction the emission goes out immediately, as it always has - but the session,
938
+ * if the caller put one on the context by hand, is REMOVED first. A subscriber must never join
939
+ * a transaction whose lifetime it does not control and whose end it cannot see.
940
+ *
941
+ * @param evt the CRUD event to announce
942
+ * @param context the context of the write; decides whether this waits for a commit
934
943
  */
935
944
  publishCRUDEvent(evt, context = {}) {
936
945
  var _a;
@@ -938,7 +947,12 @@ class BaseModel extends events_1.EventEmitter {
938
947
  return;
939
948
  if (context === null || context === void 0 ? void 0 : context.evtType)
940
949
  evt.type = context.evtType;
941
- (_a = constants_1.APP.nxPubSub) === null || _a === void 0 ? void 0 : _a.publish(constants_1.MODEL_CRUD_EVENT, evt, context);
950
+ if (context === null || context === void 0 ? void 0 : context.deferredEvents) {
951
+ context.deferredEvents.push({ key: constants_1.MODEL_CRUD_EVENT, data: evt, context });
952
+ return;
953
+ }
954
+ const { session, deferredEvents } = context, ctx = __rest(context, ["session", "deferredEvents"]);
955
+ (_a = constants_1.APP.nxPubSub) === null || _a === void 0 ? void 0 : _a.publish(constants_1.MODEL_CRUD_EVENT, evt, ctx);
942
956
  }
943
957
  /**
944
958
  *
@@ -12,6 +12,26 @@ export interface NxContext {
12
12
  res?: Response;
13
13
  i18n?: string;
14
14
  session?: ClientSession;
15
+ /**
16
+ * Emissions held back until the transaction that produced them has COMMITTED.
17
+ *
18
+ * Opened by `BaseController.withTransaction` on the child context and, from there, carried
19
+ * down to every model op. Its presence is the signal that a publication must wait: see
20
+ * `BaseModel.publishCRUDEvent`. It is transport-agnostic bookkeeping, not application data,
21
+ * and it never reaches a subscriber - the flush strips it along with the session.
22
+ */
23
+ deferredEvents?: DeferredEvent[];
24
+ }
25
+ /**
26
+ * One publication, captured at the moment it was asked for and released after the commit.
27
+ *
28
+ * The context is kept as it was so the subscriber still sees the user, the language and the
29
+ * rest of it; only `session` and `deferredEvents` are removed on the way out.
30
+ */
31
+ export interface DeferredEvent {
32
+ key: string;
33
+ data: any;
34
+ context: NxContext;
15
35
  }
16
36
  export interface NxJwtData extends NxObject {
17
37
  rls?: string[];
@@ -1,9 +1,20 @@
1
1
  import { NxContext } from '../interfaces';
2
2
  export declare class PubSubManager {
3
3
  /**
4
+ * Register `method` for `key`.
4
5
  *
5
- * @param {*} key
6
- * @param {*} method
6
+ * THE SUBSCRIBER IS WRAPPED, for one reason: `EventEmitter.emit` calls a listener and throws
7
+ * its return value away. An async subscriber therefore leaves a promise nobody holds, and if
8
+ * it rejects the process sees an unhandled rejection - which, with the usual process-level
9
+ * handler, takes the service down over a failure in one listener. The wrapper is the only
10
+ * place that can see that promise, so it is the only place the rejection can be handled.
11
+ *
12
+ * A failing subscriber is logged and the publication continues: a publisher never learns that
13
+ * a listener failed, and must not, because the write it announced has already happened.
14
+ *
15
+ * @param key the event name
16
+ * @param method the subscriber; ignored when it is not a function
17
+ * @returns the subscription id, to be passed back to `unsubscribe`
7
18
  */
8
19
  subscribe(key: string, method: Function): string;
9
20
  /**
@@ -2,21 +2,49 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PubSubManager = void 0;
4
4
  const events_1 = require("events");
5
+ const constants_1 = require("../constants");
5
6
  const utils_1 = require("../utils");
6
7
  const subs = {};
7
8
  const eventsEmitter = new events_1.EventEmitter();
8
9
  class PubSubManager {
9
10
  /**
11
+ * Register `method` for `key`.
10
12
  *
11
- * @param {*} key
12
- * @param {*} method
13
+ * THE SUBSCRIBER IS WRAPPED, for one reason: `EventEmitter.emit` calls a listener and throws
14
+ * its return value away. An async subscriber therefore leaves a promise nobody holds, and if
15
+ * it rejects the process sees an unhandled rejection - which, with the usual process-level
16
+ * handler, takes the service down over a failure in one listener. The wrapper is the only
17
+ * place that can see that promise, so it is the only place the rejection can be handled.
18
+ *
19
+ * A failing subscriber is logged and the publication continues: a publisher never learns that
20
+ * a listener failed, and must not, because the write it announced has already happened.
21
+ *
22
+ * @param key the event name
23
+ * @param method the subscriber; ignored when it is not a function
24
+ * @returns the subscription id, to be passed back to `unsubscribe`
13
25
  */
14
26
  subscribe(key, method) {
15
27
  if (!(0, utils_1.isFunction)(method))
16
28
  return '';
17
29
  const id = (0, utils_1.uuid)();
18
- subs[id] = { key, method };
19
- eventsEmitter.on(key, (data, context) => method(data, context));
30
+ const guarded = (data, context) => {
31
+ var _a;
32
+ try {
33
+ const result = method(data, context);
34
+ if (result && typeof result.then === 'function')
35
+ result.catch((err) => {
36
+ var _a;
37
+ (_a = constants_1.APP.logger) === null || _a === void 0 ? void 0 : _a.error(`Subscriber of [${key}] rejected: ${err}`);
38
+ });
39
+ }
40
+ catch (err) {
41
+ (_a = constants_1.APP.logger) === null || _a === void 0 ? void 0 : _a.error(`Subscriber of [${key}] threw: ${err}`);
42
+ }
43
+ };
44
+ // The GUARDED function is what gets registered, so it is also what `unsubscribe` must
45
+ // remove: `off` compares by reference and would not find the original `method`.
46
+ subs[id] = { key, method, guarded };
47
+ eventsEmitter.on(key, guarded);
20
48
  return id;
21
49
  }
22
50
  /**
@@ -32,7 +60,7 @@ class PubSubManager {
32
60
  * @param {*} id
33
61
  */
34
62
  unsubscribe(id) {
35
- eventsEmitter.off(subs[id].key, subs[id].method);
63
+ eventsEmitter.off(subs[id].key, subs[id].guarded);
36
64
  }
37
65
  }
38
66
  exports.PubSubManager = PubSubManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",