@fluojs/notifications 1.0.3 → 2.0.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/service.js CHANGED
@@ -5,7 +5,8 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
7
  import { Inject } from '@fluojs/core';
8
- import { NotificationChannelNotFoundError, NotificationQueueNotConfiguredError } from './errors.js';
8
+ import { NotificationChannelNotFoundError, NotificationQueueNotConfiguredError, NotificationQueueResultIntegrityError } from './errors.js';
9
+ import { createNotificationDispatchSnapshot, createNotificationLifecycleEventSnapshot } from './snapshots.js';
9
10
  import { createNotificationsPlatformStatusSnapshot } from './status.js';
10
11
  import { NOTIFICATION_CHANNELS, NOTIFICATIONS_OPTIONS } from './tokens.js';
11
12
  let _NotificationsService;
@@ -38,6 +39,7 @@ class NotificationsService {
38
39
  * @returns A normalized dispatch result describing direct vs queued delivery.
39
40
  * @throws {NotificationChannelNotFoundError} When no registered channel matches `notification.channel`.
40
41
  * @throws {NotificationQueueNotConfiguredError} When queue delivery is requested without a queue adapter.
42
+ * @throws {NotificationQueueResultIntegrityError} When a queue adapter returns an invalid delivery identifier.
41
43
  *
42
44
  * @example
43
45
  * ```ts
@@ -50,52 +52,60 @@ class NotificationsService {
50
52
  * ```
51
53
  */
52
54
  async dispatch(notification, options = {}) {
53
- const requestedPublicationError = await this.publishLifecycleEventBestEffort('notification.dispatch.requested', notification, options);
55
+ const dispatchNotification = createNotificationDispatchSnapshot(notification);
56
+ return this.dispatchAdmitted(dispatchNotification, options);
57
+ }
58
+ async dispatchAdmitted(dispatchNotification, options) {
59
+ const requestedPublicationError = await this.publishLifecycleEventBestEffort('notification.dispatch.requested', dispatchNotification, options);
54
60
  if (this.shouldQueueSingleDispatch(options)) {
55
61
  try {
56
- this.requireChannel(notification.channel);
62
+ this.requireChannel(dispatchNotification.channel);
57
63
  } catch (error) {
58
- await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
64
+ await this.publishFailureLifecycleEvent(dispatchNotification, options, error, requestedPublicationError);
59
65
  throw error;
60
66
  }
61
- const job = this.createQueueJob(notification);
67
+ const job = this.createQueueJob(dispatchNotification);
62
68
  try {
63
- const deliveryId = await this.requireQueueAdapter().enqueue(job);
69
+ const queue = this.requireQueueAdapter();
70
+ throwIfAborted(options.signal);
71
+ const deliveryId = validateQueueDeliveryId(await queue.enqueue(job, {
72
+ signal: options.signal
73
+ }));
64
74
  const result = {
65
- channel: notification.channel,
66
- deliveryId: this.normalizeDeliveryId(deliveryId, notification),
75
+ channel: dispatchNotification.channel,
76
+ deliveryId,
67
77
  queued: true,
68
78
  status: 'queued'
69
79
  };
70
- await this.publishLifecycleEventBestEffort('notification.dispatch.queued', notification, options, result.deliveryId);
80
+ await this.publishLifecycleEventBestEffort('notification.dispatch.queued', dispatchNotification, options, result.deliveryId);
71
81
  return result;
72
82
  } catch (error) {
73
- await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
83
+ await this.publishFailureLifecycleEvent(dispatchNotification, options, error, requestedPublicationError);
74
84
  throw error;
75
85
  }
76
86
  }
77
87
  let channel;
78
88
  try {
79
- channel = this.requireChannel(notification.channel);
89
+ channel = this.requireChannel(dispatchNotification.channel);
80
90
  } catch (error) {
81
- await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
91
+ await this.publishFailureLifecycleEvent(dispatchNotification, options, error, requestedPublicationError);
82
92
  throw error;
83
93
  }
84
94
  try {
85
- const delivery = await channel.send(notification, {
95
+ const delivery = await channel.send(dispatchNotification, {
86
96
  signal: options.signal
87
97
  });
88
98
  const result = {
89
- channel: notification.channel,
90
- deliveryId: this.normalizeDeliveryId(delivery.externalId, notification),
99
+ channel: dispatchNotification.channel,
100
+ deliveryId: this.normalizeDeliveryId(delivery.externalId, dispatchNotification),
91
101
  metadata: delivery.metadata,
92
102
  queued: delivery.status === 'queued',
93
103
  status: delivery.status ?? 'delivered'
94
104
  };
95
- await this.publishLifecycleEventBestEffort(result.queued ? 'notification.dispatch.queued' : 'notification.dispatch.delivered', notification, options, result.deliveryId);
105
+ await this.publishLifecycleEventBestEffort(result.queued ? 'notification.dispatch.queued' : 'notification.dispatch.delivered', dispatchNotification, options, result.deliveryId);
96
106
  return result;
97
107
  } catch (error) {
98
- await this.publishFailureLifecycleEvent(notification, options, error, requestedPublicationError);
108
+ await this.publishFailureLifecycleEvent(dispatchNotification, options, error, requestedPublicationError);
99
109
  throw error;
100
110
  }
101
111
  }
@@ -108,6 +118,7 @@ class NotificationsService {
108
118
  * @param options Optional queue preference and tolerant error-handling controls.
109
119
  * @returns A batch summary containing successes and captured failures.
110
120
  * @throws {NotificationQueueNotConfiguredError} When queue-backed bulk delivery is requested without a queue adapter.
121
+ * @throws {NotificationQueueResultIntegrityError} When a queue adapter returns invalid delivery identifiers.
111
122
  */
112
123
  async dispatchMany(notifications, options = {}) {
113
124
  if (notifications.length === 0) {
@@ -119,42 +130,53 @@ class NotificationsService {
119
130
  succeeded: 0
120
131
  };
121
132
  }
133
+ const dispatchNotifications = notifications.map(notification => createNotificationDispatchSnapshot(notification));
122
134
  if (this.shouldQueue(notifications.length, options)) {
123
- const requestedPublicationErrors = await this.publishRequestedLifecycleEvents(notifications, options);
135
+ const requestedPublicationErrors = await this.publishRequestedLifecycleEvents(dispatchNotifications, options);
124
136
  let queue;
125
137
  try {
126
138
  queue = this.requireQueueAdapter();
127
139
  } catch (error) {
128
- await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
140
+ await this.publishFailureLifecycleEvents(dispatchNotifications, options, error, requestedPublicationErrors);
129
141
  throw error;
130
142
  }
131
143
  try {
132
- for (const notification of notifications) {
144
+ for (const notification of dispatchNotifications) {
133
145
  this.requireChannel(notification.channel);
134
146
  }
135
147
  } catch (error) {
136
- await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
148
+ await this.publishFailureLifecycleEvents(dispatchNotifications, options, error, requestedPublicationErrors);
137
149
  throw error;
138
150
  }
139
- const jobs = notifications.map(notification => this.createQueueJob(notification));
151
+ const jobs = dispatchNotifications.map(notification => this.createQueueJob(notification));
140
152
  if (!queue.enqueueMany) {
141
- return this.dispatchManyThroughSequentialQueueFallback(notifications, jobs, options, requestedPublicationErrors);
153
+ return this.dispatchManyThroughSequentialQueueFallback(dispatchNotifications, jobs, options, requestedPublicationErrors);
142
154
  }
143
- let ids;
155
+ const admittedJobCount = jobs.length;
156
+ let results;
144
157
  try {
145
- ids = validateQueueBatchDeliveryIds(await queue.enqueueMany(jobs), jobs.length);
158
+ throwIfAborted(options.signal);
159
+ const ids = validateQueueBatchDeliveryIds(await queue.enqueueMany(jobs, {
160
+ signal: options.signal
161
+ }), admittedJobCount);
162
+ results = dispatchNotifications.map((notification, index) => {
163
+ const deliveryId = ids[index];
164
+ if (deliveryId === undefined) {
165
+ throw createQueueResultIntegrityError('enqueueMany', `queue id at index ${index} must be present`);
166
+ }
167
+ return {
168
+ channel: notification.channel,
169
+ deliveryId,
170
+ queued: true,
171
+ status: 'queued'
172
+ };
173
+ });
146
174
  } catch (error) {
147
- await this.publishFailureLifecycleEvents(notifications, options, error, requestedPublicationErrors);
175
+ await this.publishFailureLifecycleEvents(dispatchNotifications, options, error, requestedPublicationErrors);
148
176
  throw error;
149
177
  }
150
- const results = notifications.map((notification, index) => ({
151
- channel: notification.channel,
152
- deliveryId: this.normalizeDeliveryId(ids[index], notification),
153
- queued: true,
154
- status: 'queued'
155
- }));
156
- for (let index = 0; index < notifications.length; index += 1) {
157
- const notification = notifications[index];
178
+ for (let index = 0; index < dispatchNotifications.length; index += 1) {
179
+ const notification = dispatchNotifications[index];
158
180
  await this.publishLifecycleEventBestEffort('notification.dispatch.queued', notification, options, results[index]?.deliveryId);
159
181
  }
160
182
  return {
@@ -167,9 +189,9 @@ class NotificationsService {
167
189
  }
168
190
  const results = [];
169
191
  const failures = [];
170
- for (const notification of notifications) {
192
+ for (const notification of dispatchNotifications) {
171
193
  try {
172
- results.push(await this.dispatch(notification, options));
194
+ results.push(await this.dispatchAdmitted(notification, options));
173
195
  } catch (error) {
174
196
  const failure = {
175
197
  error: error instanceof Error ? error : new Error('Notification dispatch failed.'),
@@ -199,6 +221,7 @@ class NotificationsService {
199
221
  return createNotificationsPlatformStatusSnapshot({
200
222
  bulkQueueThreshold: this.options.queue?.bulkThreshold ?? 0,
201
223
  channelsRegistered: this.channelsByName.size,
224
+ eventPublicationEnabled: this.options.events?.publishLifecycleEvents ?? false,
202
225
  eventPublisherConfigured: this.options.events !== undefined,
203
226
  queueConfigured: this.options.queue !== undefined
204
227
  });
@@ -261,7 +284,7 @@ class NotificationsService {
261
284
  if (!this.options.events || !this.shouldPublishLifecycleEvents(options)) {
262
285
  return;
263
286
  }
264
- const event = {
287
+ const event = createNotificationLifecycleEventSnapshot({
265
288
  channel: notification.channel,
266
289
  deliveryId,
267
290
  error: error instanceof Error ? {
@@ -271,7 +294,7 @@ class NotificationsService {
271
294
  name,
272
295
  notification,
273
296
  occurredAt: new Date().toISOString()
274
- };
297
+ });
275
298
  await this.options.events.publisher.publish(event);
276
299
  }
277
300
  async publishLifecycleEventBestEffort(name, notification, options, deliveryId, error) {
@@ -323,7 +346,10 @@ class NotificationsService {
323
346
  continue;
324
347
  }
325
348
  try {
326
- const deliveryId = this.normalizeDeliveryId(await queue.enqueue(job), notification);
349
+ throwIfAborted(options.signal);
350
+ const deliveryId = validateQueueDeliveryId(await queue.enqueue(job, {
351
+ signal: options.signal
352
+ }));
327
353
  const result = {
328
354
  channel: notification.channel,
329
355
  deliveryId,
@@ -362,43 +388,62 @@ class NotificationsService {
362
388
  }
363
389
  }
364
390
  export { _NotificationsService as NotificationsService };
391
+ function throwIfAborted(signal) {
392
+ signal?.throwIfAborted();
393
+ }
365
394
  function validateQueueBatchDeliveryIds(value, expectedCount) {
366
395
  if (!Array.isArray(value)) {
367
- throw createQueueBatchResultIntegrityError(`expected ${expectedCount} queue ids but received a non-array result`);
396
+ throw createQueueResultIntegrityError('enqueueMany', `expected ${expectedCount} queue ids but received a non-array result`);
397
+ }
398
+ const descriptors = Object.getOwnPropertyDescriptors(value);
399
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
400
+ if (!isOwnDataPropertyDescriptor(lengthDescriptor)) {
401
+ throw createQueueResultIntegrityError('enqueueMany', `expected ${expectedCount} queue ids but received an invalid length descriptor`);
368
402
  }
369
- if (value.length !== expectedCount) {
370
- throw createQueueBatchResultIntegrityError(`expected ${expectedCount} queue ids but received ${value.length}`);
403
+ if (lengthDescriptor.value !== expectedCount) {
404
+ throw createQueueResultIntegrityError('enqueueMany', `expected ${expectedCount} queue ids but received ${String(lengthDescriptor.value)}`);
371
405
  }
372
406
  const ids = [];
373
- for (let index = 0; index < value.length; index += 1) {
374
- if (!Object.hasOwn(value, index)) {
375
- throw createQueueBatchResultIntegrityError(`queue id at index ${index} must be present`);
407
+ for (let index = 0; index < expectedCount; index += 1) {
408
+ const descriptor = descriptors[String(index)];
409
+ if (!descriptor) {
410
+ throw createQueueResultIntegrityError('enqueueMany', `queue id at index ${index} must be present`);
376
411
  }
377
- const entry = value[index];
412
+ if (!isOwnDataPropertyDescriptor(descriptor)) {
413
+ throw createQueueResultIntegrityError('enqueueMany', `queue id at index ${index} must be an own data property`);
414
+ }
415
+ const entry = descriptor.value;
378
416
  if (typeof entry !== 'string' || entry.length === 0) {
379
- throw createQueueBatchResultIntegrityError(`queue id at index ${index} must be a non-empty string`);
417
+ throw createQueueResultIntegrityError('enqueueMany', `queue id at index ${index} must be a non-empty string`);
380
418
  }
381
419
  ids.push(entry);
382
420
  }
383
- return ids;
421
+ return Object.freeze(ids);
384
422
  }
385
- function createQueueBatchResultIntegrityError(message) {
386
- const error = new Error(`Notifications queue adapter returned an invalid enqueueMany() result: ${message}.`);
387
- error.name = 'NotificationQueueResultIntegrityError';
388
- return error;
423
+ function isOwnDataPropertyDescriptor(descriptor) {
424
+ return descriptor !== undefined && Object.hasOwn(descriptor, 'value') && !Object.hasOwn(descriptor, 'get') && !Object.hasOwn(descriptor, 'set');
425
+ }
426
+ function validateQueueDeliveryId(value) {
427
+ if (typeof value !== 'string' || value.length === 0) {
428
+ throw createQueueResultIntegrityError('enqueue', 'queue id must be a non-empty string');
429
+ }
430
+ return value;
431
+ }
432
+ function createQueueResultIntegrityError(operation, message) {
433
+ return new NotificationQueueResultIntegrityError(operation, message);
389
434
  }
390
435
  function createLifecyclePublicationFailureError(dispatchError, ...publicationErrors) {
391
436
  const primaryMessage = dispatchError instanceof Error ? dispatchError.message : 'Notification dispatch failed.';
392
437
  return new AggregateError([dispatchError, ...publicationErrors], `Notification dispatch failed, and failed lifecycle event publication also failed: ${primaryMessage}`);
393
438
  }
394
439
  function stableNotificationHash(notification) {
395
- let hash = 0x811c9dc5;
440
+ let hash = 0xcbf29ce484222325n;
396
441
  const input = stableStringify(notification, createStableStringifyContext());
397
442
  for (let index = 0; index < input.length; index += 1) {
398
- hash ^= input.charCodeAt(index);
399
- hash = Math.imul(hash, 0x01000193) >>> 0;
443
+ hash ^= BigInt(input.charCodeAt(index));
444
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
400
445
  }
401
- return hash.toString(36).padStart(7, '0');
446
+ return hash.toString(36).padStart(13, '0');
402
447
  }
403
448
  function createStableStringifyContext() {
404
449
  return {
@@ -447,6 +492,16 @@ function stableStringify(value, context) {
447
492
  if (circularReferenceId !== undefined) {
448
493
  return `Circular:${circularReferenceId}`;
449
494
  }
495
+ if (value instanceof ArrayBuffer) {
496
+ const serialized = `ArrayBuffer:{byteLength:${value.byteLength},bytes:${stableByteArray(new Uint8Array(value))}}`;
497
+ context.seen.delete(value);
498
+ return serialized;
499
+ }
500
+ if (ArrayBuffer.isView(value)) {
501
+ const serialized = `ArrayBufferView:{view:${JSON.stringify(Object.prototype.toString.call(value).slice(8, -1))},byteOffset:${value.byteOffset},byteLength:${value.byteLength},bytes:${stableByteArray(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))}}`;
502
+ context.seen.delete(value);
503
+ return serialized;
504
+ }
450
505
  if (value instanceof Date) {
451
506
  const serialized = Number.isNaN(value.getTime()) ? 'Date:Invalid' : `Date:${JSON.stringify(value.toISOString())}`;
452
507
  context.seen.delete(value);
@@ -496,8 +551,11 @@ function stableStringify(value, context) {
496
551
  }
497
552
  const prototype = Object.getPrototypeOf(value);
498
553
  const objectTag = prototype && prototype !== Object.prototype ? `${prototype.constructor?.name ?? 'Object'}:` : '';
499
- const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
554
+ const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => compareStableString(left, right));
500
555
  const serialized = `${objectTag}{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry, context)}`).join(',')}}`;
501
556
  context.seen.delete(value);
502
557
  return serialized;
558
+ }
559
+ function stableByteArray(bytes) {
560
+ return `[${Array.from(bytes).join(',')}]`;
503
561
  }
@@ -0,0 +1,28 @@
1
+ import type { NotificationDispatchRequest, NotificationLifecycleEvent } from './types.js';
2
+ /**
3
+ * Copies and freezes one notification envelope at dispatch admission.
4
+ *
5
+ * @param notification Notification envelope supplied by the caller.
6
+ * @returns The immutable dispatch snapshot.
7
+ * @internal
8
+ */
9
+ export declare function createNotificationDispatchSnapshot<TRequest extends NotificationDispatchRequest>(notification: TRequest): TRequest;
10
+ /**
11
+ * Copies and freezes one lifecycle event before publication.
12
+ *
13
+ * @param event Lifecycle event details captured by the dispatch service.
14
+ * @returns The immutable lifecycle event snapshot.
15
+ * @internal
16
+ */
17
+ export declare function createNotificationLifecycleEventSnapshot<TRequest extends NotificationDispatchRequest>(event: {
18
+ channel: string;
19
+ deliveryId?: string;
20
+ error?: {
21
+ message: string;
22
+ name: string;
23
+ };
24
+ name: NotificationLifecycleEvent<TRequest>['name'];
25
+ notification: TRequest;
26
+ occurredAt: string;
27
+ }): NotificationLifecycleEvent<TRequest>;
28
+ //# sourceMappingURL=snapshots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,0BAA0B,EAU3B,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,QAAQ,SAAS,2BAA2B,EAC7F,YAAY,EAAE,QAAQ,GACrB,QAAQ,CAEV;AAED;;;;;;GAMG;AACH,wBAAgB,wCAAwC,CAAC,QAAQ,SAAS,2BAA2B,EACnG,KAAK,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,IAAI,EAAE,0BAA0B,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;IACnD,YAAY,EAAE,QAAQ,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,GACA,0BAA0B,CAAC,QAAQ,CAAC,CAItC"}
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Copies and freezes one notification envelope at dispatch admission.
3
+ *
4
+ * @param notification Notification envelope supplied by the caller.
5
+ * @returns The immutable dispatch snapshot.
6
+ * @internal
7
+ */
8
+ export function createNotificationDispatchSnapshot(notification) {
9
+ return freezeSnapshot(cloneSnapshot(notification, new Map()));
10
+ }
11
+
12
+ /**
13
+ * Copies and freezes one lifecycle event before publication.
14
+ *
15
+ * @param event Lifecycle event details captured by the dispatch service.
16
+ * @returns The immutable lifecycle event snapshot.
17
+ * @internal
18
+ */
19
+ export function createNotificationLifecycleEventSnapshot(event) {
20
+ return freezeSnapshot(createLifecycleSnapshot(event, new Map()));
21
+ }
22
+ function cloneSnapshot(value, seen) {
23
+ if (value === null || typeof value !== 'object') {
24
+ return value;
25
+ }
26
+ const existing = seen.get(value);
27
+ if (existing) {
28
+ return existing;
29
+ }
30
+ if (value instanceof Date) {
31
+ const clone = new Date(value.getTime());
32
+ seen.set(value, clone);
33
+ return clone;
34
+ }
35
+ if (value instanceof URL) {
36
+ const clone = new URL(value.href);
37
+ seen.set(value, clone);
38
+ return clone;
39
+ }
40
+ if (value instanceof URLSearchParams) {
41
+ const clone = new URLSearchParams(value);
42
+ seen.set(value, clone);
43
+ return clone;
44
+ }
45
+ if (value instanceof RegExp) {
46
+ const clone = new RegExp(value.source, value.flags);
47
+ clone.lastIndex = value.lastIndex;
48
+ seen.set(value, clone);
49
+ return clone;
50
+ }
51
+ if (value instanceof ArrayBuffer) {
52
+ const clone = value.slice(0);
53
+ seen.set(value, clone);
54
+ return clone;
55
+ }
56
+ if (ArrayBuffer.isView(value)) {
57
+ if (!(value.buffer instanceof ArrayBuffer)) {
58
+ throw new TypeError('Notification snapshots only support ArrayBuffer-backed views.');
59
+ }
60
+ const buffer = cloneSnapshot(value.buffer, seen);
61
+ const clone = cloneArrayBufferView(value, buffer);
62
+ seen.set(value, clone);
63
+ return clone;
64
+ }
65
+ if (value instanceof Map) {
66
+ const clone = new Map();
67
+ seen.set(value, clone);
68
+ for (const [key, entry] of value) {
69
+ clone.set(cloneSnapshot(key, seen), cloneSnapshot(entry, seen));
70
+ }
71
+ return clone;
72
+ }
73
+ if (value instanceof Set) {
74
+ const clone = new Set();
75
+ seen.set(value, clone);
76
+ for (const entry of value) {
77
+ clone.add(cloneSnapshot(entry, seen));
78
+ }
79
+ return clone;
80
+ }
81
+ assertSnapshotObjectIsDataOnly(value);
82
+ const clone = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
83
+ seen.set(value, clone);
84
+ for (const key of Reflect.ownKeys(value)) {
85
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
86
+ if (!descriptor) {
87
+ continue;
88
+ }
89
+ if ('value' in descriptor) {
90
+ descriptor.value = cloneSnapshot(descriptor.value, seen);
91
+ }
92
+ Object.defineProperty(clone, key, descriptor);
93
+ }
94
+ return clone;
95
+ }
96
+ function createLifecycleSnapshot(value, seen) {
97
+ if (value === null || typeof value !== 'object') {
98
+ return value;
99
+ }
100
+ const existing = seen.get(value);
101
+ if (existing) {
102
+ return existing;
103
+ }
104
+ if (value instanceof Date) {
105
+ const snapshot = {
106
+ epochMilliseconds: Number.isNaN(value.getTime()) ? null : value.getTime(),
107
+ kind: 'Date'
108
+ };
109
+ seen.set(value, snapshot);
110
+ return snapshot;
111
+ }
112
+ if (value instanceof URL) {
113
+ const snapshot = {
114
+ href: value.href,
115
+ kind: 'URL'
116
+ };
117
+ seen.set(value, snapshot);
118
+ return snapshot;
119
+ }
120
+ if (value instanceof URLSearchParams) {
121
+ const snapshot = {
122
+ kind: 'URLSearchParams',
123
+ query: value.toString()
124
+ };
125
+ seen.set(value, snapshot);
126
+ return snapshot;
127
+ }
128
+ if (value instanceof RegExp) {
129
+ const snapshot = {
130
+ flags: value.flags,
131
+ kind: 'RegExp',
132
+ lastIndex: value.lastIndex,
133
+ source: value.source
134
+ };
135
+ seen.set(value, snapshot);
136
+ return snapshot;
137
+ }
138
+ if (value instanceof ArrayBuffer) {
139
+ const snapshot = {
140
+ byteLength: value.byteLength,
141
+ bytes: Array.from(new Uint8Array(value)),
142
+ kind: 'ArrayBuffer'
143
+ };
144
+ seen.set(value, snapshot);
145
+ return snapshot;
146
+ }
147
+ if (ArrayBuffer.isView(value)) {
148
+ const snapshot = {
149
+ byteLength: value.byteLength,
150
+ byteOffset: value.byteOffset,
151
+ bytes: Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)),
152
+ kind: 'ArrayBufferView',
153
+ view: Object.prototype.toString.call(value).slice(8, -1)
154
+ };
155
+ seen.set(value, snapshot);
156
+ return snapshot;
157
+ }
158
+ if (value instanceof Map) {
159
+ const snapshot = {
160
+ entries: [],
161
+ kind: 'Map'
162
+ };
163
+ seen.set(value, snapshot);
164
+ for (const [key, entry] of value) {
165
+ snapshot.entries.push([createLifecycleSnapshot(key, seen), createLifecycleSnapshot(entry, seen)]);
166
+ }
167
+ return snapshot;
168
+ }
169
+ if (value instanceof Set) {
170
+ const snapshot = {
171
+ kind: 'Set',
172
+ values: []
173
+ };
174
+ seen.set(value, snapshot);
175
+ for (const entry of value) {
176
+ snapshot.values.push(createLifecycleSnapshot(entry, seen));
177
+ }
178
+ return snapshot;
179
+ }
180
+ assertSnapshotObjectIsDataOnly(value);
181
+ const snapshot = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
182
+ seen.set(value, snapshot);
183
+ for (const key of Reflect.ownKeys(value)) {
184
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
185
+ if (!descriptor) {
186
+ continue;
187
+ }
188
+ if ('value' in descriptor) {
189
+ descriptor.value = createLifecycleSnapshot(descriptor.value, seen);
190
+ }
191
+ Object.defineProperty(snapshot, key, descriptor);
192
+ }
193
+ return snapshot;
194
+ }
195
+ function freezeSnapshot(value, seen = new WeakSet()) {
196
+ if (value === null || typeof value !== 'object' || seen.has(value)) {
197
+ return value;
198
+ }
199
+ seen.add(value);
200
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
201
+ return value;
202
+ }
203
+ if (value instanceof Map) {
204
+ for (const [key, entry] of value) {
205
+ freezeSnapshot(key, seen);
206
+ freezeSnapshot(entry, seen);
207
+ }
208
+ } else if (value instanceof Set) {
209
+ for (const entry of value) {
210
+ freezeSnapshot(entry, seen);
211
+ }
212
+ } else {
213
+ for (const key of Reflect.ownKeys(value)) {
214
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
215
+ if (descriptor && 'value' in descriptor) {
216
+ freezeSnapshot(descriptor.value, seen);
217
+ }
218
+ }
219
+ }
220
+ return Object.freeze(value);
221
+ }
222
+ function cloneArrayBufferView(value, buffer) {
223
+ if (value instanceof DataView) {
224
+ return new DataView(buffer, value.byteOffset, value.byteLength);
225
+ }
226
+ const typedArray = value;
227
+ return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
228
+ }
229
+ function assertSnapshotObjectIsDataOnly(value) {
230
+ const prototype = Object.getPrototypeOf(value);
231
+ if (Array.isArray(value) && prototype !== Array.prototype || !Array.isArray(value) && prototype !== null && prototype !== Object.prototype) {
232
+ throw new TypeError('Notification snapshots only support data properties on plain objects.');
233
+ }
234
+ for (const key of Reflect.ownKeys(value)) {
235
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
236
+ if (descriptor && !('value' in descriptor)) {
237
+ throw new TypeError('Notification snapshots only support data properties on plain objects.');
238
+ }
239
+ }
240
+ }
package/dist/status.d.ts CHANGED
@@ -5,20 +5,50 @@ export type NotificationsOperationMode = 'direct-only' | 'direct-with-events' |
5
5
  export interface NotificationsStatusAdapterInput {
6
6
  bulkQueueThreshold: number;
7
7
  channelsRegistered: number;
8
+ /**
9
+ * Whether lifecycle publication is enabled for the configured publisher. Defaults to
10
+ * `eventPublisherConfigured` when omitted so callers that only know about configuration keep
11
+ * their previous diagnostics.
12
+ */
13
+ eventPublicationEnabled?: boolean;
8
14
  eventPublisherConfigured: boolean;
9
15
  queueConfigured: boolean;
10
16
  }
17
+ /**
18
+ * Typed diagnostics published under {@link NotificationsPlatformStatusSnapshot.details}.
19
+ *
20
+ * The index signature keeps the shape assignable to `Record<string, unknown>` consumers while the
21
+ * named members give typed access to the documented diagnostics.
22
+ */
23
+ export interface NotificationsStatusDetails {
24
+ bulkQueueThreshold: number;
25
+ channelsRegistered: number;
26
+ dependencies: readonly string[];
27
+ /** Whether a configured publisher is actually publishing lifecycle events. */
28
+ eventPublicationEnabled: boolean;
29
+ /** Whether an event publisher is wired, regardless of publication enablement. */
30
+ eventPublisherConfigured: boolean;
31
+ operationMode: NotificationsOperationMode;
32
+ queueConfigured: boolean;
33
+ [detail: string]: unknown;
34
+ }
11
35
  /** Structured snapshot returned by {@link createNotificationsPlatformStatusSnapshot}. */
12
36
  export interface NotificationsPlatformStatusSnapshot {
13
37
  readiness: PlatformReadinessReport;
14
38
  health: PlatformHealthReport;
15
39
  ownership: PlatformSnapshot['ownership'];
16
- details: Record<string, unknown>;
40
+ details: NotificationsStatusDetails;
17
41
  }
18
42
  /**
19
43
  * Creates a health/readiness snapshot for the notifications orchestration layer.
20
44
  *
21
- * @param input Registered-channel and optional-integration counts derived from the active module wiring.
45
+ * Publisher configuration and lifecycle publication enablement are reported separately:
46
+ * `details.eventPublisherConfigured` records the wiring while `details.eventPublicationEnabled`
47
+ * records whether events are actually published. Operation mode, active dependencies, and external
48
+ * ownership are derived from enablement so a configured-but-disabled publisher is never reported as
49
+ * an active event-backed runtime.
50
+ *
51
+ * @param input Registered-channel and optional-integration state derived from the active module wiring.
22
52
  * @returns A structured snapshot suitable for status endpoints and operational diagnostics.
23
53
  */
24
54
  export declare function createNotificationsPlatformStatusSnapshot(input: NotificationsStatusAdapterInput): NotificationsPlatformStatusSnapshot;
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,+DAA+D;AAC/D,MAAM,MAAM,0BAA0B,GAClC,aAAa,GACb,oBAAoB,GACpB,cAAc,GACd,0BAA0B,GAC1B,cAAc,CAAC;AAEnB,wEAAwE;AACxE,MAAM,WAAW,+BAA+B;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wBAAwB,EAAE,OAAO,CAAC;IAClC,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,yFAAyF;AACzF,MAAM,WAAW,mCAAmC;IAClD,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAyDD;;;;;GAKG;AACH,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,+BAA+B,GACrC,mCAAmC,CAoBrC"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,+DAA+D;AAC/D,MAAM,MAAM,0BAA0B,GAClC,aAAa,GACb,oBAAoB,GACpB,cAAc,GACd,0BAA0B,GAC1B,cAAc,CAAC;AAEnB,wEAAwE;AACxE,MAAM,WAAW,+BAA+B;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,wBAAwB,EAAE,OAAO,CAAC;IAClC,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IACzC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,8EAA8E;IAC9E,uBAAuB,EAAE,OAAO,CAAC;IACjC,iFAAiF;IACjF,wBAAwB,EAAE,OAAO,CAAC;IAClC,aAAa,EAAE,0BAA0B,CAAC;IAC1C,eAAe,EAAE,OAAO,CAAC;IACzB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAED,yFAAyF;AACzF,MAAM,WAAW,mCAAmC;IAClD,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,0BAA0B,CAAC;CACrC;AA4ED;;;;;;;;;;;GAWG;AACH,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,+BAA+B,GACrC,mCAAmC,CAuBrC"}