@ultimat3/notify 25.0.0 → 25.2.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/README.md +6 -4
- package/package.json +5 -5
- package/src/ledger.ts +7 -3
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ import {
|
|
|
104
104
|
setNotifyStores,
|
|
105
105
|
} from '@ultimat3/notify';
|
|
106
106
|
|
|
107
|
-
declare const executor: PgExecutor; // `@ultimat3/
|
|
107
|
+
declare const executor: PgExecutor; // `@ultimat3/db`'s dbExecutor()
|
|
108
108
|
declare const idempotency: { readonly windowMs: number }; // the boot's own store
|
|
109
109
|
// The app's preference table, behind whatever taxonomy it named.
|
|
110
110
|
declare const prefs: {
|
|
@@ -133,8 +133,8 @@ setNotifyStores({
|
|
|
133
133
|
| `inbox` | **none** — `X_NOTIFY_STORE_MISSING` | a message written to nowhere is worse than a refusal |
|
|
134
134
|
| `digest` | **none** — `X_NOTIFY_STORE_MISSING` | same. `memoryDigestStore()` for one process; `postgresDigestStore({ executor })` keeps a window across a restart and gives two replicas ONE window (a partial unique index on the open one), so exactly one run owns its flush |
|
|
135
135
|
|
|
136
|
-
`executor` is a structural `{ query(sql, params) }` — `@ultimat3/
|
|
137
|
-
|
|
136
|
+
`executor` is a structural `{ query(sql, params) }` — `@ultimat3/db`'s `dbExecutor()` is the
|
|
137
|
+
framework's own. `Bun.sql` does **not** satisfy it.
|
|
138
138
|
|
|
139
139
|
## Retention
|
|
140
140
|
|
|
@@ -225,7 +225,9 @@ delay sends nothing.
|
|
|
225
225
|
A job body runs before its checkpoint lands. Two layers stop a double send: the step checkpoint
|
|
226
226
|
(`deliver:<channel>:<recipient>`), and the delivery ledger's atomic claim on
|
|
227
227
|
`(notifier, key, channel, coalesce(recipient, ''))` — taken before the send, settled after. A claim
|
|
228
|
-
that already reads `sent` answers `false`.
|
|
228
|
+
that already reads `sent` answers `false`. `memoryDeliveryLedger` keys and settles exactly as the
|
|
229
|
+
Postgres ledger does — a null and an empty recipient are one row, and a `settle` with no claim
|
|
230
|
+
records nothing (`ledger-parity.live.test.ts` runs one script against both).
|
|
229
231
|
|
|
230
232
|
A bulk channel claims **one** row for the whole audience, with a null recipient: half a bulk POST is
|
|
231
233
|
not a state this package can represent. That null is why the key coalesces rather than naming the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/notify",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.2.0",
|
|
4
4
|
"description": "Notifications: one declaration, many channels — fan-out, preference gate, digest window, delivery ledger, in-app inbox",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ultimat3/core": "25.
|
|
39
|
-
"@ultimat3/jobs": "25.
|
|
40
|
-
"@ultimat3/schema": "25.
|
|
41
|
-
"@ultimat3/time": "25.
|
|
38
|
+
"@ultimat3/core": "25.2.0",
|
|
39
|
+
"@ultimat3/jobs": "25.2.0",
|
|
40
|
+
"@ultimat3/schema": "25.2.0",
|
|
41
|
+
"@ultimat3/time": "25.2.0"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/ledger.ts
CHANGED
|
@@ -58,9 +58,10 @@ export interface DeliveryLedger {
|
|
|
58
58
|
* The tuple as one string. `JSON.stringify` of an ARRAY rather than a joined separator: a
|
|
59
59
|
* recipient id or a channel name containing the separator would otherwise collide two distinct
|
|
60
60
|
* deliveries into one ledger row, and the failure mode is a notification nobody ever receives.
|
|
61
|
+
* `recipient ?? ''` is the Postgres ledger's `coalesce(recipient, '')`: one identity, both stores.
|
|
61
62
|
*/
|
|
62
63
|
const keyOf = (claim: DeliveryClaim): string =>
|
|
63
|
-
JSON.stringify([claim.notifier, claim.key, claim.recipient, claim.channel]);
|
|
64
|
+
JSON.stringify([claim.notifier, claim.key, claim.recipient ?? '', claim.channel]);
|
|
64
65
|
|
|
65
66
|
export interface MemoryDeliveryLedgerOptions {
|
|
66
67
|
/**
|
|
@@ -122,8 +123,9 @@ export function memoryDeliveryLedger(
|
|
|
122
123
|
const id = keyOf(claim);
|
|
123
124
|
const existing = rows.get(id);
|
|
124
125
|
if (existing?.status === 'sent') return Promise.resolve(false);
|
|
126
|
+
// The row it found keeps its own identity, as the pg `on conflict do update` does.
|
|
125
127
|
rows.set(id, {
|
|
126
|
-
...claim,
|
|
128
|
+
...(existing ?? claim),
|
|
127
129
|
status: 'sending',
|
|
128
130
|
attempts: (existing?.attempts ?? 0) + 1,
|
|
129
131
|
at,
|
|
@@ -134,7 +136,9 @@ export function memoryDeliveryLedger(
|
|
|
134
136
|
settle(claim, status, at) {
|
|
135
137
|
const id = keyOf(claim);
|
|
136
138
|
const existing = rows.get(id);
|
|
137
|
-
|
|
139
|
+
// No claim, no row: the pg `update` matches nothing, and a row written here would refuse
|
|
140
|
+
// the claim after it — a notification dev never sends and production does.
|
|
141
|
+
if (existing !== undefined) rows.set(id, { ...existing, status, at });
|
|
138
142
|
return Promise.resolve();
|
|
139
143
|
},
|
|
140
144
|
find(claim) {
|