@objectstack/service-messaging 10.2.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +77 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +256 -25
- package/dist/index.d.ts +256 -25
- package/dist/index.js +77 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/LICENSE.apache +0 -202
package/dist/index.js
CHANGED
|
@@ -892,6 +892,19 @@ function classifyDeliveryAttempt(result, errorClass, attemptsSoFar, now = Date.n
|
|
|
892
892
|
return { success: false, error: result.error, nextAttemptAt: now + delay };
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
// src/audit-timestamp.ts
|
|
896
|
+
function toEpochMs(value) {
|
|
897
|
+
if (typeof value === "number") return value;
|
|
898
|
+
if (value instanceof Date) return value.getTime();
|
|
899
|
+
if (typeof value === "string") {
|
|
900
|
+
const parsed = Date.parse(value);
|
|
901
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
902
|
+
const numeric = Number(value);
|
|
903
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
904
|
+
}
|
|
905
|
+
return 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
895
908
|
// src/sql-outbox.ts
|
|
896
909
|
var DELIVERY_OBJECT = "sys_notification_delivery";
|
|
897
910
|
var SqlNotificationOutbox = class {
|
|
@@ -910,7 +923,7 @@ var SqlNotificationOutbox = class {
|
|
|
910
923
|
const existing = await this.engine.findOne(this.objectName, { where: dedup, fields: ["id"] });
|
|
911
924
|
if (existing?.id) return String(existing.id);
|
|
912
925
|
const id = randomUUID();
|
|
913
|
-
const now = Date
|
|
926
|
+
const now = /* @__PURE__ */ new Date();
|
|
914
927
|
const row = {
|
|
915
928
|
id,
|
|
916
929
|
notification_id: input.notificationId,
|
|
@@ -1071,8 +1084,8 @@ var SqlNotificationOutbox = class {
|
|
|
1071
1084
|
lastAttemptedAt: r.last_attempted_at ?? void 0,
|
|
1072
1085
|
error: r.error ?? void 0,
|
|
1073
1086
|
digestKey: r.digest_key ?? void 0,
|
|
1074
|
-
createdAt: r.created_at,
|
|
1075
|
-
updatedAt: r.updated_at
|
|
1087
|
+
createdAt: toEpochMs(r.created_at),
|
|
1088
|
+
updatedAt: toEpochMs(r.updated_at)
|
|
1076
1089
|
};
|
|
1077
1090
|
}
|
|
1078
1091
|
};
|
|
@@ -1201,8 +1214,11 @@ var HttpDelivery = ObjectSchema.create({
|
|
|
1201
1214
|
response_code: Field.number({ label: "HTTP Status", required: false }),
|
|
1202
1215
|
response_body: Field.textarea({ label: "Response Body (capped)", required: false }),
|
|
1203
1216
|
error: Field.textarea({ label: "Error", required: false }),
|
|
1204
|
-
|
|
1205
|
-
|
|
1217
|
+
// Builtin audit columns are native TIMESTAMP columns (Postgres/MySQL),
|
|
1218
|
+
// so declare them `datetime` and write `Date`s (not epoch-ms numbers,
|
|
1219
|
+
// which a real timestamp column rejects). See SqlHttpOutbox.
|
|
1220
|
+
created_at: Field.datetime({ label: "Created At", required: true }),
|
|
1221
|
+
updated_at: Field.datetime({ label: "Updated At", required: true })
|
|
1206
1222
|
},
|
|
1207
1223
|
indexes: [
|
|
1208
1224
|
{ fields: ["source", "dedup_key"], unique: true },
|
|
@@ -1232,7 +1248,7 @@ var SqlHttpOutbox = class {
|
|
|
1232
1248
|
});
|
|
1233
1249
|
if (existing?.id) return existing.id;
|
|
1234
1250
|
const id = randomUUID2();
|
|
1235
|
-
const now = Date
|
|
1251
|
+
const now = /* @__PURE__ */ new Date();
|
|
1236
1252
|
const row = {
|
|
1237
1253
|
id,
|
|
1238
1254
|
source: input.source,
|
|
@@ -1401,8 +1417,8 @@ var SqlHttpOutbox = class {
|
|
|
1401
1417
|
responseCode: r.response_code ?? void 0,
|
|
1402
1418
|
responseBody: r.response_body ?? void 0,
|
|
1403
1419
|
error: r.error ?? void 0,
|
|
1404
|
-
createdAt: r.created_at,
|
|
1405
|
-
updatedAt: r.updated_at
|
|
1420
|
+
createdAt: toEpochMs(r.created_at),
|
|
1421
|
+
updatedAt: toEpochMs(r.updated_at)
|
|
1406
1422
|
};
|
|
1407
1423
|
}
|
|
1408
1424
|
};
|
|
@@ -1876,10 +1892,10 @@ function stableNodeOffset2(nodeId, partitionCount) {
|
|
|
1876
1892
|
// src/retention.ts
|
|
1877
1893
|
var DEFAULT_NOTIFICATION_RETENTION_DAYS = 90;
|
|
1878
1894
|
var DEFAULT_RETENTION_TARGETS = [
|
|
1879
|
-
{ object: RECEIPT_OBJECT, tsField: "created_at"
|
|
1880
|
-
{ object: INBOX_OBJECT, tsField: "created_at"
|
|
1881
|
-
{ object: DELIVERY_OBJECT, tsField: "created_at"
|
|
1882
|
-
{ object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at"
|
|
1895
|
+
{ object: RECEIPT_OBJECT, tsField: "created_at" },
|
|
1896
|
+
{ object: INBOX_OBJECT, tsField: "created_at" },
|
|
1897
|
+
{ object: DELIVERY_OBJECT, tsField: "created_at" },
|
|
1898
|
+
{ object: NOTIFICATION_EVENT_OBJECT, tsField: "created_at" }
|
|
1883
1899
|
];
|
|
1884
1900
|
var NotificationRetention = class {
|
|
1885
1901
|
constructor(opts) {
|
|
@@ -1902,14 +1918,15 @@ var NotificationRetention = class {
|
|
|
1902
1918
|
this.opts.logger.warn(`[messaging] retention: invalid retentionDays=${retentionDays}; prune skipped`);
|
|
1903
1919
|
return [];
|
|
1904
1920
|
}
|
|
1905
|
-
const
|
|
1906
|
-
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
1921
|
+
const cutoffIso = new Date(this.now() - retentionDays * 864e5).toISOString();
|
|
1907
1922
|
const outcomes = [];
|
|
1908
1923
|
for (const t of this.targets) {
|
|
1909
|
-
const cutoff = t.format === "epoch" ? cutoffMs : cutoffIso;
|
|
1910
1924
|
try {
|
|
1911
1925
|
const res = await data.delete(t.object, {
|
|
1912
|
-
|
|
1926
|
+
// ISO-8601 cutoff for every target: `created_at` is a native
|
|
1927
|
+
// timestamp column, which rejects a bare epoch-ms number on
|
|
1928
|
+
// Postgres. The driver coerces this per dialect on the way down.
|
|
1929
|
+
where: { [t.tsField]: { $lt: cutoffIso } },
|
|
1913
1930
|
multi: true,
|
|
1914
1931
|
// System context: retention is an operator policy that spans
|
|
1915
1932
|
// tenants, so it must not be scoped by the caller's RLS.
|
|
@@ -2261,8 +2278,12 @@ var NotificationDelivery = ObjectSchema4.create({
|
|
|
2261
2278
|
next_attempt_at: Field4.number({ label: "Next Attempt At (ms)" }),
|
|
2262
2279
|
last_attempted_at: Field4.number({ label: "Last Attempted At (ms)" }),
|
|
2263
2280
|
error: Field4.textarea({ label: "Error" }),
|
|
2264
|
-
|
|
2265
|
-
updated_at
|
|
2281
|
+
// Builtin audit columns: the SQL driver provisions `created_at` /
|
|
2282
|
+
// `updated_at` as native TIMESTAMP columns (Postgres/MySQL), so they are
|
|
2283
|
+
// declared `datetime` and written as `Date`s — a bare epoch-ms number is
|
|
2284
|
+
// rejected by a real timestamp column. See SqlNotificationOutbox.
|
|
2285
|
+
created_at: Field4.datetime({ label: "Created At", readonly: true }),
|
|
2286
|
+
updated_at: Field4.datetime({ label: "Updated At" })
|
|
2266
2287
|
},
|
|
2267
2288
|
indexes: [
|
|
2268
2289
|
// Dedup: one delivery per (event, recipient, channel).
|
|
@@ -2499,6 +2520,12 @@ var MessagingServicePlugin = class {
|
|
|
2499
2520
|
}
|
|
2500
2521
|
]
|
|
2501
2522
|
});
|
|
2523
|
+
if (typeof ctx.hook === "function") {
|
|
2524
|
+
ctx.hook("kernel:ready", async () => {
|
|
2525
|
+
const engine = getData();
|
|
2526
|
+
if (engine) await this.provisionSystemTables(engine, ctx);
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2502
2529
|
if (typeof ctx.hook === "function") {
|
|
2503
2530
|
const templateStore = new NotificationTemplateStore({ getData });
|
|
2504
2531
|
const getEmail = () => {
|
|
@@ -2581,6 +2608,38 @@ var MessagingServicePlugin = class {
|
|
|
2581
2608
|
`[messaging] service registered with channels: ${service.getRegisteredChannels().join(", ") || "(none)"}`
|
|
2582
2609
|
);
|
|
2583
2610
|
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Provision the physical tables for this service's system objects up-front.
|
|
2613
|
+
*
|
|
2614
|
+
* These objects are lazy-created on first WRITE (the SQL driver issues DDL
|
|
2615
|
+
* when the first row is inserted), so an env that READS them first — the
|
|
2616
|
+
* Console bell / inbox queries sys_inbox_message + sys_notification_receipt
|
|
2617
|
+
* before any notification has been delivered — hits "no such table", which
|
|
2618
|
+
* the engine logs as a `Find operation failed` ERROR on every page load.
|
|
2619
|
+
* Creating the tables at kernel:ready makes a new env consistent from the
|
|
2620
|
+
* start. Idempotent (the driver only creates a table when absent), so it is
|
|
2621
|
+
* safe on every boot; per-object failures are isolated.
|
|
2622
|
+
*/
|
|
2623
|
+
async provisionSystemTables(engine, ctx) {
|
|
2624
|
+
const sync = engine.syncObjectSchema;
|
|
2625
|
+
if (typeof sync !== "function") return;
|
|
2626
|
+
const objects = [
|
|
2627
|
+
InboxMessage,
|
|
2628
|
+
NotificationReceipt,
|
|
2629
|
+
NotificationDelivery,
|
|
2630
|
+
NotificationPreference,
|
|
2631
|
+
NotificationSubscription,
|
|
2632
|
+
NotificationTemplate,
|
|
2633
|
+
HttpDelivery
|
|
2634
|
+
];
|
|
2635
|
+
for (const obj of objects) {
|
|
2636
|
+
try {
|
|
2637
|
+
await sync.call(engine, obj.name);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
ctx.logger.warn(`[messaging] could not provision ${obj.name} storage \u2014 ${err?.message ?? err}`);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2584
2643
|
/** Stop the dispatcher loop + retention sweep on shutdown. */
|
|
2585
2644
|
async stop() {
|
|
2586
2645
|
await this.dispatcher?.stop();
|