@gj-kit/nest-notifications
A dispatcher with no presenter, or quiet hours with no time zone, is a compile error — not a DST bug in production.
- npm
0.1.2 - 230+ unit tests
- 30 store contract cases for your database
- 0 runtime dependencies
- 4 entry points, ESM + CJS
Keep learning
Section titled “Keep learning”Why this exists
Section titled “Why this exists”A hand-rolled notification outbox loses deliveries to non-atomic claims, writes duplicate inbox rows when two workers grab the same batch, and pushes to accounts that were deleted mid-relay. Then quiet hours turn out to be a fixed offset added to a Date, drifting an hour every DST transition, and a batch window that does not divide 24h moves the aggregation bucket every day.
What it does about it
Section titled “What it does about it”- Required options, enforced by tsc — createNotificationDispatcher without a presenter and createQuietHoursPolicy without a timeZone do not compile — no default copy, no default region.
- 30 contract cases for your database — notificationStoreContractCases() returns 30 runnable cases covering 29 numbered obligations: atomic claim, batch uniqueness, purge-versus-relay interleaving.
- A core Nest cannot leak into — Guards assert the strings @nestjs, rxjs and reflect-metadata appear nowhere in src/core, src/expo or src/testing, and nowhere in the dist/core., dist/expo. and dist/testing.* module graphs — with a control case requiring dist/index.js to contain @nestjs, so the guard is proven not to be checking an empty set.
- The latency hint returns void — NotificationPipelineWakeup.request() returns void — nothing to await, no result to inspect, no error to catch — so it cannot be mistaken for the owner of correctness. A test wires only that hint, advances the clock 12 hours, and finds the batched delivery still undelivered until dispatchDue() is called.
- DST resolution is a contract — A spring-forward gap releases at the first instant after it, an autumn fold at the earlier one, and batchWindowMs must divide 24h evenly or assembly throws ERR_NOTIFICATION_POLICY_INVALID.
Golden path
Section titled “Golden path”Outcome: Durable relay and dispatch runners wired to your stores and push gateway.
1. Install
Section titled “1. Install”pnpm add @gj-kit/nest-notifications2. Keep the app-owned boundary explicit
Section titled “2. Keep the app-owned boundary explicit”Keep product policy in app-owned stores, presenter, and scheduling policy before registering the Nest module.
3. Start with the smallest integration
Section titled “3. Start with the smallest integration”Copy this first, then replace only the app-owned values named above.
import { NestNotificationsModule, type NestNotificationsOptions } from '@gj-kit/nest-notifications';
declare const options: NestNotificationsOptions; // App stores, presenter, policy, and push gateway.
export const notifications = NestNotificationsModule.forRoot(options);What that looks like
Section titled “What that looks like”Both halves are load-bearing: the policy cannot be constructed without a time zone, and the same 30 cases the library runs against its in-memory stores become the acceptance criteria for yours.
import { createQuietHoursPolicy } from '@gj-kit/nest-notifications/core';import { notificationStoreContractCases } from '@gj-kit/nest-notifications/testing';import type { NotificationStoreSuite } from '@gj-kit/nest-notifications/testing';
declare function myPostgresStores(): Promise<NotificationStoreSuite>; // the app owns this
// The library holds no regional default, so the zone cannot be left unsaid.export const policy = createQuietHoursPolicy({ timeZone: 'Asia/Seoul', quietHours: { startHour: 22, endHour: 8 }, batchWindowMs: 600_000, // must divide 24h, or assembly throws ERR_NOTIFICATION_POLICY_INVALID});// Drop timeZone and tsc stops the build:// error TS2345: Argument of type '{ quietHours: { startHour: number; endHour: number; }; }'// is not assignable to parameter of type 'QuietHoursPolicyOptions'.
// The 30 cases the library runs on its own in-memory stores, now run on yours.for (const testCase of notificationStoreContractCases({ concurrency: 8 })) { it(testCase.name, () => testCase.run(myPostgresStores));}Use it when
Section titled “Use it when”Use it when product events must become durable, deduplicated notification work without making delivery policy part of the product domain.
Do not use it when
Section titled “Do not use it when”Do not move product copy, recipient policy, or user-preference decisions into the generic relay.
Runtime and peers
Section titled “Runtime and peers”Engines: node >=20
| Peer | Supported range | Required for |
|---|---|---|
@nestjs/common |
`^10 | |
reflect-metadata |
`^0.1.13 | |
rxjs |
^7 |
the package integration |
Public subpaths
Section titled “Public subpaths”| Entry | Exported symbols |
|---|---|
Root entry |
77 |
./core |
78 |
./expo |
11 |
./testing |
14 |
Safety boundary
Section titled “Safety boundary”Keep credentials, endpoint ownership, and user-visible product wording in the application. Use the typed error and delivery outcomes instead of raw provider failures.
API reference
Section titled “API reference”complete API reference contains every public function, class, type, constant, and error contract generated from release declarations. Each item shows its import path, release signature, and package version.
Package relationships
Section titled “Package relationships”| Relationship | Package | Why combine them |
|---|---|---|
| Standalone | — | This package can be adopted without another GJ Kit package. |
| Optional companion | @gj-kit/nest-operations-jobs |
Schedule application-owned relay or dispatch work through a protected operations boundary. |