unified-notification-core 0.1.0 → 0.1.1

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - Made delivery claims unique per row and attempt, with durable attempt counts
6
+ incremented before adapter invocation and exhausted stale claims closed.
7
+ - Made cancellation races truthful: current-claim provider success is `sent`,
8
+ failure remains `canceled`, and stale callbacks report durable state without
9
+ mutating it.
10
+ - Restricted requeueing to failed deliveries on active notifications and made
11
+ attempt-budget/reset semantics explicit.
12
+ - Normalized recipient/topic/channel keys across publish, preference APIs, the
13
+ public resolver, and configured defaults; canonical default collisions now
14
+ fail fast. Oversized delivery error codes are rejected before persistence.
15
+ - Preserved `0.1.0` whitespace preference rows without a migration, with
16
+ deterministic opt-out-wins conflict handling and atomic consolidation on
17
+ writes/clears.
18
+ - Matched legacy SQL key normalization to the complete ECMAScript trim set,
19
+ batched preference reads into exact plus one legacy fallback query, and added
20
+ the corresponding functional recipient index to the schema factory.
21
+ - Hardened adapters, defaults, attempt limits, channel payloads, and inbox keys
22
+ against prototype-name collisions and runtime-invalid values.
23
+ - Prevented provider dispatch through transaction-scoped cores. Transactional
24
+ `publish()` remains supported, while `publishAndDispatch()` and `dispatchDue()`
25
+ fail before publication, claiming, or adapter invocation.
26
+
3
27
  ## 0.1.0
4
28
 
5
29
  - Established the UNC (Unified Notification Core) package identity.
package/README.md CHANGED
@@ -100,6 +100,12 @@ createNotificationSchema({ tablePrefix: "app_notification_" });
100
100
  Run the application's normal Drizzle schema workflow after exporting the
101
101
  tables. The package never connects to or mutates a database by itself.
102
102
 
103
+ Upgrading an existing `0.1.0` database to `0.1.1` adds a functional recipient
104
+ preference index used by legacy-key fallback reads. Generate and apply that
105
+ schema diff through the application's normal reviewed migration workflow. UNC
106
+ does not create the index at runtime; correctness is preserved before the index
107
+ is applied, but legacy fallback reads can be slower.
108
+
103
109
  ## Define channels
104
110
 
105
111
  Channel names are plain text, not a PostgreSQL enum. Adding a future channel
