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,67 @@
# `in-app-channel` — In-app notification channel
## Purpose
`in-app-channel.ts` ships the `InAppChannel` implementation that stores notifications for an in-app inbox / unread badge. The default storage is an in-memory `Map<recipientId, InAppNotification[]>`, but the constructor accepts any `InAppStore` implementation so consumers can persist to SurrealDB, Postgres, Redis, or a local IndexedDB browser store. The class is universal — no Node-only or browser-only APIs.
## Features
- Default in-memory store — useful for tests and prototypes.
- Pluggable `InAppStore` adapter for persistence.
- `getByRecipient(recipientId)` and `markRead(id)` convenience methods.
- Records unread by default — flips to read via `markRead`.
- Returns `DeliveryResult { success: false }` when the store throws, never propagates.
- Injectable `Clock` for deterministic `createdAt` and `timestamp` fields.
## Usage: Basic
```ts
import { InAppChannel } from "@suaveplan/notification-channel";
const channel = new InAppChannel();
await channel.send(notification);
const unread = (await channel.getByRecipient(notification.recipient)).filter(
(n) => !n.read
);
```
## Usage: Advanced
```ts
import {
InAppChannel,
type InAppNotification,
type InAppStore,
} from "@suaveplan/notification-channel";
const dbStore: InAppStore = {
async save(n) {
await db.execute("INSERT INTO notifications ...", [n]);
},
async getByRecipient(recipientId) {
return db.query<InAppNotification>(
"SELECT * FROM notifications WHERE recipientId = ?",
[recipientId]
);
},
async markRead(id) {
await db.execute(
"UPDATE notifications SET read = true WHERE id = ?",
[id]
);
},
};
const channel = new InAppChannel(dbStore);
```
## API Reference
- `InAppChannel(store?: InAppStore, clock?: Clock)` — constructor.
- `InAppNotification` — `{ id, recipientId, subject?, body, read, createdAt, metadata? }`.
- `InAppStore` — `save`, `getByRecipient`, `markRead`.
- `channel.canSend(n)`, `channel.send(n)`, `channel.getByRecipient(id)`, `channel.markRead(id)`.
## Implementation Notes
The default in-memory store backs onto a private `Map` so multiple `InAppChannel` instances do not share state. The store factory closes over the per-instance map to avoid leaking persistence between independent channel objects (useful in tests that construct multiple channels). `markRead` is implemented by scanning every recipient's list — for production-scale deployments the consumer must inject a real store with an indexed lookup; the in-memory variant is a development convenience, not a recommended persistence layer.