{"version":3,"file":"completion-batcher.d.ts","sourceRoot":"","sources":["../../../../src/runs/background/completion-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,MAAM,WAAW,6BAA6B;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,+BAA+B,EAAE,6BAO7C,CAAC;AAOF,wBAAgB,4BAA4B,CAC3C,YAAY,CAAC,EAAE,qBAAqB,EACpC,QAAQ,CAAC,EAAE,qBAAqB,GAC9B,6BAA6B,CA8B/B;AAED,KAAK,WAAW,GAAG,OAAO,CAAC;AAE3B,UAAU,QAAQ;IACjB,UAAU,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CACxC;AAkBD,MAAM,WAAW,wBAAwB,CAAC,CAAC;IAC1C,MAAM,EAAE,6BAA6B,CAAC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC;IACnC,yEAAyE;IACzE,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,yDAAyD;IACzD,KAAK,IAAI,IAAI,CAAC;IACd,6DAA6D;IAC7D,OAAO,IAAI,CAAC,EAAE,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAqErG","sourcesContent":["/**\n * Smart completion batching with straggler handling.\n *\n * Holds successful async-completion notifications briefly so sibling jobs that\n * finish within a short window arrive as a single grouped message. A hard\n * max-wait cap (measured from the first item in a group) prevents holding\n * notifications indefinitely. After a group is emitted, late-finishing\n * siblings that arrive within the straggler window join a shorter \"straggler\"\n * group with reduced debounce and max-wait timers.\n *\n * Failure and attention signals bypass this batcher entirely. Callers must\n * flush() held items and emit those signals immediately so failures and\n * needs-attention notices are never delayed.\n */\n\nimport type { CompletionBatchConfig } from \"../../shared/types.ts\";\n\nexport type { CompletionBatchConfig };\n\nexport interface ResolvedCompletionBatchConfig {\n\tenabled: boolean;\n\tdebounceMs: number;\n\tmaxWaitMs: number;\n\tstragglerDebounceMs: number;\n\tstragglerMaxWaitMs: number;\n\tstragglerWindowMs: number;\n}\n\nexport const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {\n\tenabled: true,\n\tdebounceMs: 150,\n\tmaxWaitMs: 1000,\n\tstragglerDebounceMs: 75,\n\tstragglerMaxWaitMs: 400,\n\tstragglerWindowMs: 2000,\n};\n\nfunction parsePositiveInt(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;\n\treturn value;\n}\n\nexport function resolveCompletionBatchConfig(\n\tglobalConfig?: CompletionBatchConfig,\n\toverride?: CompletionBatchConfig,\n): ResolvedCompletionBatchConfig {\n\tconst enabled =\n\t\ttypeof override?.enabled === \"boolean\"\n\t\t\t? override.enabled\n\t\t\t: typeof globalConfig?.enabled === \"boolean\"\n\t\t\t\t? globalConfig.enabled\n\t\t\t\t: DEFAULT_COMPLETION_BATCH_CONFIG.enabled;\n\treturn {\n\t\tenabled,\n\t\tdebounceMs:\n\t\t\tparsePositiveInt(override?.debounceMs) ??\n\t\t\tparsePositiveInt(globalConfig?.debounceMs) ??\n\t\t\tDEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,\n\t\tmaxWaitMs:\n\t\t\tparsePositiveInt(override?.maxWaitMs) ??\n\t\t\tparsePositiveInt(globalConfig?.maxWaitMs) ??\n\t\t\tDEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,\n\t\tstragglerDebounceMs:\n\t\t\tparsePositiveInt(override?.stragglerDebounceMs) ??\n\t\t\tparsePositiveInt(globalConfig?.stragglerDebounceMs) ??\n\t\t\tDEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,\n\t\tstragglerMaxWaitMs:\n\t\t\tparsePositiveInt(override?.stragglerMaxWaitMs) ??\n\t\t\tparsePositiveInt(globalConfig?.stragglerMaxWaitMs) ??\n\t\t\tDEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,\n\t\tstragglerWindowMs:\n\t\t\tparsePositiveInt(override?.stragglerWindowMs) ??\n\t\t\tparsePositiveInt(globalConfig?.stragglerWindowMs) ??\n\t\t\tDEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,\n\t};\n}\n\ntype TimerHandle = unknown;\n\ninterface TimerApi {\n\tsetTimeout(handler: () => void, delayMs: number): TimerHandle;\n\tclearTimeout(handle: TimerHandle): void;\n}\n\nconst defaultTimers: TimerApi = {\n\tsetTimeout: (handler, delayMs) => setTimeout(handler, delayMs),\n\tclearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n};\n\nfunction unrefHandle(handle: TimerHandle): void {\n\tif (\n\t\thandle &&\n\t\ttypeof handle === \"object\" &&\n\t\t\"unref\" in handle &&\n\t\ttypeof (handle as { unref: unknown }).unref === \"function\"\n\t) {\n\t\t(handle as { unref: () => void }).unref();\n\t}\n}\n\nexport interface CompletionBatcherOptions<T> {\n\tconfig: ResolvedCompletionBatchConfig;\n\temit: (items: T[]) => void;\n\ttimers?: TimerApi;\n\tnow?: () => number;\n}\n\nexport interface CompletionBatcher<T> {\n\t/** Add a batchable item. Emits immediately when batching is disabled. */\n\tpush(item: T): void;\n\t/** Emit any held items immediately as a single group. */\n\tflush(): void;\n\t/** Clear timers and return items that were never emitted. */\n\tdispose(): T[];\n}\n\n/**\n * Create a completion batcher. The batcher is single-use per registration: it\n * holds at most one open group. `flush` forces emission; `dispose` tears down\n * timers for reload/shutdown without emitting.\n */\nexport function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {\n\tconst timers = options.timers ?? defaultTimers;\n\tconst now = options.now ?? Date.now;\n\tconst config = options.config;\n\n\tif (!config.enabled) {\n\t\treturn {\n\t\t\tpush(item: T) {\n\t\t\t\toptions.emit([item]);\n\t\t\t},\n\t\t\tflush() {},\n\t\t\tdispose() {\n\t\t\t\treturn [];\n\t\t\t},\n\t\t};\n\t}\n\n\tlet pending: T[] = [];\n\tlet debounceTimer: TimerHandle | null = null;\n\tlet maxWaitTimer: TimerHandle | null = null;\n\tlet straggler = false;\n\tlet lastEmitAt: number | null = null;\n\n\tconst clearTimers = () => {\n\t\tif (debounceTimer) {\n\t\t\ttimers.clearTimeout(debounceTimer);\n\t\t\tdebounceTimer = null;\n\t\t}\n\t\tif (maxWaitTimer) {\n\t\t\ttimers.clearTimeout(maxWaitTimer);\n\t\t\tmaxWaitTimer = null;\n\t\t}\n\t};\n\n\tconst emitGroup = () => {\n\t\tclearTimers();\n\t\tif (pending.length === 0) return;\n\t\tconst items = pending;\n\t\tpending = [];\n\t\tlastEmitAt = now();\n\t\toptions.emit(items);\n\t};\n\n\treturn {\n\t\tpush(item: T) {\n\t\t\tif (pending.length === 0) {\n\t\t\t\tstraggler = lastEmitAt !== null && now() - lastEmitAt < config.stragglerWindowMs;\n\t\t\t}\n\t\t\tpending.push(item);\n\n\t\t\tif (debounceTimer) timers.clearTimeout(debounceTimer);\n\t\t\tconst debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;\n\t\t\tdebounceTimer = timers.setTimeout(emitGroup, debounceDelay);\n\t\t\tunrefHandle(debounceTimer);\n\n\t\t\tif (!maxWaitTimer) {\n\t\t\t\tconst maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;\n\t\t\t\tmaxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);\n\t\t\t\tunrefHandle(maxWaitTimer);\n\t\t\t}\n\t\t},\n\t\tflush: emitGroup,\n\t\tdispose() {\n\t\t\tclearTimers();\n\t\t\tconst abandoned = pending;\n\t\t\tpending = [];\n\t\t\treturn abandoned;\n\t\t},\n\t};\n}\n"]}