@@ -144,6 +150,12 @@ const result = await notificationCore.publishAndDispatch({
144
150
  `publish()` only persists. `publishAndDispatch()` persists and immediately
145
151
  attempts due deliveries. Both use the same durable outbox records.
146
152
 
153
+ A core constructed with a Drizzle `PgTransaction` is publish-only. This lets an
154
+ application commit its own row and `publish()` atomically. Do not call
155
+ `publishAndDispatch()` or `dispatchDue()` on that core: both fail before writing
156
+ or invoking an adapter. After the outer transaction commits, dispatch with a
157
+ core constructed from the top-level `PgDatabase`.
158
+
147
159
  For scheduled delivery or retries, call `dispatchDue()` from infrastructure the
148
160
  application already owns:
149
161
 
@@ -159,6 +171,9 @@ This can run from a cron handler, queue consumer, scheduled function, or an
159
171
  application-owned interval. The package starts no timer and exposes no port.
160
172
  Concurrent workers claim rows with `FOR UPDATE SKIP LOCKED`; abandoned
161
173
  `processing` rows become eligible again after the configured lock timeout.
174
+ `workerId` is diagnostic (maximum 154 characters). Every claimed delivery gets
175
+ a separate claim token, so two attempts from the same worker cannot finalize
176
+ each other's rows.
162
177
 
163
178
  ## Preferences
164
179
 
@@ -215,6 +230,23 @@ const notificationCore = createNotificationCore<AppChannel>({
215
230
  });
216
231
  ```
217
232
 
233
+ Recipient, topic, and channel keys are trimmed at the public boundary, including
234
+ `resolvePreference()` and configured defaults. Keys that become empty are
235
+ rejected. Configuration also fails fast when two raw default keys normalize to
236
+ the same topic or channel, rather than silently choosing one.
237
+
238
+ Reads remain compatible with `0.1.0` preference rows whose stored keys contain
239
+ surrounding whitespace. If multiple stored rows collapse to one canonical
240
+ override, opt-out wins: any `enabled: false` row disables that topic/channel.
241
+ Writing or clearing that override atomically consolidates its legacy duplicates.
242
+ Channel-keyed configuration is copied into own-key registries, so names such as
243
+ `toString`, `constructor`, and `__proto__` have no inherited behavior and work
244
+ only when explicitly configured.
245
+
246
+ Batch publication loads preferences in one exact indexed query plus at most one
247
+ legacy canonical fallback query for the whole recipient batch. It does not scan
248
+ preferences once per recipient.
249
+
218
250
  Use `preferencePolicy: "ignore"` only for notifications that legally or
219
251
  operationally must be delivered regardless of opt-out:
220
252
 
@@ -257,7 +289,8 @@ const unread = await notificationCore.unreadCount(userId);
257
289
  ```
258
290
 
259
291
  Inbox mutations always require the recipient id as well as the notification id,
260
- so a route can scope changes to its authenticated user.
292
+ so a route can scope changes to its authenticated user. Inbox recipient ids use
293
+ the same trim-and-reject-empty normalization as publication and preferences.
261
294
 
262
295
  ## Retry behavior
263
296
 
@@ -277,7 +310,11 @@ throw new NotificationDeliveryError(
277
310
  Unknown errors are retryable. The default backoff is 1 minute, 5 minutes,
278
311
  15 minutes, 1 hour, then 6 hours. Configure `retryDelayMs` and
279
312
  `defaultMaxAttempts` for application policy. A failed delivery can be explicitly
280
- requeued with `requeueDelivery()`.
313
+ requeued with `requeueDelivery()`. Attempts are durably incremented when a row
314
+ is claimed, before the adapter runs. `requeueDelivery()` preserves that counter
315
+ by default and therefore requires remaining attempt capacity; pass a larger
316
+ `maxAttempts`, or set `resetAttempts: true` to start a new attempt budget.
317
+ Only a `failed` delivery whose notification is not canceled can be requeued.
281
318
 
282
319
  If a provider callback already has its own durable queue, returning after a
283
320
  successful enqueue is correct. UNC then tracks acceptance by that
@@ -289,8 +326,11 @@ An `idempotencyKey` is unique per recipient. Publishing the same key again
289
326
  returns the original notification and deliveries with `created: false`.
290
327
 
291
328
  `scheduledFor` delays all selected channels. `cancel(notificationId)` cancels
292
- pending/retrying/processing deliveries. Sent deliveries remain immutable audit
293
- history.
329
+ pending/retrying/processing deliveries. A provider callback already in flight
330
+ cannot be recalled: success from its current claim is persisted as `sent`
331
+ (`sent` is immutable audit history), while failure stays `canceled` and is not
332
+ retried. A stale callback never overwrites the state written by a newer claim,
333
+ and its `DispatchResult` reports the durable state it observed.
294
334
 
295
335
  ## Package guarantees
296
336
 
@@ -300,7 +340,8 @@ history.
300
340
  - No background timers or service runtime.
301
341
  - No assumption that recipient ids are UUIDs.
302
342
  - No assumption that every notification is in-app.
303
- - Provider callbacks are invoked outside database transactions.
343
+ - Provider callbacks are invoked only by a top-level-database core, after the
344
+ claim transaction commits.
304
345
  - Preferences disabled at publication are recorded as `skipped` deliveries for
305
346
  auditability.
306
347
 
package/dist/core.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PgDatabase } from "drizzle-orm/pg-core";
1
+ 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>;
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAerE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,mBAAmB,EAEnB,KAAK,gBAAgB,EAErB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,UAAU,EAEf,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAEpB,KAAK,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE/C,MAAM,MAAM,sBAAsB,CAChC,QAAQ,SAAS,MAAM,EACvB,KAAK,SAAS,UAAU,GAAG,UAAU,IACnC;IACF,EAAE,EAAE,aAAa,CAAC;IAClB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAClD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AA0GF,qBAAa,uBAAuB,CAClC,QAAQ,SAAS,MAAM,EACvB,KAAK,SAAS,UAAU,GAAG,UAAU;;gBAgBzB,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAwCrD,OAAO,CACX,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,GACnC,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAkO5C,kBAAkB,CACtB,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,EACpC,OAAO,GAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACjB,GACL,OAAO,CAAC;QACT,SAAS,EAAE,KAAK,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC;QAClD,UAAU,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;KAC7C,CAAC;IAiBI,WAAW,CACf,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;KACjB,GACL,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;IAIrC,aAAa,CACjB,UAAU,EAAE,kBAAkB,CAAC,QAAQ,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC;IA4DV,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,aAAa,CACxB,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAClD,GACA,OAAO,CAAC,IAAI,CAAC;IA+DV,eAAe,CAAC,KAAK,EAAE;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,QAAQ,GAAG,OAAO,mBAAmB,CAAC;KAChD,GAAG,OAAO,CAAC,OAAO,CAAC;IAyCd,cAAc,CAAC,KAAK,EAAE;QAC1B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;QAC1B,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;KAC/B,GAAG,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC;IA6C1C,SAAS,CAAC,KAAK,EAAE;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IA+DvB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BjD,QAAQ,CAAC,KAAK,EAAE;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBd,OAAO,CAAC,KAAK,EAAE;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBd,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiDhD,eAAe,CAAC,KAAK,EAAE;QAC3B,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,OAAO,CAAC;CA4crB;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,SAAS,MAAM,EACvB,KAAK,SAAS,UAAU,GAAG,UAAU,EAErC,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,GAC9C,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAE1C"}