Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
# segment-refresher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`segment-refresher.ts` orchestrates re-evaluation of one or many segments
and persists the resulting `contactIds` back to the `SegmentStore` cache. It
exposes a typed `EventBus<SegmentRefresherEventMap>` so downstream consumers
(broadcast schedulers, real-time dashboards, OTel exporters) can subscribe to
`segment:refreshed` without coupling to the refresher's internals. The
refresher is intentionally driver-agnostic — it does not own a timer; the
consumer drives `refresh` / `refreshAll` from whatever cadence policy
(cron, push, manual button) it prefers.
## Features
- `refresh(segmentId)` — single-segment re-evaluation; returns `undefined`
for an unknown ID without emitting.
- `refreshAll(tenantId)` — multi-segment fan-out; emits once per refreshed
segment.
- Typed `EventBus<SegmentRefresherEventMap>` — `events.on("segment:refreshed",
handler)` receives `{ segmentId, result }`.
- Cache write through `store.updateCache(segmentId, contactIds)` on every
refresh, updating `cachedContactIds`, `cachedCount`, `lastRefreshedAt`,
`updatedAt`.
- Pure composition of `SegmentEvaluator` + `SegmentStore` — both are
constructor-injected so test doubles wire in cleanly.
## Usage: Basic Example
```ts
import {
InMemorySegmentStore,
SegmentEvaluator,
SegmentRefresher,
} from "@suaveplan/audience-rules";
const store = new InMemorySegmentStore();
const evaluator = new SegmentEvaluator(async () => [
{ id: "c1", tags: ["vip"] },
]);
const refresher = new SegmentRefresher(store, evaluator);
const segment = await store.create({
tenantId: "t1",
name: "VIPs",
rules: [{ field: "tags", operator: "contains", value: "vip" }],
refreshMode: "manual",
});
const result = await refresher.refresh(segment.id);
```
## Usage: Advanced Example
```ts
import {
InMemorySegmentStore,
SegmentEvaluator,
SegmentRefresher,
} from "@suaveplan/audience-rules";
const refresher = new SegmentRefresher(
new InMemorySegmentStore(),
new SegmentEvaluator(async () => [])
);
refresher.events.on("segment:refreshed", ({ segmentId, result }) => {
// Forward to OTel, dashboard, downstream pub/sub, etc.
console.log(
`refreshed ${segmentId}: ${result.total} (${(result.matchRate * 100).toFixed(1)}%)`
);
});
await refresher.refreshAll("tenant-1");
```
## API Reference
- `interface SegmentRefresherEventMap` — `"segment:refreshed": { segmentId, result }`.
- `class SegmentRefresher` — constructor `(store: SegmentStore, evaluator: SegmentEvaluator)`.
- `refresher.events: EventBus<SegmentRefresherEventMap>` — typed event bus
from `@suaveplan/events`; `on` / `off` / `emit` / `use` / `clear`.
- `refresher.refresh(segmentId: string): Promise<SegmentEvaluationResult | undefined>`
- `refresher.refreshAll(tenantId: string): Promise<Map<string, SegmentEvaluationResult>>`
## Implementation Notes
- The migration swaps upstream's untyped `EventEmitter` for the suaveplan
`EventBus<TMap>`. The consumer API (`on("segment:refreshed", handler)`)
is preserved verbatim; only the type signature of `handler` becomes
precise.
- `refresh` returns `undefined` for a missing segment because the absence
is a normal (not exceptional) outcome — the consumer typically retries
with a corrected ID or surfaces a `404` at the API layer.
- `refreshAll` iterates sequentially; concurrent fan-out is intentionally
left to the caller (Promise.all over selected segment IDs) so the
refresher does not need to own a concurrency policy.
- The `events` field is publicly readable but the `EventBus` is mutable —
consumers are expected to subscribe, not replace.