first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# `preference-manager` — Per-user opt-in / opt-out + quiet hours
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`preference-manager.ts` ships the `PreferenceManager` class that answers, at every send-time, "should this notification reach this user on this channel right now?". The manager combines per-channel opt-in / opt-out preferences with optional quiet-hours configuration (with priority thresholds) and a pluggable `PreferenceStoreAdapter` for persistence.
## Features
- Per-user channel enable / disable, with explicit `enabledChannels` and `disabledChannels` sets.
- Default channels (configurable; built-in default: `["email", "in_app"]`).
- Quiet-hours config with `startHour`, `endHour` (wrap-around supported), and optional `days` filter.
- `urgentThreshold` controls which priorities punch through quiet hours (default `urgent`).
- `canDeliver(userId, channel, priority)` is the single decision point.
- Pluggable `PreferenceStoreAdapter` so persistence is delegated to SurrealDB, Postgres, Redis, etc.
- Injectable `Clock` for deterministic tests.
## Usage: Basic
```ts
import {
PreferenceManager,
type PreferenceStoreAdapter,
} from "@suaveplan/notification-channel";
const store: PreferenceStoreAdapter = makeStore();
const manager = new PreferenceManager({ store });
if (await manager.canDeliver("user-1", "email", "normal")) {
await router.send(...);
}
```
## Usage: Advanced
```ts
const manager = new PreferenceManager({
store,
defaultChannels: ["email", "push", "in_app"],
clock: () => Date.now(),
});
await manager.setEnabled("user-1", "sms", true);
await manager.setQuietHours("user-1", {
startHour: 22,
endHour: 7,
days: [1, 2, 3, 4, 5], // weekdays
});
// At dispatch time:
for (const channel of selected) {
if (await manager.canDeliver("user-1", channel, "high")) {
await router.send({ recipient: "user-1", body: text, channel });
}
}
```
## API Reference
- `PreferenceManager(options: PreferenceManagerOptions)` — constructor.
- `PreferenceManagerOptions` — `{ store, defaultChannels?, clock? }`.
- `canDeliver(userId, channel, priority): Promise<boolean>`.
- `getPreferences(userId): Promise<UserPreferences>`.
- `setEnabled(userId, channel, enabled): Promise<void>`.
- `setQuietHours(userId, quietHours | null): Promise<void>`.
- `resetPreferences(userId): Promise<boolean>`.
- `PreferenceStoreAdapter`, `UserPreferences`, `StoredPreferences`, `QuietHoursConfig`.
## Implementation Notes
Quiet-hours wrap around midnight when `startHour > endHour` (e.g. `22 → 7`). Priorities are ordered `low < normal < high < urgent`; if `urgentThreshold` is `"high"`, then both `high` and `urgent` punch through quiet hours. The day filter is opt-in — when omitted, quiet hours apply every day; when supplied, only the listed days (`0` = Sunday) trigger the window. `setQuietHours(userId, null)` clears the field; existing `enabledChannels`/`disabledChannels`/`urgentThreshold` are preserved. The store adapter is the single integration seam — the rest of the class is purely functional logic, so swapping persistence layers does not change behaviour.