import type { Clock, IdGenerator } from "@suaveplan/types"; import { AUDIENCE_RULES_CODES, AudienceRulesError, } from "../error-codes/error-codes.js"; import { createSegmentInputSchema, parseOrThrow, updateSegmentInputSchema, } from "../schemas/schemas.js"; import type { CreateSegmentInput, Segment, UpdateSegmentInput, } from "../types/segment.types.js"; /** Interface for segment persistence. */ export interface SegmentStore { create(input: CreateSegmentInput): Promise; getById(id: string): Promise; update(id: string, input: UpdateSegmentInput): Promise; delete(id: string): Promise; list(tenantId: string): Promise; updateCache( id: string, contactIds: readonly string[] ): Promise; } /** Constructor options for {@link InMemorySegmentStore}. */ export interface InMemorySegmentStoreOptions { /** Injected clock; defaults to `() => Date.now()`. */ readonly clock?: Clock; /** Injected ID generator; defaults to a deterministic per-instance counter. */ readonly idGenerator?: IdGenerator; } /** * In-memory `SegmentStore` implementation suitable for development, tests, and * lightweight production use. Uses `Clock` + `IdGenerator` injection from * `@suaveplan/types` — production paths never call `Date.now()` / `new Date()` * directly and never mutate a module-scope counter. */ export class InMemorySegmentStore implements SegmentStore { private readonly segments = new Map(); private readonly clock: Clock; private readonly idGenerator: IdGenerator; public constructor(options: InMemorySegmentStoreOptions = {}) { this.clock = options.clock ?? (() => Date.now()); this.idGenerator = options.idGenerator ?? this.makeDefaultIdGenerator(); } public async create(input: CreateSegmentInput): Promise { const validated = parseOrThrow( createSegmentInputSchema, input, "createSegment" ); const now = new Date(this.clock()); const segment: Segment = { id: this.idGenerator(), tenantId: validated.tenantId, name: validated.name, description: validated.description, rules: validated.rules, refreshMode: validated.refreshMode, createdAt: now, updatedAt: now, }; this.segments.set(segment.id, segment); return segment; } public async getById(id: string): Promise { return this.segments.get(id); } public async update( id: string, input: UpdateSegmentInput ): Promise { const validated = parseOrThrow( updateSegmentInputSchema, input, "updateSegment" ); const existing = this.segments.get(id); if (existing === undefined) return undefined; const updated: Segment = { ...existing, ...(validated.name !== undefined && { name: validated.name }), ...(validated.description !== undefined && { description: validated.description, }), ...(validated.rules !== undefined && { rules: validated.rules }), ...(validated.refreshMode !== undefined && { refreshMode: validated.refreshMode, }), updatedAt: new Date(this.clock()), }; this.segments.set(id, updated); return updated; } public async delete(id: string): Promise { return this.segments.delete(id); } public async list(tenantId: string): Promise { if (tenantId.length === 0) { throw new AudienceRulesError( AUDIENCE_RULES_CODES.INVALID_INPUT, "list requires a non-empty tenantId", { tenantId } ); } const results: Segment[] = []; for (const segment of this.segments.values()) { if (segment.tenantId === tenantId) results.push(segment); } return results; } public async updateCache( id: string, contactIds: readonly string[] ): Promise { const existing = this.segments.get(id); if (existing === undefined) return undefined; const now = new Date(this.clock()); const updated: Segment = { ...existing, cachedContactIds: contactIds, cachedCount: contactIds.length, lastRefreshedAt: now, updatedAt: now, }; this.segments.set(id, updated); return updated; } private makeDefaultIdGenerator(): IdGenerator { let counter = 0; return () => { counter += 1; return `seg_${counter}`; }; } }