unified-notification-core 0.1.1 → 0.1.5
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/CHANGELOG.md +100 -0
- package/README.md +197 -7
- package/dist/core.d.ts +32 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1254 -301
- package/dist/core.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +92 -4
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +8 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +207 -0
- package/dist/json.js.map +1 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +15 -18
- package/dist/keys.js.map +1 -1
- package/dist/preferences.d.ts +9 -0
- package/dist/preferences.d.ts.map +1 -1
- package/dist/preferences.js +52 -16
- package/dist/preferences.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +92 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +389 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +41 -4
- package/dist/schema.js.map +1 -1
- package/dist/strings.d.ts +5 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +63 -0
- package/dist/strings.js.map +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/adoption-notes.md +72 -3
- package/docs/architecture.md +117 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
- Add provider-agnostic `channelPolicies` with durable per-channel rate windows
|
|
6
|
+
and circuit-breaker cooldowns backed by the new `unc_dispatch_state` schema
|
|
7
|
+
table.
|
|
8
|
+
- Add `NotificationDeliveryError({ retryAfterMs })` so adapters can pass
|
|
9
|
+
provider throttle hints without coupling UNC to provider SDKs; retry-after
|
|
10
|
+
schedules the delivery retry and opens a shared channel cooldown.
|
|
11
|
+
- Report `dispatchUntilIdle()` stop reason `rate-limited` with `deferredUntil`
|
|
12
|
+
when due rows exist but channel policy defers claiming.
|
|
13
|
+
- Keep channel-policy deferrals attempt-free: deferred rows remain pending or
|
|
14
|
+
retrying and other channels can still dispatch in the same worker call.
|
|
15
|
+
|
|
16
|
+
## 0.1.4
|
|
17
|
+
|
|
18
|
+
- Add `dispatchUntilIdle()` as an explicit bounded drain helper with stable
|
|
19
|
+
worker identity, batch/deadline limits, and high-failure stop defaults so
|
|
20
|
+
application workers do not need to hand-roll dispatch loops.
|
|
21
|
+
- Batch-finalize built-in `in_app` deliveries after claim instead of invoking
|
|
22
|
+
per-row provider-style finalization.
|
|
23
|
+
- Run test files serially to avoid PGlite setup contention in the default
|
|
24
|
+
package check.
|
|
25
|
+
|
|
26
|
+
## 0.1.3
|
|
27
|
+
|
|
28
|
+
- Dispatch every due delivery created or reused by `publishAndDispatch()` by
|
|
29
|
+
draining its own publication in bounded delivery-id claim pages. The public
|
|
30
|
+
`dispatchDue()` worker limit remains capped per call.
|
|
31
|
+
|
|
32
|
+
## 0.1.2
|
|
33
|
+
|
|
34
|
+
- Validate runtime `preferencePolicy` values and dispatch limits before any
|
|
35
|
+
publication, claim, or provider invocation.
|
|
36
|
+
- Validate every caller-provided JSON payload recursively before persistence,
|
|
37
|
+
with an iterative 256-depth/10,000-node budget that rejects cycles,
|
|
38
|
+
unsupported values, accessors, array extras, non-finite numbers, and
|
|
39
|
+
non-plain objects without invoking getters.
|
|
40
|
+
- Keep resolved provider calls truthfully `sent` when optional adapter output is
|
|
41
|
+
invalid: discard the optional result, persist diagnostic error details, and
|
|
42
|
+
never retry the provider for result-shape failures.
|
|
43
|
+
- Isolate retry-delay and logger callbacks from delivery state. Invalid or
|
|
44
|
+
synchronously/asynchronously throwing retry policies and native Promises
|
|
45
|
+
now use the deterministic built-in backoff without unhandled rejections.
|
|
46
|
+
- Cap persisted, logged, and returned delivery diagnostic messages at 10,000
|
|
47
|
+
characters through one non-throwing normalization boundary.
|
|
48
|
+
- Reject every present runtime `channelData` value unless it is a plain own-key
|
|
49
|
+
JSON object; falsy non-objects can no longer bypass validation.
|
|
50
|
+
- Replace per-recipient publication queries with bulk inserts/selects while
|
|
51
|
+
preserving recipient order, channel uniqueness, preferences, custom text
|
|
52
|
+
identifiers, and per-recipient idempotency under concurrent publication.
|
|
53
|
+
- Avoid repeatedly normalizing immutable preference defaults during a batch.
|
|
54
|
+
- Reject PostgreSQL-unsafe NUL and unpaired-surrogate strings in public input
|
|
55
|
+
and JSON keys/values before mutation. Invalid optional adapter output follows
|
|
56
|
+
the existing sent-safe diagnostic path, while valid Unicode pairs are kept.
|
|
57
|
+
- Capture the durable finalization timestamp before provider invocation so an
|
|
58
|
+
application clock callback cannot fail after provider acceptance.
|
|
59
|
+
- Consume exact native logger Promise rejections and time out exact native retry
|
|
60
|
+
Promises after 100 ms with deterministic fallback and late-rejection handling.
|
|
61
|
+
- Validate exact runtime booleans, configured adapter functions, finite integer
|
|
62
|
+
limits, identifiers, dates, and complete preference batches before writes.
|
|
63
|
+
- Reserve JSON child-node capacity immediately after `ownKeys`, before child
|
|
64
|
+
descriptor inspection, so oversized Proxy objects remain bounded.
|
|
65
|
+
- Restrict async logger/retry results to exact same-realm native Node Promises,
|
|
66
|
+
detected with a hook-free brand check and captured intrinsic verification.
|
|
67
|
+
Arbitrary thenables are rejected without reading or invoking `then`.
|
|
68
|
+
- Harden `NotificationDeliveryError` options: only omitted or
|
|
69
|
+
plain/null-prototype records are accepted, a present `retryable` is an exact
|
|
70
|
+
boolean, and code/message strings are bounded and PostgreSQL-safe.
|
|
71
|
+
- Descriptor-capture constructor configuration, public method wrappers, public
|
|
72
|
+
arrays, schema-factory options, and preference-resolver input once before any
|
|
73
|
+
transaction, claim, or provider call, preventing mutable Proxy values from
|
|
74
|
+
changing after validation.
|
|
75
|
+
- Require configuration/API wrappers and channel-keyed maps to be plain or
|
|
76
|
+
null-prototype records, rejecting maps, sets, dates, arrays, functions, class
|
|
77
|
+
instances, accessors, and symbol keys while keeping Drizzle database/schema
|
|
78
|
+
dependencies opaque.
|
|
79
|
+
- Start the monotonic 100 ms retry-policy deadline before invoking application
|
|
80
|
+
code and reject values produced after synchronous policy stalls.
|
|
81
|
+
- Observe genuine logger Promises immediately without awaiting them. Ignore
|
|
82
|
+
invalid logger return values without traversal; synchronous logger callback
|
|
83
|
+
work remains application-owned.
|
|
84
|
+
- Reject Promise subclasses, cross-realm Promises, custom `PromiseLike` values,
|
|
85
|
+
and native Promises with own `then`/`constructor` hooks at runtime. Invalid
|
|
86
|
+
retry results use deterministic fallback.
|
|
87
|
+
- Require exact `Array.prototype` public/JSON arrays with canonical in-range
|
|
88
|
+
indices only; reject subclasses, exotic prototypes, accessors, index
|
|
89
|
+
`4294967295`, and every other extra property before mutation.
|
|
90
|
+
- Convert revoked-Proxy inspection failures, including `Array.isArray`, into
|
|
91
|
+
public `NotificationConfigurationError` values.
|
|
92
|
+
- Capture and bind every retry-isolation intrinsic at module initialization,
|
|
93
|
+
install native deadline settlement before policy invocation, attach accepted
|
|
94
|
+
Promise rejection handling before checking post-callback lateness, and remove
|
|
95
|
+
reliance on mutable public `Promise.race`.
|
|
96
|
+
- Enforce one PostgreSQL-safe `maxAttempts` range of 1 through 2,147,483,647 for
|
|
97
|
+
core defaults, scalar/per-channel publication, requeue, and claim overflow
|
|
98
|
+
guards.
|
|
99
|
+
- Classify native retry Promise lateness inside its actual fulfillment or
|
|
100
|
+
rejection callback using the captured monotonic clock, so event-loop ordering
|
|
101
|
+
cannot let a settlement after 100 ms beat a starved deadline timer.
|
|
102
|
+
|
|
3
103
|
## 0.1.1
|
|
4
104
|
|
|
5
105
|
- Made delivery claims unique per row and attempt, with durable attempt counts
|
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ export const {
|
|
|
74
74
|
notifications,
|
|
75
75
|
notificationDeliveries,
|
|
76
76
|
notificationPreferences,
|
|
77
|
+
notificationDispatchState,
|
|
77
78
|
} = notificationSchema;
|
|
78
79
|
```
|
|
79
80
|
|
|
@@ -106,6 +107,13 @@ schema diff through the application's normal reviewed migration workflow. UNC
|
|
|
106
107
|
does not create the index at runtime; correctness is preserved before the index
|
|
107
108
|
is applied, but legacy fallback reads can be slower.
|
|
108
109
|
|
|
110
|
+
Upgrading to `0.1.5` adds the `unc_dispatch_state` table, or the same suffix
|
|
111
|
+
under a custom prefix. That table stores provider-agnostic channel rate-limit
|
|
112
|
+
windows and cooldowns. Generate and apply the schema diff through the
|
|
113
|
+
application's normal reviewed migration workflow before enabling
|
|
114
|
+
`channelPolicies` or adapter `retryAfterMs` hints. UNC never creates the table
|
|
115
|
+
at runtime.
|
|
116
|
+
|
|
109
117
|
## Define channels
|
|
110
118
|
|
|
111
119
|
Channel names are plain text, not a PostgreSQL enum. Adding a future channel
|
|
@@ -148,25 +156,84 @@ const result = await notificationCore.publishAndDispatch({
|
|
|
148
156
|
```
|
|
149
157
|
|
|
150
158
|
`publish()` only persists. `publishAndDispatch()` persists and immediately
|
|
151
|
-
attempts due
|
|
159
|
+
attempts every due delivery from that publication. Internally it drains large
|
|
160
|
+
publications in bounded delivery-id claim pages, so broadcasts above 1,000
|
|
161
|
+
deliveries are not left pending just because a single claim wave is capped. Both
|
|
162
|
+
methods use the same durable outbox records.
|
|
163
|
+
|
|
164
|
+
Publication validates the complete JSON payload before opening its transaction.
|
|
165
|
+
`data`, channel-specific values, and other persisted payload properties must be
|
|
166
|
+
recursive JSON values: cycles, `undefined`, functions, symbols, bigints,
|
|
167
|
+
non-finite numbers, sparse arrays, accessors, and non-plain objects are rejected
|
|
168
|
+
with `NotificationConfigurationError`. TypeScript's `JsonValue` cannot express
|
|
169
|
+
the finite-number restriction, so this is also enforced at runtime. Invalid
|
|
170
|
+
publish input creates no notification or delivery rows.
|
|
171
|
+
|
|
172
|
+
Every persisted public string and JSON key/value must also be PostgreSQL-safe
|
|
173
|
+
UTF-8 text: NUL and unpaired UTF-16 surrogates are rejected, while valid
|
|
174
|
+
surrogate pairs such as emoji remain supported. Optional actor/entity ids must
|
|
175
|
+
be non-empty strings when present. Runtime booleans are exact booleans; truthy
|
|
176
|
+
or falsy substitutes are not accepted.
|
|
177
|
+
|
|
178
|
+
Configuration objects, public method option/input wrappers, and channel-keyed
|
|
179
|
+
maps must be plain or null-prototype records. Maps, Sets, Dates, arrays,
|
|
180
|
+
functions, class instances, symbols, and accessor properties are rejected at
|
|
181
|
+
those record boundaries. UNC captures every accepted field and array element
|
|
182
|
+
through its descriptor into an immutable snapshot before opening a transaction
|
|
183
|
+
or invoking a callback, so caller-side getters and later mutations cannot
|
|
184
|
+
change one operation midway through execution. Drizzle database and schema
|
|
185
|
+
objects are opaque values and are not subjected to the plain-record rule.
|
|
186
|
+
Public arrays must use `Array.prototype` directly and contain only canonical
|
|
187
|
+
indices from zero through `2^32 - 2` below their captured length. Subclasses,
|
|
188
|
+
exotic prototypes, accessors, sparse entries, and all extra properties are
|
|
189
|
+
rejected without invoking getters.
|
|
190
|
+
|
|
191
|
+
Validation is iterative and bounded to 256 nesting levels and 10,000 total JSON
|
|
192
|
+
nodes per checked value. It inspects own property descriptors without invoking
|
|
193
|
+
getters, rejects symbol/non-canonical/extra array properties, and copies accepted
|
|
194
|
+
values through descriptors before persistence. A present `channelData` must be
|
|
195
|
+
a plain own-key JSON object; `null`, `false`, `0`, and empty strings are invalid
|
|
196
|
+
rather than being treated as absence.
|
|
197
|
+
|
|
198
|
+
Recipients and deliveries are inserted and read in bulk. The number of database
|
|
199
|
+
query waves stays constant as recipient/channel count grows, while returned
|
|
200
|
+
recipients preserve first-seen input order and channels remain unique.
|
|
152
201
|
|
|
153
202
|
A core constructed with a Drizzle `PgTransaction` is publish-only. This lets an
|
|
154
203
|
application commit its own row and `publish()` atomically. Do not call
|
|
155
|
-
`publishAndDispatch()` or `dispatchDue()` on that core:
|
|
156
|
-
or invoking an adapter. After the outer transaction
|
|
157
|
-
core constructed from the top-level `PgDatabase`.
|
|
204
|
+
`publishAndDispatch()`, `dispatchUntilIdle()`, or `dispatchDue()` on that core:
|
|
205
|
+
all fail before writing or invoking an adapter. After the outer transaction
|
|
206
|
+
commits, dispatch with a core constructed from the top-level `PgDatabase`.
|
|
158
207
|
|
|
159
|
-
For scheduled delivery
|
|
160
|
-
application already owns:
|
|
208
|
+
For scheduled delivery, retries, or queue-style workers, call
|
|
209
|
+
`dispatchUntilIdle()` from infrastructure the application already owns:
|
|
161
210
|
|
|
162
211
|
```ts
|
|
163
|
-
await notificationCore.
|
|
212
|
+
const result = await notificationCore.dispatchUntilIdle({
|
|
164
213
|
workerId: process.env.HOSTNAME,
|
|
165
214
|
limit: 100,
|
|
166
215
|
concurrency: 10,
|
|
216
|
+
maxBatches: 25,
|
|
217
|
+
deadlineMs: 30_000,
|
|
167
218
|
});
|
|
168
219
|
```
|
|
169
220
|
|
|
221
|
+
`dispatchDue()` accepts `limit` from 1 through 1000 and `concurrency` from 1
|
|
222
|
+
through 100; the limit is a per-call worker claim cap. `dispatchUntilIdle()`
|
|
223
|
+
uses the same limit and concurrency boundaries, keeps one stable worker id for
|
|
224
|
+
its drain, stops when no due rows remain, and also stops on `maxBatches`,
|
|
225
|
+
`deadlineMs`, a high-failure batch, or a channel policy deferral. By default a
|
|
226
|
+
batch stops the drain when at least 80% of attempted deliveries fail or
|
|
227
|
+
schedule a retry, which prevents a broken provider from being hammered in a
|
|
228
|
+
tight loop. When due rows exist but a channel rate limit or cooldown blocks
|
|
229
|
+
claiming them, the stop reason is `rate-limited` and `deferredUntil` reports
|
|
230
|
+
the earliest known channel wake-up time. `publishAndDispatch()` accepts the
|
|
231
|
+
same concurrency range and uses capped internal claim pages until every due
|
|
232
|
+
delivery from its own publication has been attempted once, unless a configured
|
|
233
|
+
channel policy defers some rows. Runtime values must be finite positive
|
|
234
|
+
integers; invalid values fail before publishing, claiming, or calling a
|
|
235
|
+
provider.
|
|
236
|
+
|
|
170
237
|
This can run from a cron handler, queue consumer, scheduled function, or an
|
|
171
238
|
application-owned interval. The package starts no timer and exposes no port.
|
|
172
239
|
Concurrent workers claim rows with `FOR UPDATE SKIP LOCKED`; abandoned
|
|
@@ -175,6 +242,58 @@ Concurrent workers claim rows with `FOR UPDATE SKIP LOCKED`; abandoned
|
|
|
175
242
|
a separate claim token, so two attempts from the same worker cannot finalize
|
|
176
243
|
each other's rows.
|
|
177
244
|
|
|
245
|
+
## Channel dispatch policies
|
|
246
|
+
|
|
247
|
+
UNC can enforce provider-agnostic channel policy without knowing the provider
|
|
248
|
+
SDK. Configure static channel rate windows and circuit-breaker cooldowns when
|
|
249
|
+
creating the core:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const notificationCore = createNotificationCore<AppChannel>({
|
|
253
|
+
db,
|
|
254
|
+
adapters,
|
|
255
|
+
channelPolicies: {
|
|
256
|
+
sms: {
|
|
257
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
258
|
+
circuitBreaker: {
|
|
259
|
+
failureRatio: 0.8,
|
|
260
|
+
minAttempts: 20,
|
|
261
|
+
cooldownMs: 5 * 60_000,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
email: {
|
|
265
|
+
rateLimit: { max: 500, windowMs: 60_000 },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Rate limits are durable per channel and shared by concurrent workers through
|
|
272
|
+
the `unc_dispatch_state` table. A row that is deferred by channel policy is not
|
|
273
|
+
claimed and does not consume an attempt. Other channels keep dispatching in the
|
|
274
|
+
same worker call.
|
|
275
|
+
|
|
276
|
+
Circuit breakers are also channel-scoped and durable. After a batch for one
|
|
277
|
+
channel reaches the configured failure ratio and minimum attempted deliveries,
|
|
278
|
+
UNC opens that channel's cooldown. Claimed rows still finalize normally as
|
|
279
|
+
`sent`, `retrying`, or `failed`; the cooldown only prevents another immediate
|
|
280
|
+
wave from hammering the same provider boundary.
|
|
281
|
+
|
|
282
|
+
Adapters can also return provider throttle information without coupling UNC to
|
|
283
|
+
the provider:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
throw new NotificationDeliveryError("rate_limited", "Provider throttled", {
|
|
287
|
+
retryAfterMs: 120_000,
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`retryAfterMs` schedules the failed delivery's next attempt and opens the same
|
|
292
|
+
durable channel cooldown for fresh due rows. It is a finite non-negative number
|
|
293
|
+
no greater than 30 days. It does not replace provider-specific credentials,
|
|
294
|
+
contact lookup, account management, or webhooks; those remain in the adapter and
|
|
295
|
+
application.
|
|
296
|
+
|
|
178
297
|
## Preferences
|
|
179
298
|
|
|
180
299
|
Preferences are overrides, not a copied matrix. This keeps storage small when
|
|
@@ -260,6 +379,11 @@ await notificationCore.publish({
|
|
|
260
379
|
});
|
|
261
380
|
```
|
|
262
381
|
|
|
382
|
+
The only accepted runtime policy values are `"respect"` and `"ignore"`.
|
|
383
|
+
Omitting the property defaults to `"respect"`; an explicitly supplied `null`,
|
|
384
|
+
`undefined`, or any other value is a `NotificationConfigurationError` before
|
|
385
|
+
the publication transaction starts.
|
|
386
|
+
|
|
263
387
|
## In-app inbox
|
|
264
388
|
|
|
265
389
|
```ts
|
|
@@ -307,6 +431,14 @@ throw new NotificationDeliveryError(
|
|
|
307
431
|
);
|
|
308
432
|
```
|
|
309
433
|
|
|
434
|
+
`NotificationDeliveryError` accepts omitted options or a plain/null-prototype
|
|
435
|
+
options record. If `retryable` is present it must be an exact boolean; only
|
|
436
|
+
omission defaults it to `true`. If `retryAfterMs` is present it must be a finite
|
|
437
|
+
non-negative number no greater than 30 days. The code is trimmed, non-empty,
|
|
438
|
+
PostgreSQL-safe, and at most 191 characters. The message is non-empty,
|
|
439
|
+
PostgreSQL-safe, and at most 10,000 characters. Options are descriptor-captured
|
|
440
|
+
once before the Error is constructed.
|
|
441
|
+
|
|
310
442
|
Unknown errors are retryable. The default backoff is 1 minute, 5 minutes,
|
|
311
443
|
15 minutes, 1 hour, then 6 hours. Configure `retryDelayMs` and
|
|
312
444
|
`defaultMaxAttempts` for application policy. A failed delivery can be explicitly
|
|
@@ -315,11 +447,67 @@ is claimed, before the adapter runs. `requeueDelivery()` preserves that counter
|
|
|
315
447
|
by default and therefore requires remaining attempt capacity; pass a larger
|
|
316
448
|
`maxAttempts`, or set `resetAttempts: true` to start a new attempt budget.
|
|
317
449
|
Only a `failed` delivery whose notification is not canceled can be requeued.
|
|
450
|
+
Constructor defaults, scalar or per-channel publish overrides, and requeue
|
|
451
|
+
overrides all accept `maxAttempts` from 1 through 2,147,483,647, matching the
|
|
452
|
+
positive PostgreSQL `integer` range. Larger values fail before a transaction or
|
|
453
|
+
claim, and claim predicates prevent `attempts` from overflowing that range.
|
|
454
|
+
|
|
455
|
+
`retryDelayMs` may return a number or an exact same-realm native Node
|
|
456
|
+
`Promise<number>`. UNC installs the deadline before invoking the policy and
|
|
457
|
+
awaits it inside that boundary for at most 100 ms measured with a trusted
|
|
458
|
+
monotonic clock. Synchronous policy or Promise handling that crosses the
|
|
459
|
+
deadline cannot submit a late value. The resolved value must be finite,
|
|
460
|
+
non-negative, and produce a valid retry timestamp. Synchronous throws,
|
|
461
|
+
asynchronous rejections, never-settling Promises, timeouts, and
|
|
462
|
+
invalid values are consumed and logged before the deterministic built-in
|
|
463
|
+
backoff is used; the provider failure is still durably finalized and never left
|
|
464
|
+
stuck in `processing`. Late policy rejections are consumed. Logger callbacks may
|
|
465
|
+
return `void` or an exact native `Promise<void>`. UNC observes that Promise
|
|
466
|
+
immediately without awaiting it, so rejection is consumed while dispatch remains
|
|
467
|
+
independent. The logger callback itself is synchronous application code and must
|
|
468
|
+
return promptly.
|
|
469
|
+
|
|
470
|
+
The monotonic clock, timer functions, Promise constructor/prototype/`then`,
|
|
471
|
+
Promise brand check, and required Object/Reflect inspection operations are
|
|
472
|
+
captured and bound when the module initializes. Deadline settlement is installed
|
|
473
|
+
before application policy code runs. For an accepted native Promise, rejection
|
|
474
|
+
handling is attached before UNC checks whether synchronous policy work already
|
|
475
|
+
crossed the deadline. Fulfillment and rejection callbacks read the captured
|
|
476
|
+
monotonic clock before settling the deadline, so a timer starved by blocking
|
|
477
|
+
event-loop work cannot admit a value produced after 100 ms. UNC does not depend
|
|
478
|
+
on mutable public `Promise.race`.
|
|
479
|
+
|
|
480
|
+
Arbitrary `PromiseLike` values, custom thenables, cross-realm Promises, Promise
|
|
481
|
+
subclasses, and genuine Promises with own `then`/`constructor` hooks are invalid.
|
|
482
|
+
UNC uses Node's hook-free Promise brand check plus the captured exact native
|
|
483
|
+
prototype/intrinsics and never reads or calls an invalid value's `then` property.
|
|
484
|
+
An invalid retry result uses deterministic fallback; an invalid logger result is
|
|
485
|
+
ignored. The caller remains responsible for any rejected Promise hidden inside
|
|
486
|
+
an invalid object.
|
|
318
487
|
|
|
319
488
|
If a provider callback already has its own durable queue, returning after a
|
|
320
489
|
successful enqueue is correct. UNC then tracks acceptance by that
|
|
321
490
|
queue; provider delivery events may be stored in the adapter's own tables.
|
|
322
491
|
|
|
492
|
+
After an adapter resolves, provider success is final even if its optional return
|
|
493
|
+
value is malformed. `providerMessageId`, when present, must be a non-empty
|
|
494
|
+
string of at most 10,000 characters. `metadata`, when present, must be a finite
|
|
495
|
+
recursive JSON object under the same rules as publish input. If either field is
|
|
496
|
+
invalid, UNC discards the entire optional adapter result, durably marks the
|
|
497
|
+
delivery `sent`, stores `invalid_adapter_result` diagnostics, and logs the
|
|
498
|
+
problem without invoking the provider again.
|
|
499
|
+
|
|
500
|
+
Optional adapter strings and metadata must obey the same PostgreSQL-safe string
|
|
501
|
+
contract. Invalid NUL or unpaired-surrogate output is discarded through the
|
|
502
|
+
same sent-safe path. UNC captures the finalization timestamp with the claim,
|
|
503
|
+
before calling the provider, so a later failure in an application-supplied
|
|
504
|
+
`now()` callback cannot turn provider acceptance into a resend risk.
|
|
505
|
+
|
|
506
|
+
Diagnostic extraction never invokes error getters, `toString`, or coercion
|
|
507
|
+
hooks. Persisted `errorMessage`, logger context, and returned `DispatchResult`
|
|
508
|
+
share one 10,000-character cap, including malformed adapter-result diagnostics;
|
|
509
|
+
unsafe PostgreSQL characters are replaced before the diagnostic is persisted.
|
|
510
|
+
|
|
323
511
|
## Idempotency and scheduling
|
|
324
512
|
|
|
325
513
|
An `idempotencyKey` is unique per recipient. Publishing the same key again
|
|
@@ -340,6 +528,8 @@ and its `DispatchResult` reports the durable state it observed.
|
|
|
340
528
|
- No background timers or service runtime.
|
|
341
529
|
- No assumption that recipient ids are UUIDs.
|
|
342
530
|
- No assumption that every notification is in-app.
|
|
531
|
+
- Provider-agnostic channel rate/cooldown state is stored in UNC tables, not in
|
|
532
|
+
process memory.
|
|
343
533
|
- Provider callbacks are invoked only by a top-level-database core, after the
|
|
344
534
|
claim transaction commits.
|
|
345
535
|
- Preferences disabled at publication are recorded as `skipped` deliveries for
|
package/dist/core.d.ts
CHANGED
|
@@ -2,6 +2,35 @@ import { type PgDatabase } from "drizzle-orm/pg-core";
|
|
|
2
2
|
import { type NotificationSchema } from "./schema.js";
|
|
3
3
|
import { PREFERENCE_WILDCARD, type DeliveryAdapters, type DispatchResult, type NotificationLogger, type InboxCursor, type InboxPage, type JsonObject, type PreferenceDefaults, type PreferenceOverride, type PublishInput, type PublishedNotification, type ResolvedPreference } from "./types.js";
|
|
4
4
|
type AnyPgDatabase = PgDatabase<any, any, any>;
|
|
5
|
+
export type DispatchUntilIdleOptions = {
|
|
6
|
+
limit?: number;
|
|
7
|
+
workerId?: string;
|
|
8
|
+
concurrency?: number;
|
|
9
|
+
maxBatches?: number;
|
|
10
|
+
deadlineMs?: number;
|
|
11
|
+
stopOnFailureRatio?: number;
|
|
12
|
+
stopOnFailureCount?: number;
|
|
13
|
+
};
|
|
14
|
+
export type DispatchUntilIdleStopReason = "idle" | "maxBatches" | "deadline" | "failure-threshold" | "rate-limited";
|
|
15
|
+
export type DispatchUntilIdleResult<TChannel extends string = string> = {
|
|
16
|
+
batches: number;
|
|
17
|
+
reason: DispatchUntilIdleStopReason;
|
|
18
|
+
dispatched: Array<DispatchResult<TChannel>>;
|
|
19
|
+
deferredUntil?: Date;
|
|
20
|
+
};
|
|
21
|
+
export type ChannelRateLimitPolicy = {
|
|
22
|
+
max: number;
|
|
23
|
+
windowMs: number;
|
|
24
|
+
};
|
|
25
|
+
export type ChannelCircuitBreakerPolicy = {
|
|
26
|
+
failureRatio?: number;
|
|
27
|
+
minAttempts?: number;
|
|
28
|
+
cooldownMs?: number;
|
|
29
|
+
};
|
|
30
|
+
export type ChannelDispatchPolicy = {
|
|
31
|
+
rateLimit?: ChannelRateLimitPolicy;
|
|
32
|
+
circuitBreaker?: ChannelCircuitBreakerPolicy;
|
|
33
|
+
};
|
|
5
34
|
export type NotificationCoreConfig<TChannel extends string, TData extends JsonObject = JsonObject> = {
|
|
6
35
|
db: AnyPgDatabase;
|
|
7
36
|
schema?: NotificationSchema;
|
|
@@ -10,7 +39,8 @@ export type NotificationCoreConfig<TChannel extends string, TData extends JsonOb
|
|
|
10
39
|
preferenceDefaults?: PreferenceDefaults<TChannel>;
|
|
11
40
|
defaultPreferenceEnabled?: boolean;
|
|
12
41
|
defaultMaxAttempts?: number;
|
|
13
|
-
|
|
42
|
+
channelPolicies?: Partial<Record<TChannel, ChannelDispatchPolicy>>;
|
|
43
|
+
retryDelayMs?: (attempt: number, error: unknown) => number | Promise<number>;
|
|
14
44
|
lockTimeoutMs?: number;
|
|
15
45
|
now?: () => Date;
|
|
16
46
|
logger?: NotificationLogger;
|
|
@@ -31,6 +61,7 @@ export declare class UnifiedNotificationCore<TChannel extends string, TData exte
|
|
|
31
61
|
workerId?: string;
|
|
32
62
|
concurrency?: number;
|
|
33
63
|
}): Promise<Array<DispatchResult<TChannel>>>;
|
|
64
|
+
dispatchUntilIdle(options?: DispatchUntilIdleOptions): Promise<DispatchUntilIdleResult<TChannel>>;
|
|
34
65
|
setPreference(preference: PreferenceOverride<TChannel>): Promise<void>;
|
|
35
66
|
setPreferences(recipientId: string, preferences: ReadonlyArray<Omit<PreferenceOverride<TChannel>, "recipientId">>): Promise<void>;
|
|
36
67
|
clearPreference(input: {
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|