codexmeter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,595 @@
1
+ import { readThreads } from './sqlite-reader.js';
2
+ import { randomUUID } from 'crypto';
3
+ import {
4
+ normalizeCwd, deriveRepoKey, deriveRepoLabel,
5
+ classifyAgentFamily, isSubagent, normalizeModelName,
6
+ } from './normalize.js';
7
+ import { initPricing, priceSession } from './cost-catalog.js';
8
+ import { buildAggregates, buildSessionView } from './aggregator.js';
9
+ import { createDayKeyFormatter } from './day-key.js';
10
+ import { createLiveAggregateState, createEmptyLivePatch, applySessionToLiveState, buildLiveBootstrap, buildLivePatch } from './live-state.js';
11
+ import { createRolloutWorkerPool } from './rollout-worker-pool.js';
12
+
13
+ const LIVE_FRAME_INTERVAL_MS = 50;
14
+ const LIVE_DAYS_PER_SECOND = 6;
15
+ const LIVE_DAY_CADENCE_MS = Math.round(1000 / LIVE_DAYS_PER_SECOND);
16
+ const LIVE_DAY_KEYS_PER_EMIT = 1;
17
+ const LIVE_SURFACE_CADENCE_MS = {
18
+ overview: LIVE_FRAME_INTERVAL_MS * 2,
19
+ rankings: LIVE_FRAME_INTERVAL_MS * 3,
20
+ daily: LIVE_DAY_CADENCE_MS,
21
+ heatmap: LIVE_DAY_CADENCE_MS,
22
+ };
23
+
24
+ export function createIngestState() {
25
+ return {
26
+ ingest_id: randomUUID(),
27
+ run_token: 0,
28
+ phase: 'idle',
29
+ total_threads: 0,
30
+ inventoried: 0,
31
+ needs_enrichment: 0,
32
+ enriched: 0,
33
+ current_date_bucket: null,
34
+ percent: 0,
35
+ complete: false,
36
+ error: null,
37
+ sessions: [],
38
+ aggregates: null,
39
+ generated_at: null,
40
+ presentation_complete_pending: false,
41
+ live_state: null,
42
+ live_seq: 0,
43
+ live_subscribers: new Set(),
44
+ live_pump_timer: null,
45
+ live_pending_patch: createEmptyLivePatch(),
46
+ live_progress_dirty: false,
47
+ live_last_emit_at: 0,
48
+ live_last_surface_emit_at: { overview: 0, rankings: 0, daily: 0, heatmap: 0 },
49
+ };
50
+ }
51
+
52
+ export async function runIngest(codexHome, state, opts = {}) {
53
+ const tz = opts.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
54
+ const toDayKey = createDayKeyFormatter(tz);
55
+ const runToken = state.run_token;
56
+ const isCurrentRun = () => state.run_token === runToken;
57
+ const workerPool = createRolloutWorkerPool({ size: opts.workerThreads });
58
+
59
+ try {
60
+ state.live_state = createLiveAggregateState(tz);
61
+ state.phase = 'inventory';
62
+ state.percent = 0;
63
+ queueLiveProgress(state);
64
+ broadcastBootstrap(state);
65
+
66
+ const threads = readThreads(codexHome, ({ total, read }) => {
67
+ if (!isCurrentRun()) return;
68
+ state.total_threads = total;
69
+ state.inventoried = read;
70
+ state.percent = total > 0 ? (read / total) * 0.08 : 0;
71
+ queueLiveProgress(state);
72
+ });
73
+ if (!isCurrentRun()) return;
74
+
75
+ state.inventoried = threads.length;
76
+ state.total_threads = threads.length;
77
+ state.percent = 0.08;
78
+ state.phase = 'normalizing';
79
+ queueLiveProgress(state);
80
+
81
+ await initPricing();
82
+ if (!isCurrentRun()) return;
83
+
84
+ const sessions = [];
85
+ for (const t of threads) {
86
+ const nc = normalizeCwd(t.cwd_raw);
87
+ sessions.push({
88
+ thread_id: t.thread_id,
89
+ rollout_path: t.rollout_path,
90
+ cwd_raw: t.cwd_raw,
91
+ repo_key: deriveRepoKey(nc),
92
+ repo_label: deriveRepoLabel(nc),
93
+ started_at: t.created_at,
94
+ ended_at: t.updated_at,
95
+ elapsed_seconds: null,
96
+ tokens_used: t.tokens_used,
97
+ model_provider: t.model_provider,
98
+ model_name: null,
99
+ reasoning_effort: null,
100
+ usage_total: null,
101
+ active_by_day: null,
102
+ agent_role: t.agent_role,
103
+ agent_nickname: t.agent_nickname,
104
+ agent_family: classifyAgentFamily(t.agent_role),
105
+ is_subagent: isSubagent(t.agent_role),
106
+ parent_thread_id: null,
107
+ cost: null,
108
+ cost_source: 'unavailable',
109
+ materialized: !t.rollout_path,
110
+ title: t.title,
111
+ cli_version: t.cli_version,
112
+ });
113
+ }
114
+
115
+ state.phase = 'enrichment';
116
+ queueLiveProgress(state);
117
+
118
+ const bootstrapSessions = sessions.filter((session) => !session.rollout_path);
119
+ if (bootstrapSessions.length) {
120
+ assignRootThreadIds(sessions);
121
+ const bootstrapPatch = createEmptyLivePatch();
122
+ for (const session of bootstrapSessions) {
123
+ finalizeSessionMetrics(session, toDayKey);
124
+ applySessionToLiveState(state.live_state, session, bootstrapPatch);
125
+ }
126
+ queueLivePatch(state, bootstrapPatch);
127
+ }
128
+
129
+ const candidates = sessions
130
+ .filter(s => s.rollout_path)
131
+ .sort((a, b) => (a.started_at || 0) - (b.started_at || 0));
132
+
133
+ state.needs_enrichment = candidates.length;
134
+ state.percent = candidates.length > 0 ? 0.08 : 0.90;
135
+ const BATCH_SIZE = opts.batchSize || 100;
136
+ const ROOT_REFRESH_EVERY = opts.rootRefreshEvery || 1000;
137
+ let lastRootRefreshCount = 0;
138
+
139
+ for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
140
+ const batch = candidates.slice(i, i + BATCH_SIZE);
141
+
142
+ if (batch[0]?.started_at) {
143
+ const d = new Date(batch[0].started_at * 1000);
144
+ state.current_date_bucket = d.toLocaleDateString('en-CA', { timeZone: tz });
145
+ }
146
+
147
+ const results = await workerPool.mapRollouts(batch.map((session) => session.rollout_path), tz);
148
+ if (!isCurrentRun()) return;
149
+
150
+ const workerError = results.find((result) => result?.ok === false);
151
+ if (workerError) {
152
+ throw new Error(`Rollout worker failure: ${workerError.error}`);
153
+ }
154
+
155
+ for (let j = 0; j < batch.length; j++) {
156
+ const s = batch[j];
157
+ const data = results[j]?.data || null;
158
+ if (data) {
159
+ if (data.model_name) s.model_name = normalizeModelName(data.model_name);
160
+ if (data.reasoning_effort) s.reasoning_effort = data.reasoning_effort;
161
+ if (data.parent_thread_id) s.parent_thread_id = data.parent_thread_id;
162
+ if (data.usage_total) s.usage_total = data.usage_total;
163
+ if (data.active_seconds && data.active_seconds > 0) {
164
+ s.elapsed_seconds = data.active_seconds;
165
+ s.active_by_day = data.active_by_day || null;
166
+ }
167
+ }
168
+ finalizeSessionMetrics(s, toDayKey);
169
+ s.materialized = true;
170
+ }
171
+
172
+ const shouldRefreshRoots =
173
+ state.enriched === 0 ||
174
+ (state.enriched - lastRootRefreshCount) >= ROOT_REFRESH_EVERY;
175
+
176
+ if (shouldRefreshRoots) {
177
+ assignRootThreadIds(sessions);
178
+ lastRootRefreshCount = state.enriched;
179
+ }
180
+ const livePatch = createEmptyLivePatch();
181
+ for (const session of batch) {
182
+ applySessionToLiveState(state.live_state, session, livePatch);
183
+ }
184
+ queueLivePatch(state, livePatch);
185
+
186
+ state.enriched = Math.min(i + BATCH_SIZE, candidates.length);
187
+ state.percent = candidates.length > 0
188
+ ? 0.08 + (state.enriched / candidates.length) * 0.82
189
+ : 0.90;
190
+ queueLiveProgress(state);
191
+
192
+ if (!isCurrentRun()) return;
193
+ }
194
+
195
+ for (const s of sessions) {
196
+ finalizeSessionMetrics(s, toDayKey);
197
+ }
198
+
199
+ assignRootThreadIds(sessions);
200
+
201
+ state.phase = 'aggregation';
202
+ state.percent = state.needs_enrichment > 0 ? 0.95 : 0.90;
203
+ queueLiveProgress(state);
204
+
205
+ rebuildAggregates(sessions, state, opts, tz);
206
+ if (!isCurrentRun()) return;
207
+ state.percent = 0.99;
208
+ state.phase = 'finalizing';
209
+ state.complete = false;
210
+ state.presentation_complete_pending = true;
211
+ state.current_date_bucket = null;
212
+ queueLiveProgress(state);
213
+ finalizeWithoutSubscribers(state);
214
+
215
+ } catch (err) {
216
+ if (!isCurrentRun()) return;
217
+ state.error = err.message;
218
+ state.phase = 'error';
219
+ console.error('Ingest error:', err);
220
+ flushLive(state, 'ingest-error');
221
+ } finally {
222
+ await workerPool.close();
223
+ }
224
+ }
225
+
226
+ export function restartIngest(codexHome, state, opts = {}) {
227
+ if (state.live_pump_timer) {
228
+ clearInterval(state.live_pump_timer);
229
+ state.live_pump_timer = null;
230
+ }
231
+
232
+ state.run_token += 1;
233
+ state.ingest_id = randomUUID();
234
+ state.phase = 'idle';
235
+ state.total_threads = 0;
236
+ state.inventoried = 0;
237
+ state.needs_enrichment = 0;
238
+ state.enriched = 0;
239
+ state.current_date_bucket = null;
240
+ state.percent = 0;
241
+ state.complete = false;
242
+ state.error = null;
243
+ state.sessions = [];
244
+ state.aggregates = null;
245
+ state.generated_at = null;
246
+ state.presentation_complete_pending = false;
247
+ state.live_state = null;
248
+ state.live_seq = 0;
249
+ state.live_pending_patch = createEmptyLivePatch();
250
+ state.live_progress_dirty = false;
251
+ state.live_last_emit_at = 0;
252
+ state.live_last_surface_emit_at = { overview: 0, rankings: 0, daily: 0, heatmap: 0 };
253
+
254
+ return runIngest(codexHome, state, opts);
255
+ }
256
+
257
+ function rebuildAggregates(sessions, state, opts, tz, mode = {}) {
258
+ const source = mode.partial ? sessions.filter(session => session.materialized) : sessions;
259
+ const filtered = applyFilters(source, opts);
260
+ const sessionView = buildSessionView(filtered, source);
261
+ state.sessions = sessionView;
262
+ state.aggregates = buildAggregates(filtered, tz, sessionView);
263
+ state.generated_at = new Date().toISOString();
264
+ }
265
+
266
+ function applyFilters(sessions, opts) {
267
+ let result = sessions;
268
+ if (opts.from) {
269
+ const fromTs = new Date(opts.from + 'T00:00:00').getTime() / 1000;
270
+ result = result.filter(s => s.ended_at >= fromTs);
271
+ }
272
+ if (opts.to) {
273
+ const toDate = new Date(opts.to + 'T00:00:00');
274
+ toDate.setDate(toDate.getDate() + 1);
275
+ const toTs = toDate.getTime() / 1000;
276
+ result = result.filter(s => s.started_at < toTs);
277
+ }
278
+ if (opts.repo) {
279
+ const sub = opts.repo.toLowerCase();
280
+ result = result.filter(s => s.repo_label.toLowerCase().includes(sub));
281
+ }
282
+ if (opts.agentFamily) {
283
+ result = result.filter(s => s.agent_family === opts.agentFamily);
284
+ }
285
+ return result;
286
+ }
287
+
288
+ function assignRootThreadIds(sessions) {
289
+ const byId = new Map(sessions.map(session => [session.thread_id, session]));
290
+ const memo = new Map();
291
+
292
+ const resolveRoot = (session) => {
293
+ if (!session) return null;
294
+ if (memo.has(session.thread_id)) return memo.get(session.thread_id);
295
+
296
+ const trail = [];
297
+ const seen = new Set();
298
+ let current = session;
299
+ let rootId = session.thread_id;
300
+
301
+ while (current) {
302
+ trail.push(current.thread_id);
303
+ const parentId = current.parent_thread_id;
304
+
305
+ if (!parentId || parentId === current.thread_id || seen.has(parentId)) {
306
+ rootId = current.thread_id;
307
+ break;
308
+ }
309
+
310
+ if (memo.has(parentId)) {
311
+ rootId = memo.get(parentId);
312
+ break;
313
+ }
314
+
315
+ seen.add(parentId);
316
+ const parent = byId.get(parentId);
317
+ if (!parent) {
318
+ rootId = parentId;
319
+ break;
320
+ }
321
+ current = parent;
322
+ }
323
+
324
+ for (const threadId of trail) memo.set(threadId, rootId);
325
+ return rootId;
326
+ };
327
+
328
+ for (const session of sessions) {
329
+ session.root_thread_id = resolveRoot(session) || session.thread_id;
330
+ }
331
+ }
332
+
333
+ function finalizeSessionMetrics(session, toDayKey) {
334
+ if (session.elapsed_seconds === null) {
335
+ const fallback = (session.ended_at || 0) - (session.started_at || 0);
336
+ if (fallback > 0 && fallback < 3600) {
337
+ session.elapsed_seconds = fallback;
338
+ if (session.started_at) {
339
+ const dayKey = toDayKey(session.started_at * 1000);
340
+ session.active_by_day = { [dayKey]: fallback };
341
+ }
342
+ }
343
+ }
344
+
345
+ if (session.cost === null) {
346
+ const priced = priceSession(session.model_name, {
347
+ totalTokens: session.tokens_used,
348
+ usageBuckets: session.usage_total,
349
+ });
350
+ session.cost = priced.cost;
351
+ session.cost_source = priced.source;
352
+ }
353
+ }
354
+
355
+ function queueLiveProgress(state) {
356
+ state.live_progress_dirty = true;
357
+ ensureLivePump(state);
358
+ }
359
+
360
+ function queueLivePatch(state, patch) {
361
+ mergePatchInto(state.live_pending_patch, patch);
362
+ state.live_progress_dirty = true;
363
+ ensureLivePump(state);
364
+ }
365
+
366
+ function ensureLivePump(state) {
367
+ if (state.live_pump_timer || !state.live_subscribers.size) return;
368
+ state.live_pump_timer = setInterval(() => flushLive(state), LIVE_FRAME_INTERVAL_MS);
369
+ }
370
+
371
+ function stopLivePumpIfIdle(state) {
372
+ if (!state.live_pump_timer) return;
373
+ if (state.live_subscribers.size) return;
374
+ clearInterval(state.live_pump_timer);
375
+ state.live_pump_timer = null;
376
+ finalizeWithoutSubscribers(state);
377
+ }
378
+
379
+ function finalizeWithoutSubscribers(state) {
380
+ if (state.live_subscribers.size) return;
381
+ if (!state.presentation_complete_pending) return;
382
+ state.phase = 'complete';
383
+ state.percent = 1;
384
+ state.complete = true;
385
+ state.presentation_complete_pending = false;
386
+ state.live_progress_dirty = false;
387
+ state.live_pending_patch = createEmptyLivePatch();
388
+ }
389
+
390
+ function flushLive(state, forcedEvent = null) {
391
+ if (!state.live_subscribers.size) {
392
+ state.live_pending_patch = createEmptyLivePatch();
393
+ state.live_progress_dirty = false;
394
+ stopLivePumpIfIdle(state);
395
+ return;
396
+ }
397
+
398
+ if (!forcedEvent) {
399
+ const now = Date.now();
400
+ const earliestNextEmitAt = state.live_last_emit_at + LIVE_FRAME_INTERVAL_MS;
401
+ if (now < earliestNextEmitAt) {
402
+ return;
403
+ }
404
+ }
405
+
406
+ const flushablePatch = forcedEvent ? takeAllPendingPatch(state) : takeFlushablePatch(state);
407
+ const patchEmpty = isPatchEmpty(flushablePatch);
408
+ const pendingPatchEmpty = isPatchEmpty(state.live_pending_patch);
409
+ const shouldEmitComplete = !forcedEvent && patchEmpty && pendingPatchEmpty && state.presentation_complete_pending;
410
+ const event = forcedEvent || (shouldEmitComplete ? 'complete' : (!patchEmpty ? 'patch' : 'progress'));
411
+
412
+ if (shouldEmitComplete) {
413
+ state.phase = 'complete';
414
+ state.percent = 1;
415
+ state.complete = true;
416
+ state.presentation_complete_pending = false;
417
+ }
418
+
419
+ const payload = {
420
+ ingest_id: state.ingest_id,
421
+ seq: ++state.live_seq,
422
+ progress: progressPayload(state),
423
+ };
424
+
425
+ if (event === 'patch') {
426
+ payload.data = buildLivePatch(state.live_state, flushablePatch);
427
+ } else if (event === 'bootstrap') {
428
+ payload.data = buildLiveBootstrap(state.live_state);
429
+ }
430
+
431
+ broadcastLive(state, event, payload);
432
+ state.live_last_emit_at = Date.now();
433
+ state.live_progress_dirty = shouldEmitComplete ? false : (event === 'progress' ? false : state.live_progress_dirty);
434
+ if (forcedEvent) {
435
+ state.live_pending_patch = createEmptyLivePatch();
436
+ }
437
+ }
438
+
439
+ function broadcastLive(state, event, payload) {
440
+ const data = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
441
+ for (const res of state.live_subscribers) {
442
+ try {
443
+ res.write(data);
444
+ } catch {
445
+ state.live_subscribers.delete(res);
446
+ }
447
+ }
448
+ }
449
+
450
+ function mergePatchInto(target, source) {
451
+ for (const key of source.overview) target.overview.add(key);
452
+ mergeRangePatchSets(target.repos, source.repos);
453
+ mergeRangePatchSets(target.models, source.models);
454
+ mergeRangePatchSets(target.families, source.families);
455
+ for (const key of source.daily) target.daily.add(key);
456
+ for (const key of source.heatmap) target.heatmap.add(key);
457
+ }
458
+
459
+ function mergeRangePatchSets(target, source) {
460
+ for (const rangeKey of ['total', 'd7', 'd30']) {
461
+ for (const key of source[rangeKey]) target[rangeKey].add(key);
462
+ }
463
+ }
464
+
465
+ function isPatchEmpty(patch) {
466
+ return patch.overview.size === 0 &&
467
+ patch.repos.total.size === 0 && patch.repos.d7.size === 0 && patch.repos.d30.size === 0 &&
468
+ patch.models.total.size === 0 && patch.models.d7.size === 0 && patch.models.d30.size === 0 &&
469
+ patch.families.total.size === 0 && patch.families.d7.size === 0 && patch.families.d30.size === 0 &&
470
+ patch.daily.size === 0 &&
471
+ patch.heatmap.size === 0;
472
+ }
473
+
474
+ function takeFlushablePatch(state) {
475
+ const now = Date.now();
476
+ const sent = createEmptyLivePatch();
477
+
478
+ if (state.live_pending_patch.overview.size > 0 && readyForSurface(state, 'overview', now)) {
479
+ moveSet(state.live_pending_patch.overview, sent.overview);
480
+ state.live_last_surface_emit_at.overview = now;
481
+ }
482
+
483
+ const rankingsDirty =
484
+ state.live_pending_patch.repos.total.size > 0 || state.live_pending_patch.repos.d7.size > 0 || state.live_pending_patch.repos.d30.size > 0 ||
485
+ state.live_pending_patch.models.total.size > 0 || state.live_pending_patch.models.d7.size > 0 || state.live_pending_patch.models.d30.size > 0 ||
486
+ state.live_pending_patch.families.total.size > 0 || state.live_pending_patch.families.d7.size > 0 || state.live_pending_patch.families.d30.size > 0;
487
+
488
+ if (rankingsDirty && readyForSurface(state, 'rankings', now)) {
489
+ moveRangeSets(state.live_pending_patch.repos, sent.repos);
490
+ moveRangeSets(state.live_pending_patch.models, sent.models);
491
+ moveRangeSets(state.live_pending_patch.families, sent.families);
492
+ state.live_last_surface_emit_at.rankings = now;
493
+ }
494
+
495
+ const dayDirty = state.live_pending_patch.daily.size > 0 || state.live_pending_patch.heatmap.size > 0;
496
+ if (dayDirty && readyForSurface(state, 'daily', now)) {
497
+ const nextDayKeys = takeNextChronologicalDayKeys(
498
+ state.live_pending_patch.daily,
499
+ state.live_pending_patch.heatmap,
500
+ LIVE_DAY_KEYS_PER_EMIT
501
+ );
502
+ moveSpecificKeys(state.live_pending_patch.daily, sent.daily, nextDayKeys);
503
+ moveSpecificKeys(state.live_pending_patch.heatmap, sent.heatmap, nextDayKeys);
504
+ state.live_last_surface_emit_at.daily = now;
505
+ state.live_last_surface_emit_at.heatmap = now;
506
+ }
507
+
508
+ return sent;
509
+ }
510
+
511
+ function readyForSurface(state, surfaceKey, now) {
512
+ return (now - state.live_last_surface_emit_at[surfaceKey]) >= LIVE_SURFACE_CADENCE_MS[surfaceKey];
513
+ }
514
+
515
+ function moveSet(from, to) {
516
+ for (const value of from) to.add(value);
517
+ from.clear();
518
+ }
519
+
520
+ function moveRangeSets(fromRanges, toRanges) {
521
+ for (const rangeKey of ['total', 'd7', 'd30']) {
522
+ moveSet(fromRanges[rangeKey], toRanges[rangeKey]);
523
+ }
524
+ }
525
+
526
+ function takeAllPendingPatch(state) {
527
+ const sent = createEmptyLivePatch();
528
+ moveSet(state.live_pending_patch.overview, sent.overview);
529
+ moveRangeSets(state.live_pending_patch.repos, sent.repos);
530
+ moveRangeSets(state.live_pending_patch.models, sent.models);
531
+ moveRangeSets(state.live_pending_patch.families, sent.families);
532
+ moveSet(state.live_pending_patch.daily, sent.daily);
533
+ moveSet(state.live_pending_patch.heatmap, sent.heatmap);
534
+ return sent;
535
+ }
536
+
537
+ function moveSpecificKeys(from, to, keys) {
538
+ for (const key of keys) {
539
+ if (!from.has(key)) continue;
540
+ from.delete(key);
541
+ to.add(key);
542
+ }
543
+ }
544
+
545
+ function takeNextChronologicalDayKeys(dailySet, heatmapSet, limit) {
546
+ const allKeys = new Set([...dailySet, ...heatmapSet]);
547
+ return [...allKeys]
548
+ .sort((a, b) => String(a).localeCompare(String(b)))
549
+ .slice(0, limit);
550
+ }
551
+
552
+ function progressPayload(state) {
553
+ return {
554
+ phase: state.phase,
555
+ total_threads: state.total_threads,
556
+ inventoried: state.inventoried,
557
+ needs_enrichment: state.needs_enrichment,
558
+ enriched: state.enriched,
559
+ current_date_bucket: state.current_date_bucket,
560
+ percent: state.percent,
561
+ complete: state.complete,
562
+ error: state.error,
563
+ generated_at: state.generated_at,
564
+ };
565
+ }
566
+
567
+ export function attachLiveSubscriber(state, res) {
568
+ state.live_subscribers.add(res);
569
+ ensureLivePump(state);
570
+ const payload = {
571
+ ingest_id: state.ingest_id,
572
+ seq: ++state.live_seq,
573
+ progress: progressPayload(state),
574
+ data: buildLiveBootstrap(state.live_state || createLiveAggregateState(Intl.DateTimeFormat().resolvedOptions().timeZone)),
575
+ };
576
+ res.write(`event: bootstrap\ndata: ${JSON.stringify(payload)}\n\n`);
577
+ }
578
+
579
+ export function detachLiveSubscriber(state, res) {
580
+ state.live_subscribers.delete(res);
581
+ stopLivePumpIfIdle(state);
582
+ }
583
+
584
+ function broadcastBootstrap(state) {
585
+ if (!state.live_subscribers.size) return;
586
+ const payload = {
587
+ ingest_id: state.ingest_id,
588
+ seq: ++state.live_seq,
589
+ progress: progressPayload(state),
590
+ data: buildLiveBootstrap(state.live_state || createLiveAggregateState(Intl.DateTimeFormat().resolvedOptions().timeZone)),
591
+ };
592
+ broadcastLive(state, 'bootstrap', payload);
593
+ }
594
+
595
+