Skip to content

Commit b87c717

Browse files
committed
scope observability state by source to prevent flicker
1 parent b05ea85 commit b87c717

4 files changed

Lines changed: 173 additions & 93 deletions

File tree

src/runtime.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,13 @@ const program = Effect.gen(function* () {
190190
console.log(
191191
`[Runtime] Calling storeActions.addSpan for ${spanOrEvent.name}`,
192192
);
193-
yield* storeActions.addSpan(spanOrEvent);
193+
yield* storeActions.addSpan(spanOrEvent, client.id);
194194
console.log(`[Runtime] Called storeActions.addSpan successfully`);
195195
} else if (spanOrEvent._tag === "SpanEvent") {
196196
console.log(
197197
`[Runtime] Calling storeActions.addSpanEvent for ${spanOrEvent.name}`,
198198
);
199-
yield* storeActions.addSpanEvent(spanOrEvent);
199+
yield* storeActions.addSpanEvent(spanOrEvent, client.id);
200200
console.log(
201201
`[Runtime] Called storeActions.addSpanEvent successfully`,
202202
);
@@ -213,7 +213,7 @@ const program = Effect.gen(function* () {
213213
Stream.fromQueue(metricsQueue),
214214
(snapshot: Domain.MetricsSnapshot) =>
215215
Effect.gen(function* () {
216-
yield* storeActions.updateMetrics(snapshot);
216+
yield* storeActions.updateMetrics(snapshot, client.id);
217217
}),
218218
).pipe(Effect.fork);
219219

src/store.tsx

Lines changed: 137 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,12 @@ export function StoreProvider(props: ParentProps) {
183183
`[Store] StoreProvider: Initializing new store instance at ${new Date().toISOString()}`,
184184
);
185185
const [store, setStore] = createStore<StoreState>({
186+
serverSpans: [],
187+
serverMetrics: [],
186188
spans: [],
187189
metrics: [],
190+
spansByClient: {},
191+
metricsByClient: {},
188192
clients: [],
189193
activeClient: Option.none(),
190194
serverStatus: "starting",
@@ -232,51 +236,49 @@ export function StoreProvider(props: ParentProps) {
232236
setStore("debugCounter", (c) => c + 1);
233237
}, 1000);
234238

235-
// Span buffer for batching updates
236-
let spanBuffer: SimpleSpan[] = [];
237-
let spanUpdateBuffer: Map<string, SimpleSpan> = new Map();
238-
let eventBuffer: Map<string, SimpleSpanEvent[]> = new Map(); // Buffer events by spanId
239-
240-
const flushSpans = () => {
241-
if (spanBuffer.length > 0 || spanUpdateBuffer.size > 0) {
242-
const newSpans = [...spanBuffer];
243-
const updates = new Map(spanUpdateBuffer);
244-
spanBuffer = [];
245-
spanUpdateBuffer = new Map();
246-
247-
// Use direct setStore path-based updates for better reactivity
248-
// First, apply updates to existing spans
249-
for (const [spanId, updatedSpan] of updates) {
250-
const idx = store.spans.findIndex((s) => s.spanId === spanId);
251-
if (idx >= 0) {
252-
setStore("spans", idx, updatedSpan);
253-
}
254-
}
239+
let eventBuffer: Map<string, SimpleSpanEvent[]> = new Map();
255240

256-
// Add new spans
257-
if (newSpans.length > 0) {
258-
const currentSpans = [...store.spans];
241+
const sourceKey = (clientId?: number) =>
242+
`${clientId === undefined ? "server" : clientId}`;
259243

260-
// Apply buffered events to new spans before adding them
261-
for (const span of newSpans) {
262-
const bufferedEvents = eventBuffer.get(span.spanId);
263-
if (bufferedEvents && bufferedEvents.length > 0) {
264-
span.events = bufferedEvents;
265-
eventBuffer.delete(span.spanId);
266-
}
267-
}
244+
const isVisibleSource = (clientId?: number) =>
245+
store.activeClient._tag === "Some"
246+
? clientId === store.activeClient.value.id
247+
: clientId === undefined;
268248

269-
const allSpans = [...currentSpans, ...newSpans];
249+
const getSourceSpans = (clientId?: number) =>
250+
clientId === undefined
251+
? store.serverSpans
252+
: (store.spansByClient[clientId] ?? []);
270253

271-
// No automatic pruning - let spans accumulate
272-
// Users can press 'c' to clear spans manually when needed
273-
setStore("spans", allSpans);
274-
}
254+
const setSourceSpans = (nextSpans: SimpleSpan[], clientId?: number) => {
255+
if (clientId === undefined) {
256+
setStore("serverSpans", nextSpans);
257+
} else {
258+
setStore("spansByClient", clientId, nextSpans);
259+
}
260+
261+
if (isVisibleSource(clientId)) {
262+
setStore("spans", nextSpans);
275263
}
276264
};
277265

278-
// Flush spans periodically
279-
setInterval(flushSpans, 100);
266+
const getSourceMetrics = (clientId?: number) =>
267+
clientId === undefined
268+
? store.serverMetrics
269+
: (store.metricsByClient[clientId] ?? []);
270+
271+
const setSourceMetrics = (nextMetrics: SimpleMetric[], clientId?: number) => {
272+
if (clientId === undefined) {
273+
setStore("serverMetrics", nextMetrics);
274+
} else {
275+
setStore("metricsByClient", clientId, nextMetrics);
276+
}
277+
278+
if (isVisibleSource(clientId)) {
279+
setStore("metrics", nextMetrics);
280+
}
281+
};
280282

281283
// Helper types for navigation
282284
type NavigableItem = { type: "span"; span: SimpleSpan };
@@ -348,51 +350,78 @@ export function StoreProvider(props: ParentProps) {
348350
};
349351

350352
const actions: StoreActions = {
351-
addSpan: (span: Domain.Span) => {
353+
addSpan: (span: Domain.Span, clientId?: number) => {
352354
const simple = simplifySpan(span);
353-
// Check if span already exists
354-
const existing = store.spans.find((s) => s.spanId === simple.spanId);
355-
console.log(
356-
`[Store] addSpan: ${simple.name}, store has ${store.spans.length} spans, existing=${!!existing}`,
357-
);
358-
if (existing) {
359-
spanUpdateBuffer.set(simple.spanId, simple);
355+
const spans = [...getSourceSpans(clientId)];
356+
const idx = spans.findIndex((s) => s.spanId === simple.spanId);
357+
358+
const bufferedEvents = eventBuffer.get(`${sourceKey(clientId)}:${simple.spanId}`);
359+
if (bufferedEvents && bufferedEvents.length > 0) {
360+
simple.events = bufferedEvents;
361+
eventBuffer.delete(`${sourceKey(clientId)}:${simple.spanId}`);
362+
}
363+
364+
if (idx >= 0) {
365+
spans[idx] = simple;
360366
} else {
361-
spanBuffer.push(simple);
367+
spans.push(simple);
362368
}
369+
370+
setSourceSpans(spans, clientId);
363371
},
364372

365-
updateSpan: (span: Domain.Span) => {
366-
spanUpdateBuffer.set(span.spanId, simplifySpan(span));
373+
updateSpan: (span: Domain.Span, clientId?: number) => {
374+
const simple = simplifySpan(span);
375+
const spans = [...getSourceSpans(clientId)];
376+
const idx = spans.findIndex((s) => s.spanId === simple.spanId);
377+
378+
if (idx >= 0) {
379+
spans[idx] = simple;
380+
} else {
381+
spans.push(simple);
382+
}
383+
384+
setSourceSpans(spans, clientId);
367385
},
368386

369-
addSpanEvent: (event: Domain.SpanEvent) => {
387+
addSpanEvent: (event: Domain.SpanEvent, clientId?: number) => {
370388
const spanId = event.spanId;
371389
const simpleEvent = simplifySpanEvent(event);
390+
const spans = [...getSourceSpans(clientId)];
372391

373-
// Try to find the span in the store
374-
const idx = store.spans.findIndex((s) => s.spanId === spanId);
392+
const idx = spans.findIndex((s) => s.spanId === spanId);
375393

376394
if (idx >= 0) {
377-
// Span exists, add event directly
378-
console.log(
379-
`[Store] addSpanEvent: Adding event "${simpleEvent.name}" to span ${spanId.substring(0, 8)}`,
380-
);
381-
setStore("spans", idx, "events", (events) => [...events, simpleEvent]);
395+
spans[idx] = {
396+
...spans[idx],
397+
events: [...spans[idx].events, simpleEvent],
398+
};
399+
setSourceSpans(spans, clientId);
382400
} else {
383-
// Span doesn't exist yet, buffer the event
384-
console.log(
385-
`[Store] addSpanEvent: Buffering event "${simpleEvent.name}" for span ${spanId.substring(0, 8)}`,
386-
);
387-
const buffered = eventBuffer.get(spanId) || [];
401+
const key = `${sourceKey(clientId)}:${spanId}`;
402+
const buffered = eventBuffer.get(key) || [];
388403
buffered.push(simpleEvent);
389-
eventBuffer.set(spanId, buffered);
404+
eventBuffer.set(key, buffered);
390405
}
391406
},
392407

393408
clearSpans: () => {
394-
spanBuffer = [];
395-
spanUpdateBuffer.clear();
409+
const sourcePrefix =
410+
store.activeClient._tag === "Some"
411+
? `${sourceKey(store.activeClient.value.id)}:`
412+
: `${sourceKey(undefined)}:`;
413+
for (const key of eventBuffer.keys()) {
414+
if (key.startsWith(sourcePrefix)) {
415+
eventBuffer.delete(key);
416+
}
417+
}
418+
419+
if (store.activeClient._tag === "Some") {
420+
setStore("spansByClient", store.activeClient.value.id, []);
421+
} else {
422+
setStore("serverSpans", []);
423+
}
424+
396425
batch(() => {
397426
setStore("spans", []);
398427
setStore("ui", "selectedSpanId", null);
@@ -444,14 +473,18 @@ export function StoreProvider(props: ParentProps) {
444473
);
445474
},
446475

447-
updateMetrics: (snapshot: Domain.MetricsSnapshot) => {
476+
updateMetrics: (snapshot: Domain.MetricsSnapshot, clientId?: number) => {
448477
const metrics = (snapshot.metrics as Domain.Metric[]).map(simplifyMetric);
449-
batch(() => {
450-
setStore("metrics", metrics);
451-
});
478+
setSourceMetrics(metrics, clientId);
452479
},
453480

454481
clearMetrics: () => {
482+
if (store.activeClient._tag === "Some") {
483+
setStore("metricsByClient", store.activeClient.value.id, []);
484+
} else {
485+
setStore("serverMetrics", []);
486+
}
487+
455488
batch(() => {
456489
setStore("metrics", []);
457490
setStore("ui", "selectedMetricName", null);
@@ -464,13 +497,45 @@ export function StoreProvider(props: ParentProps) {
464497

465498
setClientsFromHashSet: (newClients: HashSet.HashSet<Client>) => {
466499
batch(() => {
467-
setStore("clients", Array.from(newClients));
500+
const nextClients = Array.from(newClients);
501+
setStore("clients", nextClients);
502+
503+
const nextIds = new Set(nextClients.map((client) => client.id));
504+
505+
setStore(
506+
"spansByClient",
507+
produce((draft) => {
508+
for (const clientIdStr of Object.keys(draft)) {
509+
if (!nextIds.has(Number(clientIdStr))) {
510+
delete draft[Number(clientIdStr)];
511+
}
512+
}
513+
}),
514+
);
515+
516+
setStore(
517+
"metricsByClient",
518+
produce((draft) => {
519+
for (const clientIdStr of Object.keys(draft)) {
520+
if (!nextIds.has(Number(clientIdStr))) {
521+
delete draft[Number(clientIdStr)];
522+
}
523+
}
524+
}),
525+
);
468526
});
469527
},
470528

471529
setActiveClient: (client: Option.Option<Client>) => {
472530
batch(() => {
473531
setStore("activeClient", client);
532+
if (client._tag === "Some") {
533+
setStore("spans", store.spansByClient[client.value.id] ?? []);
534+
setStore("metrics", store.metricsByClient[client.value.id] ?? []);
535+
} else {
536+
setStore("spans", store.serverSpans);
537+
setStore("metrics", store.serverMetrics);
538+
}
474539
});
475540
},
476541

@@ -485,6 +550,8 @@ export function StoreProvider(props: ParentProps) {
485550
if (client) {
486551
setStore("ui", "selectedClientIndex", index);
487552
setStore("activeClient", Option.some(client));
553+
setStore("spans", store.spansByClient[client.id] ?? []);
554+
setStore("metrics", store.metricsByClient[client.id] ?? []);
488555
console.log(
489556
`[Store] selectClientByIndex: Selected client ${index}: ${client.name}`,
490557
);

src/storeActionsService.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,22 @@ import type {
2424
*/
2525
export interface StoreActionsService {
2626
// Span actions
27-
readonly addSpan: (span: unknown) => Effect.Effect<void>;
28-
readonly updateSpan: (span: unknown) => Effect.Effect<void>;
29-
readonly addSpanEvent: (event: unknown) => Effect.Effect<void>;
27+
readonly addSpan: (span: unknown, clientId?: number) => Effect.Effect<void>;
28+
readonly updateSpan: (
29+
span: unknown,
30+
clientId?: number,
31+
) => Effect.Effect<void>;
32+
readonly addSpanEvent: (
33+
event: unknown,
34+
clientId?: number,
35+
) => Effect.Effect<void>;
3036
readonly clearSpans: () => Effect.Effect<void>;
3137

3238
// Metric actions
33-
readonly updateMetrics: (snapshot: unknown) => Effect.Effect<void>;
39+
readonly updateMetrics: (
40+
snapshot: unknown,
41+
clientId?: number,
42+
) => Effect.Effect<void>;
3443
readonly clearMetrics: () => Effect.Effect<void>;
3544

3645
// Client actions
@@ -80,26 +89,26 @@ export const makeStoreActionsLayer = (
8089
actions: StoreActions,
8190
): Layer.Layer<StoreActionsService> =>
8291
Layer.succeed(StoreActionsService, {
83-
addSpan: (span: unknown) =>
92+
addSpan: (span: unknown, clientId?: number) =>
8493
Effect.sync(() => {
85-
actions.addSpan(span as any);
94+
actions.addSpan(span as any, clientId);
8695
}),
87-
updateSpan: (span: unknown) =>
96+
updateSpan: (span: unknown, clientId?: number) =>
8897
Effect.sync(() => {
89-
actions.updateSpan(span as any);
98+
actions.updateSpan(span as any, clientId);
9099
}),
91-
addSpanEvent: (event: unknown) =>
100+
addSpanEvent: (event: unknown, clientId?: number) =>
92101
Effect.sync(() => {
93-
actions.addSpanEvent(event as any);
102+
actions.addSpanEvent(event as any, clientId);
94103
}),
95104
clearSpans: () =>
96105
Effect.sync(() => {
97106
actions.clearSpans();
98107
}),
99108

100-
updateMetrics: (snapshot: unknown) =>
109+
updateMetrics: (snapshot: unknown, clientId?: number) =>
101110
Effect.sync(() => {
102-
actions.updateMetrics(snapshot as any);
111+
actions.updateMetrics(snapshot as any, clientId);
103112
}),
104113
clearMetrics: () =>
105114
Effect.sync(() => {
@@ -188,19 +197,19 @@ export const makeMockStoreActionsLayer = Effect.gen(function* () {
188197
});
189198

190199
const service: StoreActionsService = {
191-
addSpan: (span: unknown) =>
200+
addSpan: (span: unknown, _clientId?: number) =>
192201
Ref.update(stateRef, (s) => ({ ...s, spans: [...s.spans, span] })),
193-
updateSpan: (span: unknown) =>
202+
updateSpan: (span: unknown, _clientId?: number) =>
194203
Ref.update(stateRef, (s) => ({
195204
...s,
196205
spans: s.spans.map((existing: any) =>
197206
existing.spanId === (span as any).spanId ? span : existing,
198207
),
199208
})),
200-
addSpanEvent: () => Effect.void, // Simplified for mock
209+
addSpanEvent: (_event: unknown, _clientId?: number) => Effect.void, // Simplified for mock
201210
clearSpans: () => Ref.update(stateRef, (s) => ({ ...s, spans: [] })),
202211

203-
updateMetrics: (snapshot: unknown) =>
212+
updateMetrics: (snapshot: unknown, _clientId?: number) =>
204213
Ref.update(stateRef, (s) => ({
205214
...s,
206215
metrics: (snapshot as any).metrics || [],

0 commit comments

Comments
 (0)