@futdevpro/nts-dynamo 1.15.265 → 1.15.266

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.
Files changed (50) hide show
  1. package/README.md +22 -0
  2. package/build/_modules/application-alert-delivery/_enums/application-alert-delivery-state.type-enum.d.ts +21 -0
  3. package/build/_modules/application-alert-delivery/_enums/application-alert-delivery-state.type-enum.d.ts.map +1 -0
  4. package/build/_modules/application-alert-delivery/_enums/application-alert-delivery-state.type-enum.js +27 -0
  5. package/build/_modules/application-alert-delivery/_enums/application-alert-delivery-state.type-enum.js.map +1 -0
  6. package/build/_modules/application-alert-delivery/_models/application-alert-delivery.interface.d.ts +104 -0
  7. package/build/_modules/application-alert-delivery/_models/application-alert-delivery.interface.d.ts.map +1 -0
  8. package/build/_modules/application-alert-delivery/_models/application-alert-delivery.interface.js +3 -0
  9. package/build/_modules/application-alert-delivery/_models/application-alert-delivery.interface.js.map +1 -0
  10. package/build/_modules/application-alert-delivery/application-alert-delivery.control-service.d.ts +12 -0
  11. package/build/_modules/application-alert-delivery/application-alert-delivery.control-service.d.ts.map +1 -0
  12. package/build/_modules/application-alert-delivery/application-alert-delivery.control-service.js +52 -0
  13. package/build/_modules/application-alert-delivery/application-alert-delivery.control-service.js.map +1 -0
  14. package/build/_modules/application-alert-delivery/application-alert-delivery.mongo-store.d.ts +49 -0
  15. package/build/_modules/application-alert-delivery/application-alert-delivery.mongo-store.d.ts.map +1 -0
  16. package/build/_modules/application-alert-delivery/application-alert-delivery.mongo-store.js +702 -0
  17. package/build/_modules/application-alert-delivery/application-alert-delivery.mongo-store.js.map +1 -0
  18. package/build/_modules/application-alert-delivery/index.d.ts +5 -0
  19. package/build/_modules/application-alert-delivery/index.d.ts.map +1 -0
  20. package/build/_modules/application-alert-delivery/index.js +8 -0
  21. package/build/_modules/application-alert-delivery/index.js.map +1 -0
  22. package/build/_modules/lifecycle-cleanup/_enums/lifecycle-cleanup-state.type-enum.d.ts +14 -0
  23. package/build/_modules/lifecycle-cleanup/_enums/lifecycle-cleanup-state.type-enum.d.ts.map +1 -0
  24. package/build/_modules/lifecycle-cleanup/_enums/lifecycle-cleanup-state.type-enum.js +19 -0
  25. package/build/_modules/lifecycle-cleanup/_enums/lifecycle-cleanup-state.type-enum.js.map +1 -0
  26. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-mongo-store-settings.interface.d.ts +8 -0
  27. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-mongo-store-settings.interface.d.ts.map +1 -0
  28. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-mongo-store-settings.interface.js +3 -0
  29. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-mongo-store-settings.interface.js.map +1 -0
  30. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-settings.interface.d.ts +11 -0
  31. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-settings.interface.d.ts.map +1 -0
  32. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-settings.interface.js +3 -0
  33. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup-settings.interface.js.map +1 -0
  34. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup.interface.d.ts +118 -0
  35. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup.interface.d.ts.map +1 -0
  36. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup.interface.js +3 -0
  37. package/build/_modules/lifecycle-cleanup/_models/lifecycle-cleanup.interface.js.map +1 -0
  38. package/build/_modules/lifecycle-cleanup/index.d.ts +7 -0
  39. package/build/_modules/lifecycle-cleanup/index.d.ts.map +1 -0
  40. package/build/_modules/lifecycle-cleanup/index.js +10 -0
  41. package/build/_modules/lifecycle-cleanup/index.js.map +1 -0
  42. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.control-service.d.ts +32 -0
  43. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.control-service.d.ts.map +1 -0
  44. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.control-service.js +246 -0
  45. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.control-service.js.map +1 -0
  46. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.mongo-store.d.ts +36 -0
  47. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.mongo-store.d.ts.map +1 -0
  48. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.mongo-store.js +410 -0
  49. package/build/_modules/lifecycle-cleanup/lifecycle-cleanup.mongo-store.js.map +1 -0
  50. package/package.json +21 -3
