Files
suaveplan/packages/commerce/contacts/audience-rules/src/segment-refresher/segment-refresher.md
2026-05-27 23:29:53 +01:00

3.7 KiB

segment-refresher

(no submodules)

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

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

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.