@objectstack/service-messaging 7.4.1 → 7.6.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/index.cjs +1062 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5266 -31
- package/dist/index.d.ts +5266 -31
- package/dist/index.js +1050 -183
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -20,13 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
DEFAULT_HTTP_TIMEOUT_MS: () => DEFAULT_HTTP_TIMEOUT_MS,
|
|
23
24
|
DEFAULT_LOCALE: () => DEFAULT_LOCALE,
|
|
24
25
|
DEFAULT_RETENTION_TARGETS: () => DEFAULT_RETENTION_TARGETS,
|
|
25
26
|
DELIVERY_OBJECT: () => DELIVERY_OBJECT,
|
|
26
27
|
EMAIL_USER_OBJECT: () => USER_OBJECT2,
|
|
28
|
+
HttpDelivery: () => HttpDelivery,
|
|
29
|
+
HttpDispatcher: () => HttpDispatcher,
|
|
30
|
+
HttpRedeliverError: () => HttpRedeliverError,
|
|
27
31
|
INBOX_OBJECT: () => INBOX_OBJECT,
|
|
28
32
|
InboxMessage: () => InboxMessage,
|
|
29
33
|
MEMBER_OBJECT: () => MEMBER_OBJECT,
|
|
34
|
+
MemoryHttpOutbox: () => MemoryHttpOutbox,
|
|
30
35
|
MemoryNotificationOutbox: () => MemoryNotificationOutbox,
|
|
31
36
|
MessagingService: () => MessagingService,
|
|
32
37
|
MessagingServicePlugin: () => MessagingServicePlugin,
|
|
@@ -43,23 +48,29 @@ __export(index_exports, {
|
|
|
43
48
|
PreferenceResolver: () => PreferenceResolver,
|
|
44
49
|
RECEIPT_OBJECT: () => RECEIPT_OBJECT,
|
|
45
50
|
RecipientResolver: () => RecipientResolver,
|
|
51
|
+
SYS_HTTP_DELIVERY: () => SYS_HTTP_DELIVERY,
|
|
52
|
+
SqlHttpOutbox: () => SqlHttpOutbox,
|
|
46
53
|
SqlNotificationOutbox: () => SqlNotificationOutbox,
|
|
47
54
|
TEAM_MEMBER_OBJECT: () => TEAM_MEMBER_OBJECT,
|
|
48
55
|
TEMPLATE_OBJECT: () => TEMPLATE_OBJECT,
|
|
49
56
|
USER_OBJECT: () => USER_OBJECT,
|
|
50
57
|
classifyDeliveryAttempt: () => classifyDeliveryAttempt,
|
|
58
|
+
classifyHttpAttempt: () => classifyAttempt,
|
|
51
59
|
createEmailChannel: () => createEmailChannel,
|
|
52
60
|
createInboxChannel: () => createInboxChannel,
|
|
53
61
|
hashPartition: () => hashPartition,
|
|
54
62
|
interpolate: () => interpolate,
|
|
63
|
+
newHttpDeliveryId: () => newDeliveryId,
|
|
64
|
+
nextHttpRetryDelayMs: () => nextHttpRetryDelayMs,
|
|
55
65
|
nextRetryDelayMs: () => nextRetryDelayMs,
|
|
56
66
|
quietHoursDeferral: () => quietHoursDeferral,
|
|
57
|
-
renderNotification: () => renderNotification
|
|
67
|
+
renderNotification: () => renderNotification,
|
|
68
|
+
sendHttpOnce: () => sendOnce
|
|
58
69
|
});
|
|
59
70
|
module.exports = __toCommonJS(index_exports);
|
|
60
71
|
|
|
61
72
|
// src/messaging-service-plugin.ts
|
|
62
|
-
var
|
|
73
|
+
var import_node_crypto4 = require("crypto");
|
|
63
74
|
|
|
64
75
|
// src/recipient-resolver.ts
|
|
65
76
|
function looksLikeEmail(s) {
|
|
@@ -350,8 +361,81 @@ function msg2(err) {
|
|
|
350
361
|
return err?.message ?? String(err);
|
|
351
362
|
}
|
|
352
363
|
|
|
364
|
+
// src/inbox-channel.ts
|
|
365
|
+
var INBOX_OBJECT = "sys_inbox_message";
|
|
366
|
+
var RECEIPT_OBJECT = "sys_notification_receipt";
|
|
367
|
+
function createInboxChannel(opts) {
|
|
368
|
+
const objectName = opts.objectName ?? INBOX_OBJECT;
|
|
369
|
+
const receiptObject = opts.receiptObject ?? RECEIPT_OBJECT;
|
|
370
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
371
|
+
async function writeDeliveredReceipt(ctx, data, r) {
|
|
372
|
+
if (!r.notificationId) return;
|
|
373
|
+
try {
|
|
374
|
+
await data.insert(receiptObject, {
|
|
375
|
+
notification_id: r.notificationId,
|
|
376
|
+
delivery_id: null,
|
|
377
|
+
user_id: r.userId,
|
|
378
|
+
channel: "inbox",
|
|
379
|
+
state: "delivered",
|
|
380
|
+
at: r.at,
|
|
381
|
+
organization_id: r.organizationId ?? null,
|
|
382
|
+
created_at: r.at
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
ctx.logger.warn(
|
|
386
|
+
`[inbox] delivered receipt write failed for '${r.userId}' (${err.message}); inbox row stands`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
id: "inbox",
|
|
392
|
+
async send(ctx, delivery) {
|
|
393
|
+
const data = opts.getData();
|
|
394
|
+
const n = delivery.notification;
|
|
395
|
+
if (!data) {
|
|
396
|
+
ctx.logger.warn(
|
|
397
|
+
`[inbox] no data engine registered; inbox row for '${delivery.recipient}' not persisted`
|
|
398
|
+
);
|
|
399
|
+
return { ok: true };
|
|
400
|
+
}
|
|
401
|
+
const userId = delivery.recipient;
|
|
402
|
+
const at = now();
|
|
403
|
+
const row = {
|
|
404
|
+
user_id: userId,
|
|
405
|
+
notification_id: n.notificationId ?? null,
|
|
406
|
+
topic: n.topic,
|
|
407
|
+
title: n.title,
|
|
408
|
+
body_md: n.body,
|
|
409
|
+
severity: n.severity ?? "info",
|
|
410
|
+
action_url: n.actionUrl,
|
|
411
|
+
organization_id: n.organizationId ?? null,
|
|
412
|
+
created_at: at
|
|
413
|
+
};
|
|
414
|
+
let inboxId;
|
|
415
|
+
try {
|
|
416
|
+
const created = await data.insert(objectName, row);
|
|
417
|
+
const id = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
|
|
418
|
+
inboxId = id != null ? String(id) : void 0;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
return { ok: false, error: `inbox insert failed: ${err.message}` };
|
|
421
|
+
}
|
|
422
|
+
await writeDeliveredReceipt(ctx, data, {
|
|
423
|
+
notificationId: n.notificationId,
|
|
424
|
+
userId,
|
|
425
|
+
organizationId: n.organizationId,
|
|
426
|
+
at
|
|
427
|
+
});
|
|
428
|
+
return { ok: true, externalId: inboxId };
|
|
429
|
+
},
|
|
430
|
+
classifyError(_err) {
|
|
431
|
+
return "retryable";
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
353
436
|
// src/messaging-service.ts
|
|
354
437
|
var NOTIFICATION_EVENT_OBJECT = "sys_notification";
|
|
438
|
+
var READ_RECEIPT_STATES = /* @__PURE__ */ new Set(["read", "clicked", "dismissed"]);
|
|
355
439
|
var MessagingService = class {
|
|
356
440
|
constructor(ctx) {
|
|
357
441
|
this.ctx = ctx;
|
|
@@ -373,6 +457,49 @@ var MessagingService = class {
|
|
|
373
457
|
setOutbox(outbox) {
|
|
374
458
|
this.outbox = outbox;
|
|
375
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Attach the generic outbound-HTTP delivery outbox (ADR-0018 M3). Wired by
|
|
462
|
+
* the plugin at `kernel:ready` once the data engine is resolvable. Once set,
|
|
463
|
+
* {@link enqueueHttp} persists durable rows the {@link HttpDispatcher}
|
|
464
|
+
* drains with retry / dead-letter; the Flow `http` node enqueues through it.
|
|
465
|
+
*/
|
|
466
|
+
setHttpOutbox(outbox) {
|
|
467
|
+
this.httpOutbox = outbox;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Whether durable HTTP delivery is available. Callers (e.g. the `http` node)
|
|
471
|
+
* fall back to a direct, non-durable send when this is `false`.
|
|
472
|
+
*/
|
|
473
|
+
isHttpDeliveryReady() {
|
|
474
|
+
return this.httpOutbox !== void 0;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Enqueue a durable outbound-HTTP delivery (ADR-0018 M3). Returns the row id.
|
|
478
|
+
* Throws if no HTTP outbox is wired — guard with {@link isHttpDeliveryReady}.
|
|
479
|
+
*/
|
|
480
|
+
async enqueueHttp(input) {
|
|
481
|
+
if (!this.httpOutbox) {
|
|
482
|
+
throw new Error("messaging: HTTP delivery outbox not configured (no data engine / reliableDelivery off)");
|
|
483
|
+
}
|
|
484
|
+
return this.httpOutbox.enqueue(input);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Reset a terminal HTTP delivery row back to `pending` so the dispatcher
|
|
488
|
+
* re-sends it (ADR-0018 M3). Backs the webhook redeliver admin endpoint.
|
|
489
|
+
* Throws if no HTTP outbox is wired, or `HttpRedeliverError` for a missing /
|
|
490
|
+
* non-terminal row.
|
|
491
|
+
*/
|
|
492
|
+
async redeliverHttp(id) {
|
|
493
|
+
if (!this.httpOutbox) {
|
|
494
|
+
throw new Error("messaging: HTTP delivery outbox not configured");
|
|
495
|
+
}
|
|
496
|
+
return this.httpOutbox.redeliver(id);
|
|
497
|
+
}
|
|
498
|
+
/** List HTTP delivery rows (admin/tests). Empty when no outbox is wired. */
|
|
499
|
+
async listHttp(filter) {
|
|
500
|
+
if (!this.httpOutbox) return [];
|
|
501
|
+
return this.httpOutbox.list(filter);
|
|
502
|
+
}
|
|
376
503
|
/** Register a channel implementation. A duplicate id warns and replaces. */
|
|
377
504
|
registerChannel(channel) {
|
|
378
505
|
if (this.channels.has(channel.id)) {
|
|
@@ -393,6 +520,119 @@ var MessagingService = class {
|
|
|
393
520
|
getRegisteredChannels() {
|
|
394
521
|
return [...this.channels.keys()];
|
|
395
522
|
}
|
|
523
|
+
/* ------------------------------------------------------------------ */
|
|
524
|
+
/* Inbox read API (ADR-0030) — backs /api/v1/notifications */
|
|
525
|
+
/* */
|
|
526
|
+
/* The REST notification surface reads the L5 `sys_inbox_message` */
|
|
527
|
+
/* materialization joined with the `sys_notification_receipt` */
|
|
528
|
+
/* read-state spine — NOT the re-modeled `sys_notification` L2 event */
|
|
529
|
+
/* (which carries no recipient/read columns). Mark-read upserts the */
|
|
530
|
+
/* receipt, keyed `(notification_id, user_id, channel:'inbox')`. */
|
|
531
|
+
/* ------------------------------------------------------------------ */
|
|
532
|
+
/**
|
|
533
|
+
* List the signed-in user's in-app inbox, joined with read-state.
|
|
534
|
+
*
|
|
535
|
+
* A message is unread until its event has a `read`/`clicked`/`dismissed`
|
|
536
|
+
* receipt; the `read` filter (when given) is applied in-memory after the
|
|
537
|
+
* join. `unreadCount` is computed over the fetched window (bounded by
|
|
538
|
+
* `limit`, like the Console bell's poll). Returns the REST contract shape
|
|
539
|
+
* (`ListNotificationsResponseSchema`): `{ notifications, unreadCount }`.
|
|
540
|
+
*/
|
|
541
|
+
async listInbox(userId, opts = {}) {
|
|
542
|
+
const data = this.ctx.getData?.();
|
|
543
|
+
if (!data || !userId) return { notifications: [], unreadCount: 0 };
|
|
544
|
+
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
|
|
545
|
+
const where = { user_id: userId };
|
|
546
|
+
if (opts.type) where.topic = opts.type;
|
|
547
|
+
const [rows, receipts] = await Promise.all([
|
|
548
|
+
data.find(INBOX_OBJECT, { where, orderBy: [{ field: "created_at", order: "desc" }], limit }),
|
|
549
|
+
// Read-state spine. Best-effort: if receipts are unavailable the
|
|
550
|
+
// inbox still lists (everything reads as unread) rather than erroring.
|
|
551
|
+
data.find(RECEIPT_OBJECT, { where: { user_id: userId, channel: "inbox" } }).catch(() => [])
|
|
552
|
+
]);
|
|
553
|
+
const stateByNotif = /* @__PURE__ */ new Map();
|
|
554
|
+
for (const r of receipts) {
|
|
555
|
+
const nid = r?.notification_id != null ? String(r.notification_id) : "";
|
|
556
|
+
if (!nid) continue;
|
|
557
|
+
const state = String(r.state ?? "delivered");
|
|
558
|
+
const prev = stateByNotif.get(nid);
|
|
559
|
+
if (!prev || !READ_RECEIPT_STATES.has(prev) && READ_RECEIPT_STATES.has(state)) {
|
|
560
|
+
stateByNotif.set(nid, state);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let unreadCount = 0;
|
|
564
|
+
const all = rows.map((m) => {
|
|
565
|
+
const nid = m?.notification_id != null ? String(m.notification_id) : null;
|
|
566
|
+
const state = nid ? stateByNotif.get(nid) : void 0;
|
|
567
|
+
const read = state ? READ_RECEIPT_STATES.has(state) : false;
|
|
568
|
+
if (!read) unreadCount += 1;
|
|
569
|
+
return {
|
|
570
|
+
id: nid ?? String(m.id),
|
|
571
|
+
type: m.topic ?? "notification",
|
|
572
|
+
title: m.title ?? "",
|
|
573
|
+
body: m.body_md ?? "",
|
|
574
|
+
read,
|
|
575
|
+
actionUrl: m.action_url ?? void 0,
|
|
576
|
+
createdAt: m.created_at ?? this.now()
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
const notifications = opts.read === void 0 ? all : all.filter((n) => n.read === opts.read);
|
|
580
|
+
return { notifications, unreadCount };
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Mark specific notifications read by upserting their inbox receipts to
|
|
584
|
+
* `read`. Updates the existing `delivered` receipt in place (keyed
|
|
585
|
+
* `(notification_id, user_id, channel:'inbox')`); inserts one only when
|
|
586
|
+
* absent. `ids` are notification (event) ids. Returns the REST contract
|
|
587
|
+
* shape (`MarkNotificationsReadResponseSchema`): `{ success, readCount }`.
|
|
588
|
+
*/
|
|
589
|
+
async markRead(userId, ids) {
|
|
590
|
+
const data = this.ctx.getData?.();
|
|
591
|
+
if (!data || !userId || !ids?.length) return { success: true, readCount: 0 };
|
|
592
|
+
const at = this.now();
|
|
593
|
+
let readCount = 0;
|
|
594
|
+
for (const id of ids) {
|
|
595
|
+
const nid = String(id ?? "");
|
|
596
|
+
if (!nid) continue;
|
|
597
|
+
try {
|
|
598
|
+
readCount += await this.upsertReadReceipt(data, userId, nid, at);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
this.ctx.logger.warn(`[messaging] markRead failed for '${nid}': ${err.message}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return { success: true, readCount };
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Mark every currently-unread inbox message for the user as read. Returns
|
|
607
|
+
* `{ success, readCount }` (`MarkAllNotificationsReadResponseSchema`).
|
|
608
|
+
*/
|
|
609
|
+
async markAllRead(userId) {
|
|
610
|
+
const data = this.ctx.getData?.();
|
|
611
|
+
if (!data || !userId) return { success: true, readCount: 0 };
|
|
612
|
+
const { notifications } = await this.listInbox(userId, { read: false, limit: 200 });
|
|
613
|
+
return this.markRead(userId, notifications.map((n) => n.id));
|
|
614
|
+
}
|
|
615
|
+
/** Upsert a `read` receipt for one notification; returns 1 when it persisted. */
|
|
616
|
+
async upsertReadReceipt(data, userId, notificationId, at) {
|
|
617
|
+
const existing = await data.findOne(RECEIPT_OBJECT, {
|
|
618
|
+
where: { notification_id: notificationId, user_id: userId, channel: "inbox" },
|
|
619
|
+
fields: ["id"]
|
|
620
|
+
});
|
|
621
|
+
if (existing?.id) {
|
|
622
|
+
await data.update(RECEIPT_OBJECT, { state: "read", at }, { where: { id: existing.id } });
|
|
623
|
+
} else {
|
|
624
|
+
await data.insert(RECEIPT_OBJECT, {
|
|
625
|
+
notification_id: notificationId,
|
|
626
|
+
delivery_id: null,
|
|
627
|
+
user_id: userId,
|
|
628
|
+
channel: "inbox",
|
|
629
|
+
state: "read",
|
|
630
|
+
at,
|
|
631
|
+
created_at: at
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return 1;
|
|
635
|
+
}
|
|
396
636
|
/**
|
|
397
637
|
* The single notification ingress. Writes the L2 event, resolves the
|
|
398
638
|
* audience, and fans the result out to its channels. An unregistered
|
|
@@ -600,78 +840,6 @@ function actionUrlFor(input, payload) {
|
|
|
600
840
|
return obj && id ? `/${obj}/${id}` : void 0;
|
|
601
841
|
}
|
|
602
842
|
|
|
603
|
-
// src/inbox-channel.ts
|
|
604
|
-
var INBOX_OBJECT = "sys_inbox_message";
|
|
605
|
-
var RECEIPT_OBJECT = "sys_notification_receipt";
|
|
606
|
-
function createInboxChannel(opts) {
|
|
607
|
-
const objectName = opts.objectName ?? INBOX_OBJECT;
|
|
608
|
-
const receiptObject = opts.receiptObject ?? RECEIPT_OBJECT;
|
|
609
|
-
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
610
|
-
async function writeDeliveredReceipt(ctx, data, r) {
|
|
611
|
-
if (!r.notificationId) return;
|
|
612
|
-
try {
|
|
613
|
-
await data.insert(receiptObject, {
|
|
614
|
-
notification_id: r.notificationId,
|
|
615
|
-
delivery_id: null,
|
|
616
|
-
user_id: r.userId,
|
|
617
|
-
channel: "inbox",
|
|
618
|
-
state: "delivered",
|
|
619
|
-
at: r.at,
|
|
620
|
-
organization_id: r.organizationId ?? null,
|
|
621
|
-
created_at: r.at
|
|
622
|
-
});
|
|
623
|
-
} catch (err) {
|
|
624
|
-
ctx.logger.warn(
|
|
625
|
-
`[inbox] delivered receipt write failed for '${r.userId}' (${err.message}); inbox row stands`
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
id: "inbox",
|
|
631
|
-
async send(ctx, delivery) {
|
|
632
|
-
const data = opts.getData();
|
|
633
|
-
const n = delivery.notification;
|
|
634
|
-
if (!data) {
|
|
635
|
-
ctx.logger.warn(
|
|
636
|
-
`[inbox] no data engine registered; inbox row for '${delivery.recipient}' not persisted`
|
|
637
|
-
);
|
|
638
|
-
return { ok: true };
|
|
639
|
-
}
|
|
640
|
-
const userId = delivery.recipient;
|
|
641
|
-
const at = now();
|
|
642
|
-
const row = {
|
|
643
|
-
user_id: userId,
|
|
644
|
-
notification_id: n.notificationId ?? null,
|
|
645
|
-
topic: n.topic,
|
|
646
|
-
title: n.title,
|
|
647
|
-
body_md: n.body,
|
|
648
|
-
severity: n.severity ?? "info",
|
|
649
|
-
action_url: n.actionUrl,
|
|
650
|
-
organization_id: n.organizationId ?? null,
|
|
651
|
-
created_at: at
|
|
652
|
-
};
|
|
653
|
-
let inboxId;
|
|
654
|
-
try {
|
|
655
|
-
const created = await data.insert(objectName, row);
|
|
656
|
-
const id = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
|
|
657
|
-
inboxId = id != null ? String(id) : void 0;
|
|
658
|
-
} catch (err) {
|
|
659
|
-
return { ok: false, error: `inbox insert failed: ${err.message}` };
|
|
660
|
-
}
|
|
661
|
-
await writeDeliveredReceipt(ctx, data, {
|
|
662
|
-
notificationId: n.notificationId,
|
|
663
|
-
userId,
|
|
664
|
-
organizationId: n.organizationId,
|
|
665
|
-
at
|
|
666
|
-
});
|
|
667
|
-
return { ok: true, externalId: inboxId };
|
|
668
|
-
},
|
|
669
|
-
classifyError(_err) {
|
|
670
|
-
return "retryable";
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
843
|
// src/sql-outbox.ts
|
|
676
844
|
var import_node_crypto = require("crypto");
|
|
677
845
|
|
|
@@ -737,9 +905,297 @@ var SqlNotificationOutbox = class {
|
|
|
737
905
|
partition_key: hashPartition(input.notificationId, this.partitionCount),
|
|
738
906
|
status: "pending",
|
|
739
907
|
attempts: 0,
|
|
740
|
-
// Deferred dispatch (quiet-hours, P3): claim() skips pending rows
|
|
741
|
-
// whose next_attempt_at is in the future.
|
|
742
|
-
next_attempt_at: input.notBefore ?? null,
|
|
908
|
+
// Deferred dispatch (quiet-hours, P3): claim() skips pending rows
|
|
909
|
+
// whose next_attempt_at is in the future.
|
|
910
|
+
next_attempt_at: input.notBefore ?? null,
|
|
911
|
+
created_at: now,
|
|
912
|
+
updated_at: now
|
|
913
|
+
};
|
|
914
|
+
try {
|
|
915
|
+
await this.engine.insert(this.objectName, row);
|
|
916
|
+
return id;
|
|
917
|
+
} catch (err) {
|
|
918
|
+
const winner = await this.engine.findOne(this.objectName, { where: dedup, fields: ["id"] });
|
|
919
|
+
if (winner?.id) return String(winner.id);
|
|
920
|
+
throw err;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async claim(opts) {
|
|
924
|
+
const now = opts.now ?? Date.now();
|
|
925
|
+
await this.engine.update(
|
|
926
|
+
this.objectName,
|
|
927
|
+
{ status: "pending", claimed_by: null, claimed_at: null, updated_at: now },
|
|
928
|
+
{ where: { status: "in_flight", claimed_at: { $lt: now - opts.claimTtlMs } }, multi: true }
|
|
929
|
+
);
|
|
930
|
+
const partitionFilter = opts.partition ? { partition_key: opts.partition.index } : {};
|
|
931
|
+
const candidates = await this.engine.find(this.objectName, {
|
|
932
|
+
where: {
|
|
933
|
+
status: "pending",
|
|
934
|
+
...partitionFilter,
|
|
935
|
+
$or: [{ next_attempt_at: null }, { next_attempt_at: { $lte: now } }]
|
|
936
|
+
},
|
|
937
|
+
fields: ["id"],
|
|
938
|
+
limit: opts.limit
|
|
939
|
+
});
|
|
940
|
+
if (!candidates.length) return [];
|
|
941
|
+
const ids = candidates.map((c) => c.id);
|
|
942
|
+
await this.engine.update(
|
|
943
|
+
this.objectName,
|
|
944
|
+
{ status: "in_flight", claimed_by: opts.nodeId, claimed_at: now, updated_at: now },
|
|
945
|
+
{ where: { id: { $in: ids }, status: "pending" }, multi: true }
|
|
946
|
+
);
|
|
947
|
+
const claimed = await this.engine.find(this.objectName, {
|
|
948
|
+
where: { id: { $in: ids }, claimed_by: opts.nodeId, claimed_at: now, status: "in_flight" }
|
|
949
|
+
});
|
|
950
|
+
return claimed.map((r) => this.toRecord(r));
|
|
951
|
+
}
|
|
952
|
+
async ack(id, result) {
|
|
953
|
+
const current = await this.engine.findOne(this.objectName, {
|
|
954
|
+
where: { id },
|
|
955
|
+
fields: ["attempts"]
|
|
956
|
+
});
|
|
957
|
+
if (!current) return;
|
|
958
|
+
const now = Date.now();
|
|
959
|
+
let status;
|
|
960
|
+
let nextAttemptAt = null;
|
|
961
|
+
let error = null;
|
|
962
|
+
if (result.success) {
|
|
963
|
+
status = "success";
|
|
964
|
+
} else if (result.suppressed) {
|
|
965
|
+
status = "suppressed";
|
|
966
|
+
error = result.error ?? null;
|
|
967
|
+
} else if (result.dead) {
|
|
968
|
+
status = "dead";
|
|
969
|
+
error = result.error ?? null;
|
|
970
|
+
} else {
|
|
971
|
+
status = "pending";
|
|
972
|
+
nextAttemptAt = result.nextAttemptAt ?? null;
|
|
973
|
+
error = result.error ?? null;
|
|
974
|
+
}
|
|
975
|
+
await this.engine.update(
|
|
976
|
+
this.objectName,
|
|
977
|
+
{
|
|
978
|
+
status,
|
|
979
|
+
attempts: (current.attempts ?? 0) + 1,
|
|
980
|
+
last_attempted_at: now,
|
|
981
|
+
claimed_by: null,
|
|
982
|
+
claimed_at: null,
|
|
983
|
+
next_attempt_at: nextAttemptAt,
|
|
984
|
+
error,
|
|
985
|
+
updated_at: now
|
|
986
|
+
},
|
|
987
|
+
{ where: { id }, multi: false }
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
async list(filter) {
|
|
991
|
+
const where = {};
|
|
992
|
+
if (filter?.status) where.status = filter.status;
|
|
993
|
+
if (filter?.notificationId) where.notification_id = filter.notificationId;
|
|
994
|
+
const rows = await this.engine.find(this.objectName, { where });
|
|
995
|
+
return rows.map((r) => this.toRecord(r));
|
|
996
|
+
}
|
|
997
|
+
toRecord(r) {
|
|
998
|
+
let payload = r.payload ?? {};
|
|
999
|
+
if (typeof payload === "string") {
|
|
1000
|
+
try {
|
|
1001
|
+
payload = JSON.parse(payload);
|
|
1002
|
+
} catch {
|
|
1003
|
+
payload = {};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
id: r.id,
|
|
1008
|
+
notificationId: r.notification_id,
|
|
1009
|
+
recipientId: r.recipient_id,
|
|
1010
|
+
channel: r.channel,
|
|
1011
|
+
topic: r.topic ?? void 0,
|
|
1012
|
+
payload,
|
|
1013
|
+
organizationId: r.organization_id ?? void 0,
|
|
1014
|
+
partitionKey: r.partition_key,
|
|
1015
|
+
status: r.status,
|
|
1016
|
+
attempts: r.attempts,
|
|
1017
|
+
claimedBy: r.claimed_by ?? void 0,
|
|
1018
|
+
claimedAt: r.claimed_at ?? void 0,
|
|
1019
|
+
nextAttemptAt: r.next_attempt_at ?? void 0,
|
|
1020
|
+
lastAttemptedAt: r.last_attempted_at ?? void 0,
|
|
1021
|
+
error: r.error ?? void 0,
|
|
1022
|
+
createdAt: r.created_at,
|
|
1023
|
+
updatedAt: r.updated_at
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// src/sql-http-outbox.ts
|
|
1029
|
+
var import_node_crypto2 = require("crypto");
|
|
1030
|
+
|
|
1031
|
+
// src/http-outbox.ts
|
|
1032
|
+
var HttpRedeliverError = class extends Error {
|
|
1033
|
+
constructor(message, code) {
|
|
1034
|
+
super(message);
|
|
1035
|
+
this.code = code;
|
|
1036
|
+
this.name = "HttpRedeliverError";
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/objects/http-delivery.object.ts
|
|
1041
|
+
var import_data = require("@objectstack/spec/data");
|
|
1042
|
+
var HttpDelivery = import_data.ObjectSchema.create({
|
|
1043
|
+
name: "sys_http_delivery",
|
|
1044
|
+
label: "HTTP Delivery",
|
|
1045
|
+
pluralLabel: "HTTP Deliveries",
|
|
1046
|
+
icon: "globe",
|
|
1047
|
+
isSystem: true,
|
|
1048
|
+
managedBy: "system",
|
|
1049
|
+
userActions: { create: false, edit: false, delete: false, import: false },
|
|
1050
|
+
description: "Durable outbox row for one outbound-HTTP attempt (ADR-0018). Managed by @objectstack/service-messaging; do not write directly.",
|
|
1051
|
+
displayNameField: "id",
|
|
1052
|
+
titleFormat: "{label} \u2192 {url}",
|
|
1053
|
+
compactLayout: ["source", "url", "status", "attempts", "next_retry_at"],
|
|
1054
|
+
listViews: {
|
|
1055
|
+
recent: {
|
|
1056
|
+
type: "grid",
|
|
1057
|
+
name: "recent",
|
|
1058
|
+
label: "Recent",
|
|
1059
|
+
data: { provider: "object", object: "sys_http_delivery" },
|
|
1060
|
+
columns: ["source", "label", "url", "status", "attempts", "response_code", "updated_at"],
|
|
1061
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
1062
|
+
pagination: { pageSize: 50 }
|
|
1063
|
+
},
|
|
1064
|
+
failures: {
|
|
1065
|
+
type: "grid",
|
|
1066
|
+
name: "failures",
|
|
1067
|
+
label: "Failures",
|
|
1068
|
+
data: { provider: "object", object: "sys_http_delivery" },
|
|
1069
|
+
columns: ["source", "url", "status", "attempts", "response_code", "error", "updated_at"],
|
|
1070
|
+
filter: [{ field: "status", operator: "in", value: ["failed", "dead"] }],
|
|
1071
|
+
sort: [{ field: "updated_at", order: "desc" }],
|
|
1072
|
+
pagination: { pageSize: 50 }
|
|
1073
|
+
},
|
|
1074
|
+
pending: {
|
|
1075
|
+
type: "grid",
|
|
1076
|
+
name: "pending",
|
|
1077
|
+
label: "Pending",
|
|
1078
|
+
data: { provider: "object", object: "sys_http_delivery" },
|
|
1079
|
+
columns: ["source", "url", "attempts", "next_retry_at", "updated_at"],
|
|
1080
|
+
filter: [{ field: "status", operator: "equals", value: "pending" }],
|
|
1081
|
+
sort: [{ field: "next_retry_at", order: "asc" }],
|
|
1082
|
+
pagination: { pageSize: 50 }
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
fields: {
|
|
1086
|
+
id: import_data.Field.text({
|
|
1087
|
+
label: "Delivery ID",
|
|
1088
|
+
required: true,
|
|
1089
|
+
maxLength: 64,
|
|
1090
|
+
description: "UUID \u2014 also doubles as the receiver-side idempotency key"
|
|
1091
|
+
}),
|
|
1092
|
+
source: import_data.Field.text({
|
|
1093
|
+
label: "Source",
|
|
1094
|
+
required: true,
|
|
1095
|
+
maxLength: 32,
|
|
1096
|
+
description: "Provenance domain, e.g. 'webhook' | 'flow'. UNIQUE(source, dedup_key)."
|
|
1097
|
+
}),
|
|
1098
|
+
ref_id: import_data.Field.text({
|
|
1099
|
+
label: "Ref ID",
|
|
1100
|
+
required: true,
|
|
1101
|
+
maxLength: 128,
|
|
1102
|
+
description: "Partition/ordering anchor within source (webhook id, flow id, \u2026)"
|
|
1103
|
+
}),
|
|
1104
|
+
dedup_key: import_data.Field.text({
|
|
1105
|
+
label: "Dedup Key",
|
|
1106
|
+
required: true,
|
|
1107
|
+
maxLength: 191,
|
|
1108
|
+
description: "UNIQUE(source, dedup_key) for at-most-once enqueue"
|
|
1109
|
+
}),
|
|
1110
|
+
label: import_data.Field.text({
|
|
1111
|
+
label: "Label",
|
|
1112
|
+
required: false,
|
|
1113
|
+
maxLength: 191,
|
|
1114
|
+
description: "Diagnostic label / event type \u2014 surfaced on X-Objectstack-Event"
|
|
1115
|
+
}),
|
|
1116
|
+
url: import_data.Field.text({
|
|
1117
|
+
label: "Target URL",
|
|
1118
|
+
required: true,
|
|
1119
|
+
maxLength: 2048,
|
|
1120
|
+
description: "Snapshotted at enqueue so config edits do not rewrite live rows"
|
|
1121
|
+
}),
|
|
1122
|
+
method: import_data.Field.text({ label: "Method", required: false, maxLength: 10 }),
|
|
1123
|
+
headers_json: import_data.Field.textarea({ label: "Headers JSON", required: false }),
|
|
1124
|
+
signing_secret: import_data.Field.text({ label: "HMAC Secret", required: false, maxLength: 256 }),
|
|
1125
|
+
timeout_ms: import_data.Field.number({ label: "Timeout (ms)", required: false }),
|
|
1126
|
+
payload_json: import_data.Field.textarea({ label: "Payload JSON", required: true }),
|
|
1127
|
+
partition_key: import_data.Field.number({
|
|
1128
|
+
label: "Partition",
|
|
1129
|
+
required: true,
|
|
1130
|
+
description: "hash(ref_id) mod partitionCount \u2014 precomputed for cheap WHERE"
|
|
1131
|
+
}),
|
|
1132
|
+
status: import_data.Field.text({
|
|
1133
|
+
label: "Status",
|
|
1134
|
+
required: true,
|
|
1135
|
+
defaultValue: "pending",
|
|
1136
|
+
maxLength: 16,
|
|
1137
|
+
description: "pending | in_flight | success | failed | dead"
|
|
1138
|
+
}),
|
|
1139
|
+
attempts: import_data.Field.number({
|
|
1140
|
+
label: "Attempts",
|
|
1141
|
+
required: true,
|
|
1142
|
+
defaultValue: 0,
|
|
1143
|
+
description: "Number of attempts made so far"
|
|
1144
|
+
}),
|
|
1145
|
+
claimed_by: import_data.Field.text({ label: "Claimed By", required: false, maxLength: 128 }),
|
|
1146
|
+
claimed_at: import_data.Field.number({ label: "Claimed At (ms)", required: false }),
|
|
1147
|
+
next_retry_at: import_data.Field.number({ label: "Next Retry At (ms)", required: false }),
|
|
1148
|
+
last_attempted_at: import_data.Field.number({ label: "Last Attempted At (ms)", required: false }),
|
|
1149
|
+
response_code: import_data.Field.number({ label: "HTTP Status", required: false }),
|
|
1150
|
+
response_body: import_data.Field.textarea({ label: "Response Body (capped)", required: false }),
|
|
1151
|
+
error: import_data.Field.textarea({ label: "Error", required: false }),
|
|
1152
|
+
created_at: import_data.Field.number({ label: "Created At (ms)", required: true }),
|
|
1153
|
+
updated_at: import_data.Field.number({ label: "Updated At (ms)", required: true })
|
|
1154
|
+
},
|
|
1155
|
+
indexes: [
|
|
1156
|
+
{ fields: ["source", "dedup_key"], unique: true },
|
|
1157
|
+
// Hot path: claim query
|
|
1158
|
+
{ fields: ["status", "partition_key", "next_retry_at"] },
|
|
1159
|
+
// Reaper: scan stale in_flight rows by claimed_at
|
|
1160
|
+
{ fields: ["status", "claimed_at"] },
|
|
1161
|
+
{ fields: ["source", "ref_id"] }
|
|
1162
|
+
]
|
|
1163
|
+
});
|
|
1164
|
+
var SYS_HTTP_DELIVERY = "sys_http_delivery";
|
|
1165
|
+
|
|
1166
|
+
// src/sql-http-outbox.ts
|
|
1167
|
+
var SqlHttpOutbox = class {
|
|
1168
|
+
constructor(engine, opts) {
|
|
1169
|
+
this.engine = engine;
|
|
1170
|
+
if (opts.partitionCount <= 0) {
|
|
1171
|
+
throw new Error("SqlHttpOutbox: partitionCount must be > 0");
|
|
1172
|
+
}
|
|
1173
|
+
this.objectName = opts.objectName ?? SYS_HTTP_DELIVERY;
|
|
1174
|
+
this.partitionCount = opts.partitionCount;
|
|
1175
|
+
}
|
|
1176
|
+
async enqueue(input) {
|
|
1177
|
+
const existing = await this.engine.findOne(this.objectName, {
|
|
1178
|
+
where: { source: input.source, dedup_key: input.dedupKey },
|
|
1179
|
+
fields: ["id"]
|
|
1180
|
+
});
|
|
1181
|
+
if (existing?.id) return existing.id;
|
|
1182
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
1183
|
+
const now = Date.now();
|
|
1184
|
+
const row = {
|
|
1185
|
+
id,
|
|
1186
|
+
source: input.source,
|
|
1187
|
+
ref_id: input.refId,
|
|
1188
|
+
dedup_key: input.dedupKey,
|
|
1189
|
+
label: input.label,
|
|
1190
|
+
url: input.url,
|
|
1191
|
+
method: input.method ?? "POST",
|
|
1192
|
+
headers_json: input.headers ? JSON.stringify(input.headers) : void 0,
|
|
1193
|
+
signing_secret: input.signingSecret,
|
|
1194
|
+
timeout_ms: input.timeoutMs,
|
|
1195
|
+
payload_json: JSON.stringify(input.payload ?? null),
|
|
1196
|
+
partition_key: hashPartition(input.refId, this.partitionCount),
|
|
1197
|
+
status: "pending",
|
|
1198
|
+
attempts: 0,
|
|
743
1199
|
created_at: now,
|
|
744
1200
|
updated_at: now
|
|
745
1201
|
};
|
|
@@ -747,8 +1203,11 @@ var SqlNotificationOutbox = class {
|
|
|
747
1203
|
await this.engine.insert(this.objectName, row);
|
|
748
1204
|
return id;
|
|
749
1205
|
} catch (err) {
|
|
750
|
-
const winner = await this.engine.findOne(this.objectName, {
|
|
751
|
-
|
|
1206
|
+
const winner = await this.engine.findOne(this.objectName, {
|
|
1207
|
+
where: { source: input.source, dedup_key: input.dedupKey },
|
|
1208
|
+
fields: ["id"]
|
|
1209
|
+
});
|
|
1210
|
+
if (winner?.id) return winner.id;
|
|
752
1211
|
throw err;
|
|
753
1212
|
}
|
|
754
1213
|
}
|
|
@@ -757,19 +1216,25 @@ var SqlNotificationOutbox = class {
|
|
|
757
1216
|
await this.engine.update(
|
|
758
1217
|
this.objectName,
|
|
759
1218
|
{ status: "pending", claimed_by: null, claimed_at: null, updated_at: now },
|
|
760
|
-
{
|
|
1219
|
+
{
|
|
1220
|
+
where: {
|
|
1221
|
+
status: "in_flight",
|
|
1222
|
+
claimed_at: { $lt: now - opts.claimTtlMs }
|
|
1223
|
+
},
|
|
1224
|
+
multi: true
|
|
1225
|
+
}
|
|
761
1226
|
);
|
|
762
1227
|
const partitionFilter = opts.partition ? { partition_key: opts.partition.index } : {};
|
|
763
1228
|
const candidates = await this.engine.find(this.objectName, {
|
|
764
1229
|
where: {
|
|
765
1230
|
status: "pending",
|
|
766
1231
|
...partitionFilter,
|
|
767
|
-
$or: [{
|
|
1232
|
+
$or: [{ next_retry_at: null }, { next_retry_at: { $lte: now } }]
|
|
768
1233
|
},
|
|
769
1234
|
fields: ["id"],
|
|
770
1235
|
limit: opts.limit
|
|
771
1236
|
});
|
|
772
|
-
if (
|
|
1237
|
+
if (candidates.length === 0) return [];
|
|
773
1238
|
const ids = candidates.map((c) => c.id);
|
|
774
1239
|
await this.engine.update(
|
|
775
1240
|
this.objectName,
|
|
@@ -779,7 +1244,7 @@ var SqlNotificationOutbox = class {
|
|
|
779
1244
|
const claimed = await this.engine.find(this.objectName, {
|
|
780
1245
|
where: { id: { $in: ids }, claimed_by: opts.nodeId, claimed_at: now, status: "in_flight" }
|
|
781
1246
|
});
|
|
782
|
-
return claimed.map((r) => this.
|
|
1247
|
+
return claimed.map((r) => this.toDelivery(r));
|
|
783
1248
|
}
|
|
784
1249
|
async ack(id, result) {
|
|
785
1250
|
const current = await this.engine.findOne(this.objectName, {
|
|
@@ -789,19 +1254,19 @@ var SqlNotificationOutbox = class {
|
|
|
789
1254
|
if (!current) return;
|
|
790
1255
|
const now = Date.now();
|
|
791
1256
|
let status;
|
|
792
|
-
let
|
|
793
|
-
let error
|
|
1257
|
+
let nextRetryAt;
|
|
1258
|
+
let error;
|
|
794
1259
|
if (result.success) {
|
|
795
1260
|
status = "success";
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
error = result.error ?? null;
|
|
1261
|
+
nextRetryAt = null;
|
|
1262
|
+
error = null;
|
|
799
1263
|
} else if (result.dead) {
|
|
800
1264
|
status = "dead";
|
|
1265
|
+
nextRetryAt = null;
|
|
801
1266
|
error = result.error ?? null;
|
|
802
1267
|
} else {
|
|
803
1268
|
status = "pending";
|
|
804
|
-
|
|
1269
|
+
nextRetryAt = result.nextRetryAt ?? null;
|
|
805
1270
|
error = result.error ?? null;
|
|
806
1271
|
}
|
|
807
1272
|
await this.engine.update(
|
|
@@ -812,7 +1277,9 @@ var SqlNotificationOutbox = class {
|
|
|
812
1277
|
last_attempted_at: now,
|
|
813
1278
|
claimed_by: null,
|
|
814
1279
|
claimed_at: null,
|
|
815
|
-
|
|
1280
|
+
response_code: result.httpStatus ?? null,
|
|
1281
|
+
response_body: result.responseBody ?? null,
|
|
1282
|
+
next_retry_at: nextRetryAt,
|
|
816
1283
|
error,
|
|
817
1284
|
updated_at: now
|
|
818
1285
|
},
|
|
@@ -822,34 +1289,65 @@ var SqlNotificationOutbox = class {
|
|
|
822
1289
|
async list(filter) {
|
|
823
1290
|
const where = {};
|
|
824
1291
|
if (filter?.status) where.status = filter.status;
|
|
825
|
-
if (filter?.
|
|
1292
|
+
if (filter?.source) where.source = filter.source;
|
|
826
1293
|
const rows = await this.engine.find(this.objectName, { where });
|
|
827
|
-
return rows.map((r) => this.
|
|
1294
|
+
return rows.map((r) => this.toDelivery(r));
|
|
828
1295
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1296
|
+
async redeliver(id) {
|
|
1297
|
+
const current = await this.engine.findOne(this.objectName, { where: { id } });
|
|
1298
|
+
if (!current) {
|
|
1299
|
+
throw new HttpRedeliverError(`Delivery row '${id}' not found`, "not_found");
|
|
1300
|
+
}
|
|
1301
|
+
if (current.status !== "success" && current.status !== "failed" && current.status !== "dead") {
|
|
1302
|
+
throw new HttpRedeliverError(
|
|
1303
|
+
`Delivery row '${id}' is '${current.status}', expected one of: success, failed, dead`,
|
|
1304
|
+
"not_eligible"
|
|
1305
|
+
);
|
|
837
1306
|
}
|
|
1307
|
+
const now = Date.now();
|
|
1308
|
+
await this.engine.update(
|
|
1309
|
+
this.objectName,
|
|
1310
|
+
{
|
|
1311
|
+
status: "pending",
|
|
1312
|
+
attempts: 0,
|
|
1313
|
+
claimed_by: null,
|
|
1314
|
+
claimed_at: null,
|
|
1315
|
+
next_retry_at: null,
|
|
1316
|
+
last_attempted_at: null,
|
|
1317
|
+
response_code: null,
|
|
1318
|
+
response_body: null,
|
|
1319
|
+
error: null,
|
|
1320
|
+
updated_at: now
|
|
1321
|
+
},
|
|
1322
|
+
{ where: { id, status: { $in: ["success", "failed", "dead"] } }, multi: false }
|
|
1323
|
+
);
|
|
1324
|
+
const after = await this.engine.findOne(this.objectName, { where: { id } });
|
|
1325
|
+
if (!after || after.status !== "pending") {
|
|
1326
|
+
throw new HttpRedeliverError(`Delivery row '${id}' state changed during redeliver`, "not_eligible");
|
|
1327
|
+
}
|
|
1328
|
+
return this.toDelivery(after);
|
|
1329
|
+
}
|
|
1330
|
+
toDelivery(r) {
|
|
838
1331
|
return {
|
|
839
1332
|
id: r.id,
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1333
|
+
source: r.source,
|
|
1334
|
+
refId: r.ref_id,
|
|
1335
|
+
dedupKey: r.dedup_key,
|
|
1336
|
+
label: r.label ?? void 0,
|
|
1337
|
+
url: r.url,
|
|
1338
|
+
method: r.method ?? void 0,
|
|
1339
|
+
headers: r.headers_json ? JSON.parse(r.headers_json) : void 0,
|
|
1340
|
+
signingSecret: r.signing_secret ?? void 0,
|
|
1341
|
+
timeoutMs: r.timeout_ms ?? void 0,
|
|
1342
|
+
payload: JSON.parse(r.payload_json),
|
|
847
1343
|
status: r.status,
|
|
848
1344
|
attempts: r.attempts,
|
|
849
1345
|
claimedBy: r.claimed_by ?? void 0,
|
|
850
1346
|
claimedAt: r.claimed_at ?? void 0,
|
|
851
|
-
|
|
1347
|
+
nextRetryAt: r.next_retry_at ?? void 0,
|
|
852
1348
|
lastAttemptedAt: r.last_attempted_at ?? void 0,
|
|
1349
|
+
responseCode: r.response_code ?? void 0,
|
|
1350
|
+
responseBody: r.response_body ?? void 0,
|
|
853
1351
|
error: r.error ?? void 0,
|
|
854
1352
|
createdAt: r.created_at,
|
|
855
1353
|
updatedAt: r.updated_at
|
|
@@ -1004,6 +1502,237 @@ function stableNodeOffset(nodeId, partitionCount) {
|
|
|
1004
1502
|
return Math.abs(h) % partitionCount;
|
|
1005
1503
|
}
|
|
1006
1504
|
|
|
1505
|
+
// src/http-sender.ts
|
|
1506
|
+
var import_node_crypto3 = require("crypto");
|
|
1507
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 15e3;
|
|
1508
|
+
var RESPONSE_BODY_CAP = 16 * 1024;
|
|
1509
|
+
async function sendOnce(delivery, fetchImpl) {
|
|
1510
|
+
const body = typeof delivery.payload === "string" ? delivery.payload : JSON.stringify(delivery.payload ?? null);
|
|
1511
|
+
const headers = {
|
|
1512
|
+
"Content-Type": "application/json",
|
|
1513
|
+
"User-Agent": "ObjectStack-Http/1.0",
|
|
1514
|
+
"X-Objectstack-Delivery": delivery.id,
|
|
1515
|
+
"X-Objectstack-Attempt": String(delivery.attempts + 1),
|
|
1516
|
+
...delivery.label ? { "X-Objectstack-Event": delivery.label } : {},
|
|
1517
|
+
...delivery.headers ?? {}
|
|
1518
|
+
};
|
|
1519
|
+
if (delivery.signingSecret) {
|
|
1520
|
+
const sig = (0, import_node_crypto3.createHmac)("sha256", delivery.signingSecret).update(body).digest("hex");
|
|
1521
|
+
headers["X-Objectstack-Signature"] = `sha256=${sig}`;
|
|
1522
|
+
}
|
|
1523
|
+
const timeoutMs = delivery.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
1524
|
+
const controller = new AbortController();
|
|
1525
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1526
|
+
const start = Date.now();
|
|
1527
|
+
try {
|
|
1528
|
+
const res = await fetchImpl(delivery.url, {
|
|
1529
|
+
method: delivery.method ?? "POST",
|
|
1530
|
+
headers,
|
|
1531
|
+
body,
|
|
1532
|
+
signal: controller.signal
|
|
1533
|
+
});
|
|
1534
|
+
clearTimeout(timer);
|
|
1535
|
+
const responseText = await safeReadBody(res);
|
|
1536
|
+
const durationMs = Date.now() - start;
|
|
1537
|
+
if (res.ok) {
|
|
1538
|
+
return { success: true, httpStatus: res.status, responseBody: responseText, durationMs };
|
|
1539
|
+
}
|
|
1540
|
+
const retriable = res.status === 408 || res.status === 429 || res.status >= 500;
|
|
1541
|
+
return {
|
|
1542
|
+
success: false,
|
|
1543
|
+
retriable,
|
|
1544
|
+
httpStatus: res.status,
|
|
1545
|
+
responseBody: responseText,
|
|
1546
|
+
error: `HTTP ${res.status}`,
|
|
1547
|
+
durationMs
|
|
1548
|
+
};
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
clearTimeout(timer);
|
|
1551
|
+
const durationMs = Date.now() - start;
|
|
1552
|
+
const e = err;
|
|
1553
|
+
const error = e?.name === "AbortError" ? `timeout after ${timeoutMs}ms` : e?.message ?? String(err);
|
|
1554
|
+
return { success: false, retriable: true, error, durationMs };
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async function safeReadBody(res) {
|
|
1558
|
+
try {
|
|
1559
|
+
const text = await res.text();
|
|
1560
|
+
return text.length > RESPONSE_BODY_CAP ? text.slice(0, RESPONSE_BODY_CAP) : text;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return void 0;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function nextHttpRetryDelayMs(attemptsSoFar, rng = Math.random) {
|
|
1566
|
+
const SCHEDULE = [1e3, 1e4, 6e4, 6e5, 36e5, 216e5, 864e5];
|
|
1567
|
+
if (attemptsSoFar < 1 || attemptsSoFar > SCHEDULE.length) return null;
|
|
1568
|
+
const base = SCHEDULE[attemptsSoFar - 1];
|
|
1569
|
+
const jitter = 0.8 + rng() * 0.4;
|
|
1570
|
+
return Math.floor(base * jitter);
|
|
1571
|
+
}
|
|
1572
|
+
function classifyAttempt(outcome, attemptsSoFar, now = Date.now(), rng) {
|
|
1573
|
+
if (outcome.success) return outcome;
|
|
1574
|
+
if (!outcome.retriable) {
|
|
1575
|
+
return {
|
|
1576
|
+
success: false,
|
|
1577
|
+
httpStatus: outcome.httpStatus,
|
|
1578
|
+
responseBody: outcome.responseBody,
|
|
1579
|
+
error: outcome.error,
|
|
1580
|
+
durationMs: outcome.durationMs,
|
|
1581
|
+
dead: true
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
const delay = nextHttpRetryDelayMs(attemptsSoFar + 1, rng);
|
|
1585
|
+
if (delay === null) {
|
|
1586
|
+
return {
|
|
1587
|
+
success: false,
|
|
1588
|
+
httpStatus: outcome.httpStatus,
|
|
1589
|
+
responseBody: outcome.responseBody,
|
|
1590
|
+
error: outcome.error,
|
|
1591
|
+
durationMs: outcome.durationMs,
|
|
1592
|
+
dead: true
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
success: false,
|
|
1597
|
+
httpStatus: outcome.httpStatus,
|
|
1598
|
+
responseBody: outcome.responseBody,
|
|
1599
|
+
error: outcome.error,
|
|
1600
|
+
durationMs: outcome.durationMs,
|
|
1601
|
+
nextRetryAt: now + delay
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function newDeliveryId() {
|
|
1605
|
+
return (0, import_node_crypto3.randomUUID)();
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/http-dispatcher.ts
|
|
1609
|
+
var SINGLE_NODE_CLUSTER2 = {
|
|
1610
|
+
lock: {
|
|
1611
|
+
async acquire() {
|
|
1612
|
+
return { release() {
|
|
1613
|
+
}, isHeld: () => true, renew() {
|
|
1614
|
+
} };
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
var HttpDispatcher = class {
|
|
1619
|
+
constructor(options) {
|
|
1620
|
+
this.running = false;
|
|
1621
|
+
const intervalMs = options.intervalMs ?? 500;
|
|
1622
|
+
const lockTtlMs = options.lockTtlMs ?? intervalMs * 5;
|
|
1623
|
+
this.opts = {
|
|
1624
|
+
nodeId: options.nodeId,
|
|
1625
|
+
outbox: options.outbox,
|
|
1626
|
+
cluster: options.cluster ?? SINGLE_NODE_CLUSTER2,
|
|
1627
|
+
partitionCount: options.partitionCount ?? 8,
|
|
1628
|
+
batchSize: options.batchSize ?? 32,
|
|
1629
|
+
intervalMs,
|
|
1630
|
+
lockTtlMs,
|
|
1631
|
+
claimTtlMs: options.claimTtlMs ?? lockTtlMs * 2,
|
|
1632
|
+
fetchImpl: options.fetchImpl,
|
|
1633
|
+
rng: options.rng,
|
|
1634
|
+
now: options.now,
|
|
1635
|
+
logger: options.logger,
|
|
1636
|
+
onAttempt: options.onAttempt
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
/** Begin the periodic loop. Safe to call once; subsequent calls are no-ops. */
|
|
1640
|
+
start() {
|
|
1641
|
+
if (this.running) return;
|
|
1642
|
+
this.running = true;
|
|
1643
|
+
this.scheduleTick();
|
|
1644
|
+
this.timer = setInterval(() => this.scheduleTick(), this.opts.intervalMs);
|
|
1645
|
+
this.timer.unref?.();
|
|
1646
|
+
}
|
|
1647
|
+
/** Stop the loop and wait for the in-flight tick to drain. */
|
|
1648
|
+
async stop() {
|
|
1649
|
+
if (!this.running) return;
|
|
1650
|
+
this.running = false;
|
|
1651
|
+
if (this.timer) {
|
|
1652
|
+
clearInterval(this.timer);
|
|
1653
|
+
this.timer = void 0;
|
|
1654
|
+
}
|
|
1655
|
+
if (this.inflightTick) {
|
|
1656
|
+
try {
|
|
1657
|
+
await this.inflightTick;
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
/** Run one full tick (all partitions). Exposed for deterministic tests. */
|
|
1663
|
+
async tick() {
|
|
1664
|
+
await this.runTick();
|
|
1665
|
+
}
|
|
1666
|
+
scheduleTick() {
|
|
1667
|
+
if (this.inflightTick) return;
|
|
1668
|
+
this.inflightTick = this.runTick().catch((err) => {
|
|
1669
|
+
this.opts.logger?.warn?.("http-dispatcher: tick failed", {
|
|
1670
|
+
nodeId: this.opts.nodeId,
|
|
1671
|
+
error: err?.message ?? String(err)
|
|
1672
|
+
});
|
|
1673
|
+
}).finally(() => {
|
|
1674
|
+
this.inflightTick = void 0;
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
async runTick() {
|
|
1678
|
+
const partitionCount = this.opts.partitionCount;
|
|
1679
|
+
const offset = stableNodeOffset2(this.opts.nodeId, partitionCount);
|
|
1680
|
+
for (let step = 0; step < partitionCount; step++) {
|
|
1681
|
+
const i = (offset + step) % partitionCount;
|
|
1682
|
+
await this.runPartition(i);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async runPartition(index) {
|
|
1686
|
+
const key = `http.dispatcher.partition.${index}`;
|
|
1687
|
+
const handle = await this.opts.cluster.lock.acquire(key, {
|
|
1688
|
+
ttlMs: this.opts.lockTtlMs,
|
|
1689
|
+
waitMs: 0
|
|
1690
|
+
});
|
|
1691
|
+
if (!handle) return;
|
|
1692
|
+
try {
|
|
1693
|
+
const claimed = await this.opts.outbox.claim({
|
|
1694
|
+
nodeId: this.opts.nodeId,
|
|
1695
|
+
limit: this.opts.batchSize,
|
|
1696
|
+
partition: { index, count: this.opts.partitionCount },
|
|
1697
|
+
claimTtlMs: this.opts.claimTtlMs,
|
|
1698
|
+
now: this.opts.now?.()
|
|
1699
|
+
});
|
|
1700
|
+
if (claimed.length === 0) return;
|
|
1701
|
+
await handle.renew?.(this.opts.lockTtlMs);
|
|
1702
|
+
for (const row of claimed) {
|
|
1703
|
+
if (handle.isHeld && !handle.isHeld()) break;
|
|
1704
|
+
await this.processRow(row);
|
|
1705
|
+
}
|
|
1706
|
+
} finally {
|
|
1707
|
+
await handle.release();
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async processRow(row) {
|
|
1711
|
+
const fetchImpl = this.opts.fetchImpl ?? globalThis.fetch;
|
|
1712
|
+
if (!fetchImpl) {
|
|
1713
|
+
this.opts.logger?.warn?.("http-dispatcher: no fetch impl available", { rowId: row.id });
|
|
1714
|
+
await this.opts.outbox.ack(row.id, {
|
|
1715
|
+
success: false,
|
|
1716
|
+
error: "no fetch implementation",
|
|
1717
|
+
durationMs: 0,
|
|
1718
|
+
dead: true
|
|
1719
|
+
});
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
const outcome = await sendOnce(row, fetchImpl);
|
|
1723
|
+
const result = classifyAttempt(outcome, row.attempts, this.opts.now?.() ?? Date.now(), this.opts.rng);
|
|
1724
|
+
await this.opts.outbox.ack(row.id, result);
|
|
1725
|
+
this.opts.onAttempt?.(row, result.success);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
function stableNodeOffset2(nodeId, partitionCount) {
|
|
1729
|
+
let h = 0;
|
|
1730
|
+
for (let i = 0; i < nodeId.length; i++) {
|
|
1731
|
+
h = h * 31 + nodeId.charCodeAt(i) | 0;
|
|
1732
|
+
}
|
|
1733
|
+
return Math.abs(h) % partitionCount;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1007
1736
|
// src/retention.ts
|
|
1008
1737
|
var DEFAULT_RETENTION_TARGETS = [
|
|
1009
1738
|
{ object: RECEIPT_OBJECT, tsField: "created_at", format: "iso" },
|
|
@@ -1211,8 +1940,8 @@ function createEmailChannel(opts) {
|
|
|
1211
1940
|
}
|
|
1212
1941
|
|
|
1213
1942
|
// src/objects/inbox-message.object.ts
|
|
1214
|
-
var
|
|
1215
|
-
var InboxMessage =
|
|
1943
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
1944
|
+
var InboxMessage = import_data2.ObjectSchema.create({
|
|
1216
1945
|
name: "sys_inbox_message",
|
|
1217
1946
|
label: "Inbox Message",
|
|
1218
1947
|
pluralLabel: "Inbox Messages",
|
|
@@ -1234,37 +1963,37 @@ var InboxMessage = import_data.ObjectSchema.create({
|
|
|
1234
1963
|
}
|
|
1235
1964
|
},
|
|
1236
1965
|
fields: {
|
|
1237
|
-
id:
|
|
1966
|
+
id: import_data2.Field.text({
|
|
1238
1967
|
label: "Inbox Message ID",
|
|
1239
1968
|
required: true,
|
|
1240
1969
|
readonly: true
|
|
1241
1970
|
}),
|
|
1242
|
-
user_id:
|
|
1971
|
+
user_id: import_data2.Field.text({
|
|
1243
1972
|
label: "Recipient User",
|
|
1244
1973
|
required: true,
|
|
1245
1974
|
searchable: true
|
|
1246
1975
|
}),
|
|
1247
|
-
notification_id:
|
|
1976
|
+
notification_id: import_data2.Field.text({
|
|
1248
1977
|
label: "Notification Event",
|
|
1249
1978
|
searchable: true,
|
|
1250
1979
|
description: "FK \u2192 sys_notification (the L2 event this row materializes)"
|
|
1251
1980
|
}),
|
|
1252
|
-
delivery_id:
|
|
1981
|
+
delivery_id: import_data2.Field.text({
|
|
1253
1982
|
label: "Delivery",
|
|
1254
1983
|
description: "FK \u2192 sys_notification_delivery (outbox row); null until P1"
|
|
1255
1984
|
}),
|
|
1256
|
-
topic:
|
|
1985
|
+
topic: import_data2.Field.text({
|
|
1257
1986
|
label: "Topic",
|
|
1258
1987
|
searchable: true
|
|
1259
1988
|
}),
|
|
1260
|
-
title:
|
|
1989
|
+
title: import_data2.Field.text({
|
|
1261
1990
|
label: "Title",
|
|
1262
1991
|
required: true
|
|
1263
1992
|
}),
|
|
1264
|
-
body_md:
|
|
1993
|
+
body_md: import_data2.Field.markdown({
|
|
1265
1994
|
label: "Body"
|
|
1266
1995
|
}),
|
|
1267
|
-
severity:
|
|
1996
|
+
severity: import_data2.Field.select({
|
|
1268
1997
|
label: "Severity",
|
|
1269
1998
|
options: [
|
|
1270
1999
|
{ label: "Info", value: "info" },
|
|
@@ -1272,10 +2001,10 @@ var InboxMessage = import_data.ObjectSchema.create({
|
|
|
1272
2001
|
{ label: "Critical", value: "critical" }
|
|
1273
2002
|
]
|
|
1274
2003
|
}),
|
|
1275
|
-
action_url:
|
|
2004
|
+
action_url: import_data2.Field.text({
|
|
1276
2005
|
label: "Action URL"
|
|
1277
2006
|
}),
|
|
1278
|
-
created_at:
|
|
2007
|
+
created_at: import_data2.Field.datetime({
|
|
1279
2008
|
label: "Created At",
|
|
1280
2009
|
readonly: true
|
|
1281
2010
|
})
|
|
@@ -1283,8 +2012,8 @@ var InboxMessage = import_data.ObjectSchema.create({
|
|
|
1283
2012
|
});
|
|
1284
2013
|
|
|
1285
2014
|
// src/objects/notification-receipt.object.ts
|
|
1286
|
-
var
|
|
1287
|
-
var NotificationReceipt =
|
|
2015
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
2016
|
+
var NotificationReceipt = import_data3.ObjectSchema.create({
|
|
1288
2017
|
name: "sys_notification_receipt",
|
|
1289
2018
|
label: "Notification Receipt",
|
|
1290
2019
|
pluralLabel: "Notification Receipts",
|
|
@@ -1295,43 +2024,43 @@ var NotificationReceipt = import_data2.ObjectSchema.create({
|
|
|
1295
2024
|
titleFormat: "{state}",
|
|
1296
2025
|
compactLayout: ["notification_id", "user_id", "channel", "state", "at"],
|
|
1297
2026
|
fields: {
|
|
1298
|
-
id:
|
|
2027
|
+
id: import_data3.Field.text({
|
|
1299
2028
|
label: "Receipt ID",
|
|
1300
2029
|
required: true,
|
|
1301
2030
|
readonly: true
|
|
1302
2031
|
}),
|
|
1303
|
-
notification_id:
|
|
2032
|
+
notification_id: import_data3.Field.text({
|
|
1304
2033
|
label: "Notification Event",
|
|
1305
2034
|
required: true,
|
|
1306
2035
|
searchable: true,
|
|
1307
2036
|
description: "FK \u2192 sys_notification (L2 event)"
|
|
1308
2037
|
}),
|
|
1309
|
-
delivery_id:
|
|
2038
|
+
delivery_id: import_data3.Field.text({
|
|
1310
2039
|
label: "Delivery",
|
|
1311
2040
|
required: false,
|
|
1312
2041
|
description: "FK \u2192 sys_notification_delivery (outbox row); null until P1"
|
|
1313
2042
|
}),
|
|
1314
|
-
user_id:
|
|
2043
|
+
user_id: import_data3.Field.text({
|
|
1315
2044
|
label: "Recipient User",
|
|
1316
2045
|
required: true,
|
|
1317
2046
|
searchable: true
|
|
1318
2047
|
}),
|
|
1319
|
-
channel:
|
|
2048
|
+
channel: import_data3.Field.text({
|
|
1320
2049
|
label: "Channel",
|
|
1321
2050
|
required: true,
|
|
1322
2051
|
description: "Channel id this receipt is for (inbox / email / push / \u2026)"
|
|
1323
2052
|
}),
|
|
1324
|
-
state:
|
|
2053
|
+
state: import_data3.Field.select(["delivered", "read", "clicked", "dismissed"], {
|
|
1325
2054
|
label: "State",
|
|
1326
2055
|
required: true,
|
|
1327
2056
|
defaultValue: "delivered"
|
|
1328
2057
|
}),
|
|
1329
|
-
at:
|
|
2058
|
+
at: import_data3.Field.datetime({
|
|
1330
2059
|
label: "At",
|
|
1331
2060
|
required: false,
|
|
1332
2061
|
description: "When the receipt reached its current state"
|
|
1333
2062
|
}),
|
|
1334
|
-
created_at:
|
|
2063
|
+
created_at: import_data3.Field.datetime({
|
|
1335
2064
|
label: "Created At",
|
|
1336
2065
|
readonly: true
|
|
1337
2066
|
})
|
|
@@ -1343,8 +2072,8 @@ var NotificationReceipt = import_data2.ObjectSchema.create({
|
|
|
1343
2072
|
});
|
|
1344
2073
|
|
|
1345
2074
|
// src/objects/notification-delivery.object.ts
|
|
1346
|
-
var
|
|
1347
|
-
var NotificationDelivery =
|
|
2075
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
2076
|
+
var NotificationDelivery = import_data4.ObjectSchema.create({
|
|
1348
2077
|
name: "sys_notification_delivery",
|
|
1349
2078
|
label: "Notification Delivery",
|
|
1350
2079
|
pluralLabel: "Notification Deliveries",
|
|
@@ -1355,34 +2084,34 @@ var NotificationDelivery = import_data3.ObjectSchema.create({
|
|
|
1355
2084
|
titleFormat: "{channel} \u2192 {recipient_id}",
|
|
1356
2085
|
compactLayout: ["notification_id", "recipient_id", "channel", "status", "attempts"],
|
|
1357
2086
|
fields: {
|
|
1358
|
-
id:
|
|
1359
|
-
notification_id:
|
|
2087
|
+
id: import_data4.Field.text({ label: "Delivery ID", required: true, readonly: true }),
|
|
2088
|
+
notification_id: import_data4.Field.text({
|
|
1360
2089
|
label: "Notification Event",
|
|
1361
2090
|
required: true,
|
|
1362
2091
|
searchable: true,
|
|
1363
2092
|
description: "FK \u2192 sys_notification (L2 event)"
|
|
1364
2093
|
}),
|
|
1365
|
-
recipient_id:
|
|
1366
|
-
channel:
|
|
1367
|
-
topic:
|
|
1368
|
-
payload:
|
|
2094
|
+
recipient_id: import_data4.Field.text({ label: "Recipient User", required: true, searchable: true }),
|
|
2095
|
+
channel: import_data4.Field.text({ label: "Channel", required: true }),
|
|
2096
|
+
topic: import_data4.Field.text({ label: "Topic", searchable: true }),
|
|
2097
|
+
payload: import_data4.Field.json({
|
|
1369
2098
|
label: "Payload",
|
|
1370
2099
|
description: "Snapshot of the rendered notification content for dispatch."
|
|
1371
2100
|
}),
|
|
1372
|
-
status:
|
|
2101
|
+
status: import_data4.Field.select(["pending", "in_flight", "success", "failed", "dead", "suppressed"], {
|
|
1373
2102
|
label: "Status",
|
|
1374
2103
|
required: true,
|
|
1375
2104
|
defaultValue: "pending"
|
|
1376
2105
|
}),
|
|
1377
|
-
attempts:
|
|
1378
|
-
partition_key:
|
|
1379
|
-
claimed_by:
|
|
1380
|
-
claimed_at:
|
|
1381
|
-
next_attempt_at:
|
|
1382
|
-
last_attempted_at:
|
|
1383
|
-
error:
|
|
1384
|
-
created_at:
|
|
1385
|
-
updated_at:
|
|
2106
|
+
attempts: import_data4.Field.number({ label: "Attempts", defaultValue: 0 }),
|
|
2107
|
+
partition_key: import_data4.Field.number({ label: "Partition Key", defaultValue: 0 }),
|
|
2108
|
+
claimed_by: import_data4.Field.text({ label: "Claimed By", description: "Node id while in_flight" }),
|
|
2109
|
+
claimed_at: import_data4.Field.number({ label: "Claimed At (ms)" }),
|
|
2110
|
+
next_attempt_at: import_data4.Field.number({ label: "Next Attempt At (ms)" }),
|
|
2111
|
+
last_attempted_at: import_data4.Field.number({ label: "Last Attempted At (ms)" }),
|
|
2112
|
+
error: import_data4.Field.textarea({ label: "Error" }),
|
|
2113
|
+
created_at: import_data4.Field.number({ label: "Created At (ms)", readonly: true }),
|
|
2114
|
+
updated_at: import_data4.Field.number({ label: "Updated At (ms)" })
|
|
1386
2115
|
},
|
|
1387
2116
|
indexes: [
|
|
1388
2117
|
// Dedup: one delivery per (event, recipient, channel).
|
|
@@ -1396,8 +2125,8 @@ var NotificationDelivery = import_data3.ObjectSchema.create({
|
|
|
1396
2125
|
});
|
|
1397
2126
|
|
|
1398
2127
|
// src/objects/notification-preference.object.ts
|
|
1399
|
-
var
|
|
1400
|
-
var NotificationPreference =
|
|
2128
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
2129
|
+
var NotificationPreference = import_data5.ObjectSchema.create({
|
|
1401
2130
|
name: "sys_notification_preference",
|
|
1402
2131
|
label: "Notification Preference",
|
|
1403
2132
|
pluralLabel: "Notification Preferences",
|
|
@@ -1408,44 +2137,44 @@ var NotificationPreference = import_data4.ObjectSchema.create({
|
|
|
1408
2137
|
titleFormat: "{user_id} \xB7 {topic} \xB7 {channel}",
|
|
1409
2138
|
compactLayout: ["user_id", "topic", "channel", "enabled", "digest"],
|
|
1410
2139
|
fields: {
|
|
1411
|
-
id:
|
|
1412
|
-
user_id:
|
|
2140
|
+
id: import_data5.Field.text({ label: "Preference ID", required: true, readonly: true }),
|
|
2141
|
+
user_id: import_data5.Field.text({
|
|
1413
2142
|
label: "User",
|
|
1414
2143
|
required: true,
|
|
1415
2144
|
searchable: true,
|
|
1416
2145
|
description: "Recipient user id, or '*' for the admin-global default."
|
|
1417
2146
|
}),
|
|
1418
|
-
topic:
|
|
2147
|
+
topic: import_data5.Field.text({
|
|
1419
2148
|
label: "Topic",
|
|
1420
2149
|
required: true,
|
|
1421
2150
|
searchable: true,
|
|
1422
2151
|
defaultValue: "*",
|
|
1423
2152
|
description: "Notification topic, or '*' for all topics."
|
|
1424
2153
|
}),
|
|
1425
|
-
channel:
|
|
2154
|
+
channel: import_data5.Field.text({
|
|
1426
2155
|
label: "Channel",
|
|
1427
2156
|
required: true,
|
|
1428
2157
|
defaultValue: "*",
|
|
1429
2158
|
description: "Channel id (inbox/email/push/\u2026), or '*' for all channels."
|
|
1430
2159
|
}),
|
|
1431
|
-
enabled:
|
|
2160
|
+
enabled: import_data5.Field.boolean({
|
|
1432
2161
|
label: "Enabled",
|
|
1433
2162
|
defaultValue: true,
|
|
1434
2163
|
description: "When false, this (user, topic, channel) is muted."
|
|
1435
2164
|
}),
|
|
1436
|
-
digest:
|
|
2165
|
+
digest: import_data5.Field.select(["none", "daily", "weekly"], {
|
|
1437
2166
|
label: "Digest",
|
|
1438
2167
|
required: false,
|
|
1439
2168
|
defaultValue: "none",
|
|
1440
2169
|
description: "Batch cadence (P3 digest middleware)."
|
|
1441
2170
|
}),
|
|
1442
|
-
quiet_hours:
|
|
2171
|
+
quiet_hours: import_data5.Field.json({
|
|
1443
2172
|
label: "Quiet Hours",
|
|
1444
2173
|
required: false,
|
|
1445
2174
|
description: "Optional { tz, start, end } window (P3 quiet-hours middleware)."
|
|
1446
2175
|
}),
|
|
1447
|
-
created_at:
|
|
1448
|
-
updated_at:
|
|
2176
|
+
created_at: import_data5.Field.datetime({ label: "Created At", readonly: true }),
|
|
2177
|
+
updated_at: import_data5.Field.datetime({ label: "Updated At", required: false })
|
|
1449
2178
|
},
|
|
1450
2179
|
indexes: [
|
|
1451
2180
|
{ fields: ["user_id", "topic", "channel"], unique: true },
|
|
@@ -1454,8 +2183,8 @@ var NotificationPreference = import_data4.ObjectSchema.create({
|
|
|
1454
2183
|
});
|
|
1455
2184
|
|
|
1456
2185
|
// src/objects/notification-subscription.object.ts
|
|
1457
|
-
var
|
|
1458
|
-
var NotificationSubscription =
|
|
2186
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
2187
|
+
var NotificationSubscription = import_data6.ObjectSchema.create({
|
|
1459
2188
|
name: "sys_notification_subscription",
|
|
1460
2189
|
label: "Notification Subscription",
|
|
1461
2190
|
pluralLabel: "Notification Subscriptions",
|
|
@@ -1466,25 +2195,25 @@ var NotificationSubscription = import_data5.ObjectSchema.create({
|
|
|
1466
2195
|
titleFormat: "{principal} \xB7 {topic}",
|
|
1467
2196
|
compactLayout: ["topic", "principal", "enabled", "created_at"],
|
|
1468
2197
|
fields: {
|
|
1469
|
-
id:
|
|
1470
|
-
topic:
|
|
2198
|
+
id: import_data6.Field.text({ label: "Subscription ID", required: true, readonly: true }),
|
|
2199
|
+
topic: import_data6.Field.text({
|
|
1471
2200
|
label: "Topic",
|
|
1472
2201
|
required: true,
|
|
1473
2202
|
searchable: true,
|
|
1474
2203
|
description: "Notification topic this principal subscribes to."
|
|
1475
2204
|
}),
|
|
1476
|
-
principal:
|
|
2205
|
+
principal: import_data6.Field.text({
|
|
1477
2206
|
label: "Principal",
|
|
1478
2207
|
required: true,
|
|
1479
2208
|
searchable: true,
|
|
1480
2209
|
description: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id."
|
|
1481
2210
|
}),
|
|
1482
|
-
enabled:
|
|
2211
|
+
enabled: import_data6.Field.boolean({
|
|
1483
2212
|
label: "Enabled",
|
|
1484
2213
|
defaultValue: true,
|
|
1485
2214
|
description: "When false, the subscription is inactive."
|
|
1486
2215
|
}),
|
|
1487
|
-
created_at:
|
|
2216
|
+
created_at: import_data6.Field.datetime({ label: "Created At", readonly: true })
|
|
1488
2217
|
},
|
|
1489
2218
|
indexes: [
|
|
1490
2219
|
{ fields: ["topic", "principal"], unique: true },
|
|
@@ -1493,8 +2222,8 @@ var NotificationSubscription = import_data5.ObjectSchema.create({
|
|
|
1493
2222
|
});
|
|
1494
2223
|
|
|
1495
2224
|
// src/objects/notification-template.object.ts
|
|
1496
|
-
var
|
|
1497
|
-
var NotificationTemplate =
|
|
2225
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
2226
|
+
var NotificationTemplate = import_data7.ObjectSchema.create({
|
|
1498
2227
|
name: "sys_notification_template",
|
|
1499
2228
|
label: "Notification Template",
|
|
1500
2229
|
pluralLabel: "Notification Templates",
|
|
@@ -1505,47 +2234,47 @@ var NotificationTemplate = import_data6.ObjectSchema.create({
|
|
|
1505
2234
|
titleFormat: "{topic} \xB7 {channel} \xB7 {locale}",
|
|
1506
2235
|
compactLayout: ["topic", "channel", "locale", "is_active"],
|
|
1507
2236
|
fields: {
|
|
1508
|
-
id:
|
|
1509
|
-
topic:
|
|
1510
|
-
channel:
|
|
2237
|
+
id: import_data7.Field.text({ label: "Template ID", required: true, readonly: true }),
|
|
2238
|
+
topic: import_data7.Field.text({ label: "Topic", required: true, searchable: true }),
|
|
2239
|
+
channel: import_data7.Field.text({
|
|
1511
2240
|
label: "Channel",
|
|
1512
2241
|
required: true,
|
|
1513
2242
|
defaultValue: "email",
|
|
1514
2243
|
description: "Channel id this template renders for (email/inbox/push/\u2026)."
|
|
1515
2244
|
}),
|
|
1516
|
-
locale:
|
|
2245
|
+
locale: import_data7.Field.text({
|
|
1517
2246
|
label: "Locale",
|
|
1518
2247
|
required: true,
|
|
1519
2248
|
defaultValue: "en",
|
|
1520
2249
|
description: "BCP-47 locale, e.g. 'en' / 'en-US' / 'zh-CN'."
|
|
1521
2250
|
}),
|
|
1522
|
-
version:
|
|
2251
|
+
version: import_data7.Field.number({
|
|
1523
2252
|
label: "Version",
|
|
1524
2253
|
required: false,
|
|
1525
2254
|
defaultValue: 1
|
|
1526
2255
|
}),
|
|
1527
|
-
subject:
|
|
2256
|
+
subject: import_data7.Field.text({
|
|
1528
2257
|
label: "Subject / Title",
|
|
1529
2258
|
required: false,
|
|
1530
2259
|
description: "Rendered into the email subject / inbox title. Supports {{ payload.x }}."
|
|
1531
2260
|
}),
|
|
1532
|
-
body:
|
|
2261
|
+
body: import_data7.Field.markdown({
|
|
1533
2262
|
label: "Body",
|
|
1534
2263
|
required: false,
|
|
1535
2264
|
description: "Template body. Supports {{ payload.x }}. Interpreted per `format`."
|
|
1536
2265
|
}),
|
|
1537
|
-
format:
|
|
2266
|
+
format: import_data7.Field.select(["markdown", "html", "text", "mjml"], {
|
|
1538
2267
|
label: "Body Format",
|
|
1539
2268
|
required: false,
|
|
1540
2269
|
defaultValue: "markdown"
|
|
1541
2270
|
}),
|
|
1542
|
-
is_active:
|
|
2271
|
+
is_active: import_data7.Field.boolean({
|
|
1543
2272
|
label: "Active",
|
|
1544
2273
|
defaultValue: true,
|
|
1545
2274
|
description: "Only active templates are selected at render time."
|
|
1546
2275
|
}),
|
|
1547
|
-
created_at:
|
|
1548
|
-
updated_at:
|
|
2276
|
+
created_at: import_data7.Field.datetime({ label: "Created At", readonly: true }),
|
|
2277
|
+
updated_at: import_data7.Field.datetime({ label: "Updated At", required: false })
|
|
1549
2278
|
},
|
|
1550
2279
|
indexes: [
|
|
1551
2280
|
{ fields: ["topic", "channel", "locale"] },
|
|
@@ -1588,6 +2317,7 @@ var MessagingServicePlugin = class {
|
|
|
1588
2317
|
service.registerChannel(createInboxChannel({ getData }));
|
|
1589
2318
|
}
|
|
1590
2319
|
ctx.registerService("messaging", service);
|
|
2320
|
+
ctx.registerService("notification", service);
|
|
1591
2321
|
ctx.getService("manifest").register({
|
|
1592
2322
|
id: "com.objectstack.service.messaging",
|
|
1593
2323
|
name: "Messaging Service",
|
|
@@ -1600,7 +2330,8 @@ var MessagingServicePlugin = class {
|
|
|
1600
2330
|
NotificationDelivery,
|
|
1601
2331
|
NotificationPreference,
|
|
1602
2332
|
NotificationSubscription,
|
|
1603
|
-
NotificationTemplate
|
|
2333
|
+
NotificationTemplate,
|
|
2334
|
+
HttpDelivery
|
|
1604
2335
|
],
|
|
1605
2336
|
navigationContributions: [
|
|
1606
2337
|
{
|
|
@@ -1647,7 +2378,7 @@ var MessagingServicePlugin = class {
|
|
|
1647
2378
|
cluster = void 0;
|
|
1648
2379
|
}
|
|
1649
2380
|
this.dispatcher = new NotificationDispatcher({
|
|
1650
|
-
nodeId: `notify-${process.pid}-${(0,
|
|
2381
|
+
nodeId: `notify-${process.pid}-${(0, import_node_crypto4.randomUUID)().slice(0, 8)}`,
|
|
1651
2382
|
outbox,
|
|
1652
2383
|
channels: service,
|
|
1653
2384
|
channelContext: { logger: ctx.logger },
|
|
@@ -1660,6 +2391,20 @@ var MessagingServicePlugin = class {
|
|
|
1660
2391
|
ctx.logger.info(
|
|
1661
2392
|
`[messaging] reliable delivery on (outbox + dispatcher, ${this.options.partitionCount} partitions${cluster ? ", clustered" : ", single-node"})`
|
|
1662
2393
|
);
|
|
2394
|
+
const httpOutbox = new SqlHttpOutbox(engine, { partitionCount: this.options.partitionCount });
|
|
2395
|
+
service.setHttpOutbox(httpOutbox);
|
|
2396
|
+
this.httpDispatcher = new HttpDispatcher({
|
|
2397
|
+
nodeId: `http-${process.pid}-${(0, import_node_crypto4.randomUUID)().slice(0, 8)}`,
|
|
2398
|
+
outbox: httpOutbox,
|
|
2399
|
+
cluster,
|
|
2400
|
+
partitionCount: this.options.partitionCount,
|
|
2401
|
+
intervalMs: this.options.dispatchIntervalMs,
|
|
2402
|
+
logger: ctx.logger
|
|
2403
|
+
});
|
|
2404
|
+
this.httpDispatcher.start();
|
|
2405
|
+
ctx.logger.info(
|
|
2406
|
+
`[messaging] HTTP delivery on (sys_http_delivery outbox + dispatcher, ${this.options.partitionCount} partitions)`
|
|
2407
|
+
);
|
|
1663
2408
|
});
|
|
1664
2409
|
}
|
|
1665
2410
|
if (this.options.retentionDays > 0 && typeof ctx.hook === "function") {
|
|
@@ -1687,6 +2432,8 @@ var MessagingServicePlugin = class {
|
|
|
1687
2432
|
async stop() {
|
|
1688
2433
|
await this.dispatcher?.stop();
|
|
1689
2434
|
this.dispatcher = void 0;
|
|
2435
|
+
await this.httpDispatcher?.stop();
|
|
2436
|
+
this.httpDispatcher = void 0;
|
|
1690
2437
|
if (this.retentionTimer) {
|
|
1691
2438
|
clearInterval(this.retentionTimer);
|
|
1692
2439
|
this.retentionTimer = void 0;
|
|
@@ -1695,7 +2442,7 @@ var MessagingServicePlugin = class {
|
|
|
1695
2442
|
};
|
|
1696
2443
|
|
|
1697
2444
|
// src/memory-outbox.ts
|
|
1698
|
-
var
|
|
2445
|
+
var import_node_crypto5 = require("crypto");
|
|
1699
2446
|
var MemoryNotificationOutbox = class {
|
|
1700
2447
|
constructor(partitionCount = 8, clock = () => Date.now()) {
|
|
1701
2448
|
this.partitionCount = partitionCount;
|
|
@@ -1708,7 +2455,7 @@ var MemoryNotificationOutbox = class {
|
|
|
1708
2455
|
return r.id;
|
|
1709
2456
|
}
|
|
1710
2457
|
}
|
|
1711
|
-
const id = (0,
|
|
2458
|
+
const id = (0, import_node_crypto5.randomUUID)();
|
|
1712
2459
|
const now = this.clock();
|
|
1713
2460
|
this.rows.set(id, {
|
|
1714
2461
|
id,
|
|
@@ -1785,15 +2532,140 @@ var MemoryNotificationOutbox = class {
|
|
|
1785
2532
|
return rows.map((r) => ({ ...r }));
|
|
1786
2533
|
}
|
|
1787
2534
|
};
|
|
2535
|
+
|
|
2536
|
+
// src/memory-http-outbox.ts
|
|
2537
|
+
var import_node_crypto6 = require("crypto");
|
|
2538
|
+
var MemoryHttpOutbox = class {
|
|
2539
|
+
constructor() {
|
|
2540
|
+
this.rows = /* @__PURE__ */ new Map();
|
|
2541
|
+
/** Dedup index keyed by `${source}::${dedupKey}` -> row id. */
|
|
2542
|
+
this.dedup = /* @__PURE__ */ new Map();
|
|
2543
|
+
}
|
|
2544
|
+
async enqueue(input) {
|
|
2545
|
+
const dedupKey = `${input.source}::${input.dedupKey}`;
|
|
2546
|
+
const existing = this.dedup.get(dedupKey);
|
|
2547
|
+
if (existing) return existing;
|
|
2548
|
+
const id = (0, import_node_crypto6.randomUUID)();
|
|
2549
|
+
const now = Date.now();
|
|
2550
|
+
const row = {
|
|
2551
|
+
id,
|
|
2552
|
+
source: input.source,
|
|
2553
|
+
refId: input.refId,
|
|
2554
|
+
dedupKey: input.dedupKey,
|
|
2555
|
+
label: input.label,
|
|
2556
|
+
url: input.url,
|
|
2557
|
+
method: input.method ?? "POST",
|
|
2558
|
+
headers: input.headers,
|
|
2559
|
+
signingSecret: input.signingSecret,
|
|
2560
|
+
timeoutMs: input.timeoutMs,
|
|
2561
|
+
payload: input.payload,
|
|
2562
|
+
status: "pending",
|
|
2563
|
+
attempts: 0,
|
|
2564
|
+
createdAt: now,
|
|
2565
|
+
updatedAt: now
|
|
2566
|
+
};
|
|
2567
|
+
this.rows.set(id, row);
|
|
2568
|
+
this.dedup.set(dedupKey, id);
|
|
2569
|
+
return id;
|
|
2570
|
+
}
|
|
2571
|
+
async claim(opts) {
|
|
2572
|
+
const now = opts.now ?? Date.now();
|
|
2573
|
+
const claimed = [];
|
|
2574
|
+
for (const row of this.rows.values()) {
|
|
2575
|
+
if (row.status === "in_flight" && row.claimedAt !== void 0 && now - row.claimedAt > opts.claimTtlMs) {
|
|
2576
|
+
row.status = "pending";
|
|
2577
|
+
row.claimedBy = void 0;
|
|
2578
|
+
row.claimedAt = void 0;
|
|
2579
|
+
row.updatedAt = now;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
for (const row of this.rows.values()) {
|
|
2583
|
+
if (claimed.length >= opts.limit) break;
|
|
2584
|
+
if (row.status !== "pending") continue;
|
|
2585
|
+
if (row.nextRetryAt !== void 0 && row.nextRetryAt > now) continue;
|
|
2586
|
+
if (opts.partition) {
|
|
2587
|
+
const p = hashPartition(row.refId, opts.partition.count);
|
|
2588
|
+
if (p !== opts.partition.index) continue;
|
|
2589
|
+
}
|
|
2590
|
+
row.status = "in_flight";
|
|
2591
|
+
row.claimedBy = opts.nodeId;
|
|
2592
|
+
row.claimedAt = now;
|
|
2593
|
+
row.updatedAt = now;
|
|
2594
|
+
claimed.push({ ...row });
|
|
2595
|
+
}
|
|
2596
|
+
return claimed;
|
|
2597
|
+
}
|
|
2598
|
+
async ack(id, result) {
|
|
2599
|
+
const row = this.rows.get(id);
|
|
2600
|
+
if (!row) return;
|
|
2601
|
+
const now = Date.now();
|
|
2602
|
+
row.attempts += 1;
|
|
2603
|
+
row.lastAttemptedAt = now;
|
|
2604
|
+
row.updatedAt = now;
|
|
2605
|
+
row.claimedBy = void 0;
|
|
2606
|
+
row.claimedAt = void 0;
|
|
2607
|
+
row.responseCode = result.httpStatus;
|
|
2608
|
+
row.responseBody = result.responseBody;
|
|
2609
|
+
let status;
|
|
2610
|
+
if (result.success) {
|
|
2611
|
+
status = "success";
|
|
2612
|
+
row.nextRetryAt = void 0;
|
|
2613
|
+
row.error = void 0;
|
|
2614
|
+
} else if (result.dead) {
|
|
2615
|
+
status = "dead";
|
|
2616
|
+
row.error = result.error;
|
|
2617
|
+
row.nextRetryAt = void 0;
|
|
2618
|
+
} else {
|
|
2619
|
+
status = "pending";
|
|
2620
|
+
row.error = result.error;
|
|
2621
|
+
row.nextRetryAt = result.nextRetryAt;
|
|
2622
|
+
}
|
|
2623
|
+
row.status = status;
|
|
2624
|
+
}
|
|
2625
|
+
async list(filter) {
|
|
2626
|
+
let all = Array.from(this.rows.values()).map((r) => ({ ...r }));
|
|
2627
|
+
if (filter?.status) all = all.filter((r) => r.status === filter.status);
|
|
2628
|
+
if (filter?.source) all = all.filter((r) => r.source === filter.source);
|
|
2629
|
+
return all;
|
|
2630
|
+
}
|
|
2631
|
+
async redeliver(id) {
|
|
2632
|
+
const row = this.rows.get(id);
|
|
2633
|
+
if (!row) {
|
|
2634
|
+
throw new HttpRedeliverError(`Delivery row '${id}' not found`, "not_found");
|
|
2635
|
+
}
|
|
2636
|
+
if (row.status !== "success" && row.status !== "failed" && row.status !== "dead") {
|
|
2637
|
+
throw new HttpRedeliverError(
|
|
2638
|
+
`Delivery row '${id}' is '${row.status}', expected one of: success, failed, dead`,
|
|
2639
|
+
"not_eligible"
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
const now = Date.now();
|
|
2643
|
+
row.status = "pending";
|
|
2644
|
+
row.attempts = 0;
|
|
2645
|
+
row.claimedBy = void 0;
|
|
2646
|
+
row.claimedAt = void 0;
|
|
2647
|
+
row.nextRetryAt = void 0;
|
|
2648
|
+
row.error = void 0;
|
|
2649
|
+
row.responseCode = void 0;
|
|
2650
|
+
row.responseBody = void 0;
|
|
2651
|
+
row.updatedAt = now;
|
|
2652
|
+
return { ...row };
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
1788
2655
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1789
2656
|
0 && (module.exports = {
|
|
2657
|
+
DEFAULT_HTTP_TIMEOUT_MS,
|
|
1790
2658
|
DEFAULT_LOCALE,
|
|
1791
2659
|
DEFAULT_RETENTION_TARGETS,
|
|
1792
2660
|
DELIVERY_OBJECT,
|
|
1793
2661
|
EMAIL_USER_OBJECT,
|
|
2662
|
+
HttpDelivery,
|
|
2663
|
+
HttpDispatcher,
|
|
2664
|
+
HttpRedeliverError,
|
|
1794
2665
|
INBOX_OBJECT,
|
|
1795
2666
|
InboxMessage,
|
|
1796
2667
|
MEMBER_OBJECT,
|
|
2668
|
+
MemoryHttpOutbox,
|
|
1797
2669
|
MemoryNotificationOutbox,
|
|
1798
2670
|
MessagingService,
|
|
1799
2671
|
MessagingServicePlugin,
|
|
@@ -1810,17 +2682,23 @@ var MemoryNotificationOutbox = class {
|
|
|
1810
2682
|
PreferenceResolver,
|
|
1811
2683
|
RECEIPT_OBJECT,
|
|
1812
2684
|
RecipientResolver,
|
|
2685
|
+
SYS_HTTP_DELIVERY,
|
|
2686
|
+
SqlHttpOutbox,
|
|
1813
2687
|
SqlNotificationOutbox,
|
|
1814
2688
|
TEAM_MEMBER_OBJECT,
|
|
1815
2689
|
TEMPLATE_OBJECT,
|
|
1816
2690
|
USER_OBJECT,
|
|
1817
2691
|
classifyDeliveryAttempt,
|
|
2692
|
+
classifyHttpAttempt,
|
|
1818
2693
|
createEmailChannel,
|
|
1819
2694
|
createInboxChannel,
|
|
1820
2695
|
hashPartition,
|
|
1821
2696
|
interpolate,
|
|
2697
|
+
newHttpDeliveryId,
|
|
2698
|
+
nextHttpRetryDelayMs,
|
|
1822
2699
|
nextRetryDelayMs,
|
|
1823
2700
|
quietHoursDeferral,
|
|
1824
|
-
renderNotification
|
|
2701
|
+
renderNotification,
|
|
2702
|
+
sendHttpOnce
|
|
1825
2703
|
});
|
|
1826
2704
|
//# sourceMappingURL=index.cjs.map
|