Skip to content

Commit b6b1861

Browse files
refactor(events): make grouped-event-buffer self-notify
Move bufferVersion ownership into the grouped-event-buffer and bump it from invalidateCache() — the single mutation choke point — coalescing many synchronous ingests into one notification via a microtask. - events.ts re-exports bufferVersion instead of owning it - workflow-run-layout drops all manual bufferVersion bumps; live-event reactivity now flows through appendLiveEvent -> invalidateCache - clearWorkflowData resets the buffer (clearing stale derived state) instead of poking the version counter - remove the now-dead onNewEvents hook from live-poll and its tests
1 parent df3067a commit b6b1861

6 files changed

Lines changed: 48 additions & 101 deletions

File tree

src/lib/components/lines-and-dots/workflow-details.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
: '',
8282
);
8383
let totalActions = $derived.by(() => {
84-
$bufferVersion;
84+
void $bufferVersion;
8585
return getEventArray()
8686
.reduce((acc, e) => (e?.billableActions ?? 0) + acc, 0)
8787
.toString();

src/lib/layouts/workflow-run-layout.svelte

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
let runId = $derived(page.params.run);
6161
let showJson = $derived(page.url.searchParams.has('json'));
6262
let fullJson = $derived.by(() => {
63-
$bufferVersion;
63+
void $bufferVersion;
6464
return { ...$workflowRun, eventHistory: getEventArray() };
6565
});
6666
@@ -160,7 +160,6 @@
160160
latestEventId = Math.max(latestEventId, parseInt(ev.eventId));
161161
return isNew;
162162
},
163-
onNewEvents: () => bufferVersion.update((v) => v + 1),
164163
}).then((lastToken) => {
165164
_lastPollToken = lastToken;
166165
});
@@ -243,7 +242,6 @@
243242
const id = parseInt(event.eventId);
244243
if (id > latestEventId) latestEventId = id;
245244
}
246-
if (events.length) bufferVersion.update((v) => v + 1);
247245
},
248246
})
249247
.then(() => {
@@ -252,7 +250,6 @@
252250
$workflowRun.workflow?.pendingNexusOperations ?? [],
253251
);
254252
fetchComplete = true;
255-
bufferVersion.update((v) => v + 1);
256253
})
257254
.catch((e: unknown) => {
258255
if (e instanceof Error && e.name !== 'AbortError') {
@@ -290,7 +287,7 @@
290287
};
291288
292289
$effect(() => {
293-
$bufferVersion;
290+
void $bufferVersion;
294291
untrack(() => {
295292
const events = getEventArray();
296293
$fullEventHistory = events;
@@ -301,7 +298,6 @@
301298
$timelineEvents = null;
302299
$workflowRun = initialWorkflowRun;
303300
$fullEventHistory = [];
304-
$bufferVersion = 0;
305301
workflowError = null;
306302
fetchComplete = false;
307303
latestEventId = 0;
@@ -312,13 +308,14 @@
312308
_lastPollToken = '';
313309
_pollPaused = false;
314310
abortAll();
311+
resetBuffer();
315312
resetLastDataEncoderSuccess();
316313
if (refreshInterval) clearInterval(refreshInterval);
317314
refreshInterval = null;
318315
};
319316
320317
$effect(() => {
321-
runId;
318+
void runId;
322319
untrack(() => {
323320
clearWorkflowData();
324321
});

src/lib/services/grouped-event-buffer.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { writable } from 'svelte/store';
2+
13
import {
24
addEventToGroup,
35
createEventGroup,
@@ -56,6 +58,27 @@ const processedWorkflowTaskIds = new Set<string>();
5658
let cachedGroups: EventGroup[] | null = null;
5759
let cachedGroupsNoWFT: EventGroup[] | null = null;
5860

61+
/**
62+
* Reactivity signal for the buffer. invalidateCache() — the single choke point
63+
* hit by every mutation (ingest, enrichGroups, reset) — schedules a coalesced
64+
* bump, so many synchronous ingests (a whole fetch page or live-poll response)
65+
* notify subscribers once. Svelte consumers read this in a derived/effect and
66+
* then call getEventArray() / getGroupArray().
67+
*/
68+
export const bufferVersion = writable(0);
69+
70+
let bumpScheduled = false;
71+
function scheduleBufferChange(): void {
72+
if (bumpScheduled) {
73+
return;
74+
}
75+
bumpScheduled = true;
76+
queueMicrotask(() => {
77+
bumpScheduled = false;
78+
bufferVersion.update((version) => version + 1);
79+
});
80+
}
81+
5982
function toWorkflowEvent(
6083
raw: HistoryEvent,
6184
isAscending: boolean,
@@ -79,6 +102,7 @@ function insertEventById(list: WorkflowEvent[], event: WorkflowEvent): void {
79102
function invalidateCache(): void {
80103
cachedGroups = null;
81104
cachedGroupsNoWFT = null;
105+
scheduleBufferChange();
82106
}
83107

84108
function isWorkflowTaskGroup(group: EventGroup): boolean {

src/lib/services/live-poll.test.ts

Lines changed: 8 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ function startPoll(
6666
) {
6767
const ctrl = new AbortController();
6868
const onEvent = vi.fn().mockReturnValue(onEventReturn);
69-
const onNewEvents = vi.fn();
7069

7170
let callIdx = 0;
7271
requestFromAPI.mockImplementation(() => {
@@ -81,11 +80,10 @@ function startPoll(
8180
startToken: 'start-tok',
8281
signal: ctrl.signal,
8382
onEvent,
84-
onNewEvents,
8583
...FAST,
8684
});
8785

88-
return { ctrl, onEvent, onNewEvents, done };
86+
return { ctrl, onEvent, done };
8987
}
9088

9189
// ---------------------------------------------------------------------------
@@ -106,7 +104,6 @@ describe('runLivePoll — waitNewEvent long-poll', () => {
106104
startToken: '',
107105
signal: ctrl.signal,
108106
onEvent: vi.fn().mockReturnValue(false),
109-
onNewEvents: vi.fn(),
110107
...FAST,
111108
});
112109

@@ -131,7 +128,6 @@ describe('runLivePoll — waitNewEvent long-poll', () => {
131128
startToken: '',
132129
signal: ctrl.signal,
133130
onEvent: vi.fn().mockReturnValue(false),
134-
onNewEvents: vi.fn(),
135131
...FAST,
136132
});
137133

@@ -156,7 +152,6 @@ describe('runLivePoll — waitNewEvent long-poll', () => {
156152
startToken: 'initial-cursor',
157153
signal: ctrl.signal,
158154
onEvent: vi.fn().mockReturnValue(false),
159-
onNewEvents: vi.fn(),
160155
...FAST,
161156
});
162157

@@ -179,7 +174,6 @@ describe('runLivePoll — waitNewEvent long-poll', () => {
179174
startToken: '',
180175
signal: ctrl.signal,
181176
onEvent: vi.fn().mockReturnValue(false),
182-
onNewEvents: vi.fn(),
183177
...FAST,
184178
});
185179

@@ -204,28 +198,6 @@ describe('runLivePoll — event delivery', () => {
204198
expect(onEvent).toHaveBeenNthCalledWith(2, makeRawEvent(2));
205199
expect(onEvent).toHaveBeenNthCalledWith(3, makeRawEvent(3));
206200
});
207-
208-
it('calls onNewEvents once when at least one event is new', async () => {
209-
const { onNewEvents, done } = startPoll([pollResponse([1, 2])], {
210-
onEventReturn: true,
211-
});
212-
await done;
213-
expect(onNewEvents).toHaveBeenCalledTimes(1);
214-
});
215-
216-
it('does NOT call onNewEvents when all events are duplicates', async () => {
217-
const { onNewEvents, done } = startPoll([pollResponse([1, 2])], {
218-
onEventReturn: false,
219-
});
220-
await done;
221-
expect(onNewEvents).not.toHaveBeenCalled();
222-
});
223-
224-
it('does NOT call onNewEvents when response has no events', async () => {
225-
const { onNewEvents, done } = startPoll([pollResponse([])]);
226-
await done;
227-
expect(onNewEvents).not.toHaveBeenCalled();
228-
});
229201
});
230202

231203
// ---------------------------------------------------------------------------
@@ -235,7 +207,6 @@ describe('runLivePoll — event delivery', () => {
235207
describe('runLivePoll — nextPageToken threading', () => {
236208
it('follows nextPageToken immediately without delay', async () => {
237209
const ctrl = new AbortController();
238-
const onNewEvents = vi.fn();
239210
let callIdx = 0;
240211
const responses = [
241212
pollResponse([1, 2], 'page-2-tok'),
@@ -253,7 +224,6 @@ describe('runLivePoll — nextPageToken threading', () => {
253224
startToken: '',
254225
signal: ctrl.signal,
255226
onEvent: vi.fn().mockReturnValue(true),
256-
onNewEvents,
257227
...FAST,
258228
});
259229

@@ -285,7 +255,6 @@ describe('runLivePoll — nextPageToken threading', () => {
285255
startToken: 'initial',
286256
signal: ctrl.signal,
287257
onEvent: vi.fn().mockReturnValue(true),
288-
onNewEvents: vi.fn(),
289258
...FAST,
290259
});
291260

@@ -297,31 +266,6 @@ describe('runLivePoll — nextPageToken threading', () => {
297266
expect.objectContaining({ token: undefined }),
298267
);
299268
});
300-
301-
it('calls onNewEvents per iteration, not once per event batch', async () => {
302-
// Two iterations, each delivering new events.
303-
const ctrl = new AbortController();
304-
let callIdx = 0;
305-
const responses = [pollResponse([1]), pollResponse([2])];
306-
requestFromAPI.mockImplementation(() => {
307-
const resp = responses[callIdx++];
308-
if (callIdx >= responses.length) ctrl.abort();
309-
return Promise.resolve(resp);
310-
});
311-
const onNewEvents = vi.fn();
312-
313-
await runLivePoll({
314-
route: '/api/events',
315-
runId: 'run-1',
316-
startToken: '',
317-
signal: ctrl.signal,
318-
onEvent: vi.fn().mockReturnValue(true),
319-
onNewEvents,
320-
...FAST,
321-
});
322-
323-
expect(onNewEvents).toHaveBeenCalledTimes(2);
324-
});
325269
});
326270

327271
// ---------------------------------------------------------------------------
@@ -348,7 +292,6 @@ describe('runLivePoll — back-off', () => {
348292
startToken: '',
349293
signal: ctrl.signal,
350294
onEvent: vi.fn().mockReturnValue(false),
351-
onNewEvents: vi.fn(),
352295
backoffMs: 0,
353296
errorBackoffMs: 0,
354297
});
@@ -357,7 +300,7 @@ describe('runLivePoll — back-off', () => {
357300
});
358301

359302
it('does NOT delay when response delivers new events without nextPageToken', async () => {
360-
// Abort inside onNewEvents so the loop exits right after processing events
303+
// Abort while processing the event so the loop exits right after the batch
361304
// but before any setTimeout could fire. If backoffMs=999 appears in delays
362305
// it means the code incorrectly backed off even though events were delivered.
363306
const delays: number[] = [];
@@ -372,16 +315,18 @@ describe('runLivePoll — back-off', () => {
372315
const ctrl = new AbortController();
373316
requestFromAPI.mockResolvedValueOnce(pollResponse([1])); // events delivered, no token
374317

375-
// Abort during onNewEvents — happens before the setTimeout check in the loop.
376-
const onNewEvents = vi.fn().mockImplementation(() => ctrl.abort());
318+
// Abort during event processing — happens before the setTimeout check in the loop.
319+
const onEvent = vi.fn().mockImplementation(() => {
320+
ctrl.abort();
321+
return true;
322+
});
377323

378324
await runLivePoll({
379325
route: '/api/events',
380326
runId: 'run-1',
381327
startToken: '',
382328
signal: ctrl.signal,
383-
onEvent: vi.fn().mockReturnValue(true),
384-
onNewEvents,
329+
onEvent,
385330
backoffMs: 999, // must NOT appear in delays
386331
errorBackoffMs: 0,
387332
});
@@ -414,7 +359,6 @@ describe('runLivePoll — back-off', () => {
414359
startToken: '',
415360
signal: ctrl.signal,
416361
onEvent: vi.fn().mockReturnValue(false),
417-
onNewEvents: vi.fn(),
418362
backoffMs: 1234, // distinctive value
419363
errorBackoffMs: 0,
420364
});
@@ -447,7 +391,6 @@ describe('runLivePoll — back-off', () => {
447391
startToken: '',
448392
signal: ctrl.signal,
449393
onEvent: vi.fn().mockReturnValue(false),
450-
onNewEvents: vi.fn(),
451394
backoffMs: 0,
452395
errorBackoffMs: 5678, // distinctive value
453396
});
@@ -484,7 +427,6 @@ describe('runLivePoll — back-off', () => {
484427
startToken: '',
485428
signal: ctrl.signal,
486429
onEvent: vi.fn().mockReturnValue(false),
487-
onNewEvents: vi.fn(),
488430
// No backoffMs / errorBackoffMs → use defaults
489431
});
490432

@@ -511,7 +453,6 @@ describe('runLivePoll — abort', () => {
511453
startToken: '',
512454
signal: ctrl.signal,
513455
onEvent: vi.fn(),
514-
onNewEvents: vi.fn(),
515456
...FAST,
516457
});
517458

@@ -531,7 +472,6 @@ describe('runLivePoll — abort', () => {
531472
startToken: '',
532473
signal: ctrl.signal,
533474
onEvent: vi.fn(),
534-
onNewEvents: vi.fn(),
535475
errorBackoffMs: 0,
536476
backoffMs: 0,
537477
});
@@ -561,7 +501,6 @@ describe('runLivePoll — pause/resume cursor', () => {
561501
startToken: '',
562502
signal: ctrl.signal,
563503
onEvent: vi.fn(),
564-
onNewEvents: vi.fn(),
565504
...FAST,
566505
});
567506

@@ -581,7 +520,6 @@ describe('runLivePoll — pause/resume cursor', () => {
581520
startToken: 'resume-tok',
582521
signal: ctrl.signal,
583522
onEvent: vi.fn(),
584-
onNewEvents: vi.fn(),
585523
...FAST,
586524
});
587525

@@ -607,7 +545,6 @@ describe('runLivePoll — pause/resume cursor', () => {
607545
startToken: '',
608546
signal: ctrl.signal,
609547
onEvent: vi.fn(),
610-
onNewEvents: vi.fn(),
611548
...FAST,
612549
});
613550

@@ -632,7 +569,6 @@ describe('runLivePoll — pause/resume cursor', () => {
632569
startToken: '',
633570
signal: ctrl1.signal,
634571
onEvent: vi.fn().mockReturnValue(true),
635-
onNewEvents: vi.fn(),
636572
...FAST,
637573
});
638574

@@ -650,7 +586,6 @@ describe('runLivePoll — pause/resume cursor', () => {
650586
startToken: resumeToken,
651587
signal: ctrl2.signal,
652588
onEvent: onEvent2,
653-
onNewEvents: vi.fn(),
654589
...FAST,
655590
});
656591

0 commit comments

Comments
 (0)