@@ -0,0 +1,702 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DyNTS_ApplicationAlertDelivery_MongoStore = void 0;
4
+ const mongoose_1 = require("mongoose");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_util_1 = require("node:util");
7
+ const application_alert_1 = require("@futdevpro/fsm-dynamo/application-alert");
8
+ const application_alert_delivery_state_type_enum_1 = require("./_enums/application-alert-delivery-state.type-enum");
9
+ const DYNTS_APPLICATION_ALERT_DELIVERY_COLLECTION = 'dynts_application_alert_deliveries';
10
+ const dynts_hashPattern = /^[a-f0-9]{64}$/u;
11
+ const dynts_safeSlugPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u;
12
+ const dynts_resultCodePattern = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+){0,15}$/u;
13
+ const dynts_maxCodeLength = 128;
14
+ const dynts_maxRetentionMs = 90 * 24 * 60 * 60 * 1_000;
15
+ const dynts_neverExpires = new Date(8_640_000_000_000_000);
16
+ /** Mongo-backed, explicitly invoked application-alert sender outbox. */
17
+ class DyNTS_ApplicationAlertDelivery_MongoStore {
18
+ settings;
19
+ ownedErrors = new WeakSet();
20
+ isReady = false;
21
+ readiness;
22
+ /** Validates finite retry, lease and retention policy without starting any worker or timer. */
23
+ constructor(settings) {
24
+ if (!settings?.connection
25
+ || !dynts_safeSlugPattern.test(settings.sourceSystem)
26
+ || !this.positiveInteger(settings.maxOperationTimeMs, 60_000)
27
+ || !this.positiveInteger(settings.leaseDurationMs, 300_000)
28
+ || settings.leaseDurationMs < 1_000
29
+ || !this.positiveInteger(settings.maxAttempts, 10)
30
+ || !Array.isArray(settings.retryBackoffMs)
31
+ || settings.retryBackoffMs.length !== settings.maxAttempts - 1
32
+ || !settings.retryBackoffMs.every((value) => (this.positiveInteger(value, 24 * 60 * 60 * 1_000)))
33
+ || !this.positiveInteger(settings.retentionMs, dynts_maxRetentionMs)) {
34
+ throw new Error('DyNTS|APPLICATION_ALERT_DELIVERY|INVALID_SETTINGS');
35
+ }
36
+ this.settings = Object.freeze({ ...settings, retryBackoffMs: Object.freeze([...settings.retryBackoffMs]) });
37
+ }
38
+ /** Creates and verifies the strict collection plus deterministic claim and TTL indexes. */
39
+ async ensureReady() {
40
+ this.readiness ??= (async () => {
41
+ await this.settings.connection.db.createCollection(DYNTS_APPLICATION_ALERT_DELIVERY_COLLECTION, {
42
+ validator: this.validator(),
43
+ validationLevel: 'strict',
44
+ validationAction: 'error',
45
+ }).catch((error) => {
46
+ if (!this.isMongoCode(error, 48)) {
47
+ throw error;
48
+ }
49
+ });
50
+ const collectionInfo = await this.settings.connection.db.listCollections({ name: DYNTS_APPLICATION_ALERT_DELIVERY_COLLECTION }, { nameOnly: false }).next();
51
+ if (!(0, node_util_1.isDeepStrictEqual)(collectionInfo?.options?.validator, this.validator())
52
+ || collectionInfo?.options?.validationLevel !== 'strict'
53
+ || collectionInfo?.options?.validationAction !== 'error') {
54
+ throw this.error('SCHEMA_MIGRATION_REQUIRED');
55
+ }
56
+ await this.collection().createIndex({ sourceSystem: 1, state: 1, nextAttemptAt: 1, leaseExpiresAt: 1, _id: 1 }, { name: 'dynts_application_alert_delivery_claim_v1', maxTimeMS: this.settings.maxOperationTimeMs });
57
+ await this.collection().createIndex({ expiresAt: 1 }, {
58
+ name: 'dynts_application_alert_delivery_expiry_v1',
59
+ expireAfterSeconds: 0,
60
+ maxTimeMS: this.settings.maxOperationTimeMs,
61
+ });
62
+ this.isReady = true;
63
+ })().catch((error) => {
64
+ this.readiness = undefined;
65
+ this.isReady = false;
66
+ if (error instanceof Error && this.ownedErrors.has(error)) {
67
+ throw error;
68
+ }
69
+ throw this.error('NOT_READY', error);
70
+ });
71
+ await this.readiness;
72
+ }
73
+ /** Inserts one validated envelope or acknowledges an exact idempotent replay. */
74
+ async schedule(value) {
75
+ return this.protect('SCHEDULE_FAILED', async () => {
76
+ this.assertReady();
77
+ const validation = application_alert_1.DyFM_ApplicationAlertEnvelope_Util.validate(value);
78
+ if (!validation.valid || !validation.envelope || !validation.payloadChecksum
79
+ || validation.envelope.sourceSystem !== this.settings.sourceSystem) {
80
+ throw this.error('ENVELOPE_INVALID');
81
+ }
82
+ const envelope = validation.envelope;
83
+ const key = this.key(envelope.sourceSystem, envelope.idempotencyKey);
84
+ const existing = await this.collection().findOne({ _id: key }, this.options());
85
+ if (existing) {
86
+ this.assertReplay(existing, envelope, validation.payloadChecksum);
87
+ return this.view(existing, false);
88
+ }
89
+ const now = await this.databaseTime();
90
+ const record = {
91
+ _id: key,
92
+ contractVersion: 'dynts-application-alert-delivery/1',
93
+ sourceSystem: envelope.sourceSystem,
94
+ idempotencyKey: envelope.idempotencyKey,
95
+ payloadChecksum: validation.payloadChecksum,
96
+ envelope: envelope,
97
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending,
98
+ attemptCount: 0,
99
+ fencingToken: 0,
100
+ claimTokenHash: '',
101
+ claimedAt: new Date(0),
102
+ leaseExpiresAt: new Date(0),
103
+ nextAttemptAt: now,
104
+ lastOutcome: '',
105
+ lastResultCode: '',
106
+ deliveryReference: '',
107
+ createdAt: now,
108
+ updatedAt: now,
109
+ completedAt: new Date(0),
110
+ expiresAt: new Date(dynts_neverExpires.getTime()),
111
+ };
112
+ try {
113
+ await this.collection().insertOne(record, this.options());
114
+ }
115
+ catch (error) {
116
+ if (!this.isMongoCode(error, 11000)) {
117
+ throw error;
118
+ }
119
+ const raced = await this.collection().findOne({ _id: key }, this.options());
120
+ if (!raced) {
121
+ throw this.error('IDEMPOTENCY_RACE');
122
+ }
123
+ this.assertReplay(raced, envelope, validation.payloadChecksum);
124
+ return this.view(raced, false);
125
+ }
126
+ return this.view(record, true);
127
+ });
128
+ }
129
+ /** Claims at most one due item or retires one exhausted crashed claim; it never waits or polls. */
130
+ async claimNext() {
131
+ return this.protect('CLAIM_FAILED', async () => {
132
+ this.assertReady();
133
+ const retired = await this.retireExhausted();
134
+ if (retired) {
135
+ this.assertRecord(retired);
136
+ return Object.freeze({
137
+ contractVersion: 'dynts-application-alert-claim/1',
138
+ code: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertInvocationCode_Type.terminalFailureRecorded,
139
+ claimed: false,
140
+ claimToken: '',
141
+ fencingToken: retired.fencingToken,
142
+ delivery: this.view(retired, true),
143
+ });
144
+ }
145
+ const claimToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
146
+ const claimTokenHash = this.tokenHash(claimToken);
147
+ const claimed = await this.collection().findOneAndUpdate({
148
+ sourceSystem: this.settings.sourceSystem,
149
+ attemptCount: { $lt: this.settings.maxAttempts },
150
+ $or: [
151
+ {
152
+ state: { $in: [
153
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending,
154
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting,
155
+ ] },
156
+ $expr: { $lte: ['$nextAttemptAt', '$$NOW'] },
157
+ },
158
+ {
159
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching,
160
+ $expr: { $lte: ['$leaseExpiresAt', '$$NOW'] },
161
+ },
162
+ ],
163
+ }, [{ $set: {
164
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching,
165
+ attemptCount: { $add: ['$attemptCount', 1] },
166
+ fencingToken: { $add: ['$fencingToken', 1] },
167
+ claimTokenHash: { $literal: claimTokenHash },
168
+ claimedAt: '$$NOW',
169
+ leaseExpiresAt: { $dateAdd: {
170
+ startDate: '$$NOW', unit: 'millisecond', amount: this.settings.leaseDurationMs,
171
+ } },
172
+ updatedAt: '$$NOW',
173
+ } }], {
174
+ ...this.options(),
175
+ sort: { nextAttemptAt: 1, _id: 1 },
176
+ returnDocument: 'after',
177
+ includeResultMetadata: false,
178
+ });
179
+ if (!claimed) {
180
+ return Object.freeze({
181
+ contractVersion: 'dynts-application-alert-claim/1',
182
+ code: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertInvocationCode_Type.noReadyDelivery,
183
+ claimed: false,
184
+ claimToken: '',
185
+ fencingToken: 0,
186
+ });
187
+ }
188
+ this.assertRecord(claimed);
189
+ return Object.freeze({
190
+ contractVersion: 'dynts-application-alert-claim/1',
191
+ code: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertInvocationCode_Type.deliveryClaimed,
192
+ claimed: true,
193
+ claimToken: claimToken,
194
+ fencingToken: claimed.fencingToken,
195
+ envelope: this.copyEnvelope(claimed.envelope),
196
+ delivery: this.view(claimed, true),
197
+ });
198
+ });
199
+ }
200
+ /** Commits accepted, retryable or terminal delivery evidence under the live DB-time fence. */
201
+ async complete(value) {
202
+ return this.protect('COMPLETE_FAILED', async () => {
203
+ this.assertReady();
204
+ const completion = this.completion(value);
205
+ const key = this.key(completion.sourceSystem, completion.idempotencyKey);
206
+ const current = await this.collection().findOne({ _id: key }, this.options());
207
+ if (!current) {
208
+ throw this.error('NOT_FOUND');
209
+ }
210
+ this.assertRecord(current);
211
+ const tokenHash = this.tokenHash(completion.claimToken);
212
+ if (current.state !== application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching) {
213
+ if (this.isExactCompletionReplay(current, completion, tokenHash)) {
214
+ return this.view(current, false);
215
+ }
216
+ throw this.error('FENCE_LOST');
217
+ }
218
+ if (current.fencingToken !== completion.fencingToken || current.claimTokenHash !== tokenHash) {
219
+ throw this.error('FENCE_LOST');
220
+ }
221
+ const retry = completion.submission.outcome
222
+ === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.retryableFailure
223
+ && current.attemptCount < this.settings.maxAttempts;
224
+ const terminalState = completion.submission.outcome
225
+ === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.accepted
226
+ ? application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.succeeded
227
+ : application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.failed;
228
+ const nextState = retry
229
+ ? application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting
230
+ : terminalState;
231
+ const backoffMs = retry ? this.settings.retryBackoffMs[current.attemptCount - 1] : 0;
232
+ const updated = await this.collection().findOneAndUpdate({
233
+ _id: key,
234
+ sourceSystem: completion.sourceSystem,
235
+ idempotencyKey: completion.idempotencyKey,
236
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching,
237
+ fencingToken: completion.fencingToken,
238
+ claimTokenHash: tokenHash,
239
+ $expr: { $gt: ['$leaseExpiresAt', '$$NOW'] },
240
+ }, [{ $set: {
241
+ state: nextState,
242
+ leaseExpiresAt: new Date(0),
243
+ nextAttemptAt: retry ? { $dateAdd: {
244
+ startDate: '$$NOW', unit: 'millisecond', amount: backoffMs,
245
+ } } : '$$NOW',
246
+ lastOutcome: completion.submission.outcome,
247
+ lastResultCode: completion.submission.resultCode,
248
+ deliveryReference: completion.submission.deliveryReference,
249
+ updatedAt: '$$NOW',
250
+ completedAt: retry ? new Date(0) : '$$NOW',
251
+ expiresAt: retry ? dynts_neverExpires : { $dateAdd: {
252
+ startDate: '$$NOW', unit: 'millisecond', amount: this.settings.retentionMs,
253
+ } },
254
+ } }], { ...this.options(), returnDocument: 'after', includeResultMetadata: false });
255
+ if (!updated) {
256
+ throw this.error('FENCE_LOST');
257
+ }
258
+ this.assertRecord(updated);
259
+ return this.view(updated, true);
260
+ });
261
+ }
262
+ /** Reads only within the configured source-system boundary. */
263
+ async read(idempotencyKey) {
264
+ return this.protect('READ_FAILED', async () => {
265
+ this.assertReady();
266
+ if (!dynts_hashPattern.test(idempotencyKey)) {
267
+ throw this.error('IDENTITY_INVALID');
268
+ }
269
+ const record = await this.collection().findOne({ _id: this.key(this.settings.sourceSystem, idempotencyKey) }, this.options());
270
+ if (!record) {
271
+ return null;
272
+ }
273
+ this.assertRecord(record);
274
+ return this.view(record, false);
275
+ });
276
+ }
277
+ /** Returns bounded aggregate delivery health without returning any alert or receiver content. */
278
+ async inspect() {
279
+ return this.protect('INSPECTION_FAILED', async () => {
280
+ this.assertReady();
281
+ const openStates = [
282
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending,
283
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching,
284
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting,
285
+ ];
286
+ const rows = await this.collection().aggregate([
287
+ { $match: { sourceSystem: this.settings.sourceSystem } },
288
+ { $group: {
289
+ _id: null,
290
+ pendingCount: { $sum: { $cond: [
291
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending] }, 1, 0,
292
+ ] } },
293
+ dispatchingCount: { $sum: { $cond: [
294
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching] }, 1, 0,
295
+ ] } },
296
+ retryWaitingCount: { $sum: { $cond: [
297
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting] }, 1, 0,
298
+ ] } },
299
+ succeededCount: { $sum: { $cond: [
300
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.succeeded] }, 1, 0,
301
+ ] } },
302
+ failedCount: { $sum: { $cond: [
303
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.failed] }, 1, 0,
304
+ ] } },
305
+ dueCount: { $sum: { $cond: [{ $or: [
306
+ { $and: [
307
+ { $in: ['$state', [
308
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending,
309
+ application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting,
310
+ ]] },
311
+ { $lte: ['$nextAttemptAt', '$$NOW'] },
312
+ ] },
313
+ { $and: [
314
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching] },
315
+ { $lte: ['$leaseExpiresAt', '$$NOW'] },
316
+ ] },
317
+ ] }, 1, 0] } },
318
+ expiredLeaseCount: { $sum: { $cond: [{ $and: [
319
+ { $eq: ['$state', application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching] },
320
+ { $lte: ['$leaseExpiresAt', '$$NOW'] },
321
+ ] }, 1, 0] } },
322
+ oldestOpenAt: { $min: { $cond: [
323
+ { $in: ['$state', openStates] }, '$createdAt', new Date(dynts_neverExpires.getTime()),
324
+ ] } },
325
+ } },
326
+ ], this.options()).toArray();
327
+ const row = rows[0];
328
+ const observedAt = await this.databaseTime();
329
+ if (rows.length > 1 || (row && (!this.aggregateCount(row.pendingCount)
330
+ || !this.aggregateCount(row.dispatchingCount) || !this.aggregateCount(row.retryWaitingCount)
331
+ || !this.aggregateCount(row.succeededCount) || !this.aggregateCount(row.failedCount)
332
+ || !this.aggregateCount(row.dueCount) || !this.aggregateCount(row.expiredLeaseCount)
333
+ || !this.date(row.oldestOpenAt)))) {
334
+ throw this.error('INSPECTION_INVALID');
335
+ }
336
+ return Object.freeze({
337
+ contractVersion: 'dynts-application-alert-operations/1',
338
+ sourceSystem: this.settings.sourceSystem,
339
+ pendingCount: Number(row?.pendingCount ?? 0),
340
+ dispatchingCount: Number(row?.dispatchingCount ?? 0),
341
+ retryWaitingCount: Number(row?.retryWaitingCount ?? 0),
342
+ succeededCount: Number(row?.succeededCount ?? 0),
343
+ failedCount: Number(row?.failedCount ?? 0),
344
+ dueCount: Number(row?.dueCount ?? 0),
345
+ expiredLeaseCount: Number(row?.expiredLeaseCount ?? 0),
346
+ oldestOpenAt: row?.oldestOpenAt instanceof Date && row.oldestOpenAt.getTime() !== dynts_neverExpires.getTime()
347
+ ? row.oldestOpenAt.toISOString() : '',
348
+ observedAt: observedAt.toISOString(),
349
+ });
350
+ });
351
+ }
352
+ async retireExhausted() {
353
+ return this.collection().findOneAndUpdate({
354
+ sourceSystem: this.settings.sourceSystem,
355
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching,
356
+ attemptCount: { $gte: this.settings.maxAttempts },
357
+ $expr: { $lte: ['$leaseExpiresAt', '$$NOW'] },
358
+ }, [{ $set: {
359
+ state: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.failed,
360
+ leaseExpiresAt: new Date(0),
361
+ nextAttemptAt: '$$NOW',
362
+ lastOutcome: application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.terminalFailure,
363
+ lastResultCode: 'LEASE_EXPIRED_AFTER_FINAL_ATTEMPT',
364
+ deliveryReference: '',
365
+ updatedAt: '$$NOW',
366
+ completedAt: '$$NOW',
367
+ expiresAt: { $dateAdd: {
368
+ startDate: '$$NOW', unit: 'millisecond', amount: this.settings.retentionMs,
369
+ } },
370
+ } }], {
371
+ ...this.options(),
372
+ sort: { leaseExpiresAt: 1, _id: 1 },
373
+ returnDocument: 'after',
374
+ includeResultMetadata: false,
375
+ });
376
+ }
377
+ completion(value) {
378
+ if (!this.exactRecord(value, [
379
+ 'contractVersion', 'sourceSystem', 'idempotencyKey', 'fencingToken', 'claimToken', 'submission',
380
+ ])) {
381
+ throw this.error('COMPLETION_INVALID');
382
+ }
383
+ const submission = this.submission(value.submission);
384
+ if (value.contractVersion !== 'dynts-application-alert-completion/1'
385
+ || value.sourceSystem !== this.settings.sourceSystem
386
+ || typeof value.idempotencyKey !== 'string'
387
+ || !dynts_hashPattern.test(value.idempotencyKey)
388
+ || !Number.isSafeInteger(value.fencingToken)
389
+ || Number(value.fencingToken) < 1
390
+ || typeof value.claimToken !== 'string'
391
+ || !dynts_hashPattern.test(value.claimToken)) {
392
+ throw this.error('COMPLETION_INVALID');
393
+ }
394
+ return Object.freeze({
395
+ contractVersion: 'dynts-application-alert-completion/1',
396
+ sourceSystem: value.sourceSystem,
397
+ idempotencyKey: value.idempotencyKey,
398
+ fencingToken: Number(value.fencingToken),
399
+ claimToken: value.claimToken,
400
+ submission: submission,
401
+ });
402
+ }
403
+ submission(value) {
404
+ if (!this.exactRecord(value, ['contractVersion', 'outcome', 'resultCode', 'deliveryReference'])) {
405
+ throw this.error('SUBMISSION_INVALID');
406
+ }
407
+ const outcome = this.submissionOutcome(value.outcome);
408
+ if (value.contractVersion !== 'dynts-application-alert-submission/1'
409
+ || !outcome
410
+ || typeof value.resultCode !== 'string'
411
+ || !dynts_resultCodePattern.test(value.resultCode)
412
+ || value.resultCode.length > dynts_maxCodeLength
413
+ || typeof value.deliveryReference !== 'string'
414
+ || (outcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.accepted
415
+ ? !dynts_hashPattern.test(value.deliveryReference)
416
+ : value.deliveryReference !== '')) {
417
+ throw this.error('SUBMISSION_INVALID');
418
+ }
419
+ return Object.freeze({
420
+ contractVersion: 'dynts-application-alert-submission/1',
421
+ outcome: outcome,
422
+ resultCode: value.resultCode,
423
+ deliveryReference: value.deliveryReference,
424
+ });
425
+ }
426
+ submissionOutcome(value) {
427
+ if (value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.accepted
428
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.retryableFailure
429
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.terminalFailure) {
430
+ return value;
431
+ }
432
+ return null;
433
+ }
434
+ isExactCompletionReplay(record, completion, tokenHash) {
435
+ return record.fencingToken === completion.fencingToken
436
+ && record.claimTokenHash === tokenHash
437
+ && record.lastOutcome === completion.submission.outcome
438
+ && record.lastResultCode === completion.submission.resultCode
439
+ && record.deliveryReference === completion.submission.deliveryReference;
440
+ }
441
+ assertReplay(record, envelope, payloadChecksum) {
442
+ this.assertRecord(record);
443
+ if (record.sourceSystem !== envelope.sourceSystem
444
+ || record.idempotencyKey !== envelope.idempotencyKey
445
+ || record.payloadChecksum !== payloadChecksum) {
446
+ throw this.error('IDENTITY_PAYLOAD_CONFLICT');
447
+ }
448
+ }
449
+ assertRecord(record) {
450
+ const validation = application_alert_1.DyFM_ApplicationAlertEnvelope_Util.validate(record?.envelope);
451
+ if (!this.exactRecord(record, [
452
+ '_id', 'contractVersion', 'sourceSystem', 'idempotencyKey', 'payloadChecksum', 'envelope', 'state',
453
+ 'attemptCount', 'fencingToken', 'claimTokenHash', 'claimedAt', 'leaseExpiresAt', 'nextAttemptAt',
454
+ 'lastOutcome', 'lastResultCode', 'deliveryReference', 'createdAt', 'updatedAt', 'completedAt', 'expiresAt',
455
+ ])
456
+ || record.contractVersion !== 'dynts-application-alert-delivery/1'
457
+ || record._id !== this.key(record.sourceSystem, record.idempotencyKey)
458
+ || record.sourceSystem !== this.settings.sourceSystem
459
+ || !dynts_hashPattern.test(record.idempotencyKey)
460
+ || !dynts_hashPattern.test(record.payloadChecksum)
461
+ || !validation.valid
462
+ || validation.payloadChecksum !== record.payloadChecksum
463
+ || validation.envelope?.sourceSystem !== record.sourceSystem
464
+ || validation.envelope?.idempotencyKey !== record.idempotencyKey
465
+ || !this.deliveryState(record.state)
466
+ || !Number.isSafeInteger(record.attemptCount)
467
+ || record.attemptCount < 0
468
+ || record.attemptCount > this.settings.maxAttempts
469
+ || !Number.isSafeInteger(record.fencingToken)
470
+ || record.fencingToken < 0
471
+ || (record.claimTokenHash !== '' && !/^sha256:[a-f0-9]{64}$/u.test(record.claimTokenHash))
472
+ || !this.date(record.claimedAt)
473
+ || !this.date(record.leaseExpiresAt)
474
+ || !this.date(record.nextAttemptAt)
475
+ || !this.lastOutcome(record.lastOutcome)
476
+ || (record.lastResultCode !== '' && !dynts_resultCodePattern.test(record.lastResultCode))
477
+ || record.lastResultCode.length > dynts_maxCodeLength
478
+ || (record.deliveryReference !== '' && !dynts_hashPattern.test(record.deliveryReference))
479
+ || !this.date(record.createdAt)
480
+ || !this.date(record.updatedAt)
481
+ || !this.date(record.completedAt)
482
+ || !this.date(record.expiresAt)
483
+ || record.updatedAt.getTime() < record.createdAt.getTime()
484
+ || !this.recordStateValid(record)) {
485
+ throw this.error('INVALID_RECORD');
486
+ }
487
+ }
488
+ recordStateValid(record) {
489
+ const zero = 0;
490
+ const never = dynts_neverExpires.getTime();
491
+ const hasClaim = /^sha256:[a-f0-9]{64}$/u.test(record.claimTokenHash)
492
+ && record.attemptCount >= 1
493
+ && record.fencingToken === record.attemptCount
494
+ && record.claimedAt.getTime() >= record.createdAt.getTime();
495
+ if (record.state === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending) {
496
+ return record.attemptCount === 0 && record.fencingToken === 0 && record.claimTokenHash === ''
497
+ && record.claimedAt.getTime() === zero && record.leaseExpiresAt.getTime() === zero
498
+ && record.lastOutcome === '' && record.lastResultCode === '' && record.deliveryReference === ''
499
+ && record.completedAt.getTime() === zero && record.expiresAt.getTime() === never;
500
+ }
501
+ if (!hasClaim) {
502
+ return false;
503
+ }
504
+ if (record.state === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching) {
505
+ const priorOutcomeValid = (record.lastOutcome === '' && record.lastResultCode === '')
506
+ || (record.lastOutcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.retryableFailure
507
+ && record.lastResultCode !== '');
508
+ return record.leaseExpiresAt.getTime() > record.claimedAt.getTime()
509
+ && priorOutcomeValid && record.deliveryReference === ''
510
+ && record.completedAt.getTime() === zero && record.expiresAt.getTime() === never;
511
+ }
512
+ if (record.leaseExpiresAt.getTime() !== zero || record.lastResultCode === '') {
513
+ return false;
514
+ }
515
+ if (record.state === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting) {
516
+ return record.attemptCount < this.settings.maxAttempts
517
+ && record.lastOutcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.retryableFailure
518
+ && record.deliveryReference === '' && record.nextAttemptAt.getTime() > record.updatedAt.getTime()
519
+ && record.completedAt.getTime() === zero && record.expiresAt.getTime() === never;
520
+ }
521
+ const terminalTimeValid = record.completedAt.getTime() === record.updatedAt.getTime()
522
+ && record.expiresAt.getTime() > record.completedAt.getTime();
523
+ if (record.state === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.succeeded) {
524
+ return terminalTimeValid
525
+ && record.lastOutcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.accepted
526
+ && dynts_hashPattern.test(record.deliveryReference);
527
+ }
528
+ return terminalTimeValid
529
+ && (record.lastOutcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.retryableFailure
530
+ || record.lastOutcome === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type.terminalFailure)
531
+ && record.deliveryReference === '';
532
+ }
533
+ copyEnvelope(envelope) {
534
+ const validation = application_alert_1.DyFM_ApplicationAlertEnvelope_Util.validate(envelope);
535
+ if (!validation.valid || !validation.envelope) {
536
+ throw this.error('INVALID_RECORD');
537
+ }
538
+ return validation.envelope;
539
+ }
540
+ view(record, changed) {
541
+ this.assertRecord(record);
542
+ return Object.freeze({
543
+ contractVersion: 'dynts-application-alert-delivery-view/1',
544
+ sourceSystem: record.sourceSystem,
545
+ idempotencyKey: record.idempotencyKey,
546
+ payloadChecksum: record.payloadChecksum,
547
+ state: record.state,
548
+ attemptCount: record.attemptCount,
549
+ fencingToken: record.fencingToken,
550
+ lastOutcome: record.lastOutcome,
551
+ lastResultCode: record.lastResultCode,
552
+ deliveryReference: record.deliveryReference,
553
+ createdAt: record.createdAt.toISOString(),
554
+ updatedAt: record.updatedAt.toISOString(),
555
+ completedAt: record.completedAt.getTime() === 0 ? '' : record.completedAt.toISOString(),
556
+ changed: changed,
557
+ });
558
+ }
559
+ deliveryState(value) {
560
+ return value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.pending
561
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.dispatching
562
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.retryWaiting
563
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.succeeded
564
+ || value === application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type.failed;
565
+ }
566
+ lastOutcome(value) {
567
+ return value === '' || this.submissionOutcome(value) !== null;
568
+ }
569
+ exactRecord(value, expectedKeys) {
570
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
571
+ return false;
572
+ }
573
+ const prototype = Object.getPrototypeOf(value);
574
+ const keys = Reflect.ownKeys(value);
575
+ return (prototype === Object.prototype || prototype === null)
576
+ && keys.length === expectedKeys.length
577
+ && keys.every((key) => {
578
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
579
+ return typeof key === 'string'
580
+ && expectedKeys.includes(key)
581
+ && !!descriptor
582
+ && Object.hasOwn(descriptor, 'value')
583
+ && descriptor.enumerable === true;
584
+ });
585
+ }
586
+ key(sourceSystem, idempotencyKey) {
587
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([
588
+ 'dynts-application-alert-delivery/1', sourceSystem, idempotencyKey,
589
+ ])).digest('hex');
590
+ }
591
+ tokenHash(token) {
592
+ return `sha256:${(0, node_crypto_1.createHash)('sha256').update(token).digest('hex')}`;
593
+ }
594
+ async databaseTime() {
595
+ const result = await this.settings.connection.db.command({ hello: 1, maxTimeMS: this.settings.maxOperationTimeMs }, { timeoutMS: this.settings.maxOperationTimeMs });
596
+ if (!(result.localTime instanceof Date) || !Number.isFinite(result.localTime.getTime())) {
597
+ throw this.error('CLOCK_UNAVAILABLE');
598
+ }
599
+ return result.localTime;
600
+ }
601
+ positiveInteger(value, maximum) {
602
+ return Number.isSafeInteger(value) && Number(value) > 0 && Number(value) <= maximum;
603
+ }
604
+ aggregateCount(value) {
605
+ return Number.isSafeInteger(value) && Number(value) >= 0;
606
+ }
607
+ date(value) {
608
+ return value instanceof Date && Number.isFinite(value.getTime());
609
+ }
610
+ collection() {
611
+ return this.settings.connection.db.collection(DYNTS_APPLICATION_ALERT_DELIVERY_COLLECTION);
612
+ }
613
+ options() {
614
+ return { maxTimeMS: this.settings.maxOperationTimeMs };
615
+ }
616
+ assertReady() {
617
+ if (!this.isReady) {
618
+ throw this.error('NOT_READY');
619
+ }
620
+ }
621
+ async protect(code, task) {
622
+ try {
623
+ return await task();
624
+ }
625
+ catch (error) {
626
+ if (error instanceof Error && this.ownedErrors.has(error)) {
627
+ throw error;
628
+ }
629
+ throw this.error(code, error);
630
+ }
631
+ }
632
+ error(code, cause) {
633
+ const error = cause === undefined
634
+ ? new Error(`DyNTS|APPLICATION_ALERT_DELIVERY|${code}`)
635
+ : new Error(`DyNTS|APPLICATION_ALERT_DELIVERY|${code}`, { cause: cause });
636
+ this.ownedErrors.add(error);
637
+ return error;
638
+ }
639
+ isMongoCode(error, code) {
640
+ return error instanceof mongoose_1.mongo.MongoServerError && error.code === code;
641
+ }
642
+ validator() {
643
+ const hash = { bsonType: 'string', pattern: '^[a-f0-9]{64}$' };
644
+ const utc = { bsonType: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T.*\\.\\d{3}Z$' };
645
+ const evidence = { bsonType: 'object', additionalProperties: false,
646
+ required: ['code', 'value'], properties: {
647
+ code: {
648
+ bsonType: 'string', pattern: '^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+){0,15}$', maxLength: dynts_maxCodeLength,
649
+ },
650
+ value: { bsonType: ['int', 'long', 'double'], minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
651
+ } };
652
+ const envelope = { bsonType: 'object', additionalProperties: false, required: [
653
+ 'contractVersion', 'sourceSystem', 'environment', 'sourceRevision', 'imageDigest', 'alertCode', 'severity',
654
+ 'observedAt', 'windowStartedAt', 'windowEndedAt', 'policyVersion', 'evaluationState', 'evidence',
655
+ 'idempotencyKey',
656
+ ], properties: {
657
+ contractVersion: { enum: ['dyfm-application-alert/1'] },
658
+ sourceSystem: { bsonType: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$' },
659
+ environment: { bsonType: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$' },
660
+ sourceRevision: { bsonType: 'string', pattern: '^[a-f0-9]{40}$' },
661
+ imageDigest: { bsonType: 'string', pattern: '^sha256:[a-f0-9]{64}$' },
662
+ alertCode: {
663
+ bsonType: 'string', pattern: '^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+){2,15}$', maxLength: dynts_maxCodeLength,
664
+ },
665
+ severity: { enum: ['low', 'medium', 'high', 'critical'] },
666
+ observedAt: utc, windowStartedAt: utc, windowEndedAt: utc,
667
+ policyVersion: { bsonType: 'string', pattern: '^[a-z0-9][a-z0-9-]{1,63}/[1-9][0-9]{0,8}$' },
668
+ evaluationState: { enum: ['active', 'resolved'] },
669
+ evidence: { bsonType: 'array', minItems: 1, maxItems: 16, uniqueItems: true, items: evidence },
670
+ idempotencyKey: hash,
671
+ } };
672
+ const properties = {
673
+ _id: hash,
674
+ contractVersion: { enum: ['dynts-application-alert-delivery/1'] },
675
+ sourceSystem: { bsonType: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$' },
676
+ idempotencyKey: hash,
677
+ payloadChecksum: hash,
678
+ envelope: envelope,
679
+ state: { enum: Object.values(application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertDeliveryState_Type) },
680
+ attemptCount: { bsonType: ['int', 'long', 'double'], minimum: 0, maximum: 10 },
681
+ fencingToken: { bsonType: ['int', 'long', 'double'], minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
682
+ claimTokenHash: { bsonType: 'string', pattern: '^(?:|sha256:[a-f0-9]{64})$' },
683
+ claimedAt: { bsonType: 'date' },
684
+ leaseExpiresAt: { bsonType: 'date' },
685
+ nextAttemptAt: { bsonType: 'date' },
686
+ lastOutcome: { enum: ['', ...Object.values(application_alert_delivery_state_type_enum_1.DyNTS_ApplicationAlertSubmissionOutcome_Type)] },
687
+ lastResultCode: {
688
+ bsonType: 'string', pattern: '^(?:|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+){0,15})$',
689
+ maxLength: dynts_maxCodeLength,
690
+ },
691
+ deliveryReference: { bsonType: 'string', pattern: '^(?:|[a-f0-9]{64})$' },
692
+ createdAt: { bsonType: 'date' },
693
+ updatedAt: { bsonType: 'date' },
694
+ completedAt: { bsonType: 'date' },
695
+ expiresAt: { bsonType: 'date' },
696
+ };
697
+ return { $jsonSchema: { bsonType: 'object', additionalProperties: false,
698
+ required: Object.keys(properties), properties: properties } };
699
+ }
700
+ }
701
+ exports.DyNTS_ApplicationAlertDelivery_MongoStore = DyNTS_ApplicationAlertDelivery_MongoStore;
702
+ //# sourceMappingURL=application-alert-delivery.mongo-store.js.map