Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user