Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,25 @@ describe('applyBatch', () => {
expect(result.success).toBe(true);
});

it('handles class instances (non-plain objects) as oldData', () => {
class ListResult {
result: any[];
success: boolean;
totalCount: number;
constructor() {
this.result = [makeItem('pod-1')];
this.success = true;
this.totalCount = 1;
}
}
const classData = new ListResult();
const events: ResourceEvent[] = [
{ type: 'ADD', payload: { data: makeItem('pod-2'), key: 'k', connection: 'c', id: 'pod-2', namespace: 'default' } },
];
const result = applyBatch(classData, events, idAccessor);
expect(result.result).toHaveLength(2);
});

it('supports nested id accessor paths', () => {
const deepAccessor = 'spec.id';
const data = {
Expand Down
59 changes: 30 additions & 29 deletions packages/omniviewdev-runtime/src/hooks/resource/useEventBatcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
import type { QueryClient, QueryKey } from '@tanstack/react-query';
import { produce } from 'immer';
import get from 'lodash.get';
import type { WatchState } from '../../types/watch';

Expand Down Expand Up @@ -37,46 +36,48 @@ export type ResourceEvent =
/**
* Apply a batch of resource events to a list query cache entry.
* Extracted for testability — this is the core batching logic.
*
* Returns a new plain object (immutable-style) without relying on Immer,
* avoiding issues with Wails binding class instances that Immer cannot draft.
*/
export function applyBatch(
oldData: any,
events: ResourceEvent[],
idAccessor: string,
): any {
const base = oldData ?? { result: [], success: true, totalCount: 0 };
return produce(base, (draft: any) => {
for (const event of events) {
switch (event.type) {
case 'ADD': {
const idx = draft.result.findIndex(
(item: any) => get(event.payload.data, idAccessor) === get(item, idAccessor),
);
if (idx === -1) {
draft.result.push(event.payload.data);
}
break;
// Shallow-copy the result array so we never mutate the cached version.
const result = [...(base.result ?? [])];

for (const event of events) {
const eventId = get(event.payload.data, idAccessor);

switch (event.type) {
case 'ADD': {
const idx = result.findIndex((item: any) => get(item, idAccessor) === eventId);
if (idx === -1) {
result.push(event.payload.data);
}
case 'UPDATE': {
const idx = draft.result.findIndex(
(item: any) => get(event.payload.data, idAccessor) === get(item, idAccessor),
);
if (idx !== -1) {
draft.result[idx] = event.payload.data;
}
break;
break;
}
case 'UPDATE': {
const idx = result.findIndex((item: any) => get(item, idAccessor) === eventId);
if (idx !== -1) {
result[idx] = event.payload.data;
}
case 'DELETE': {
const idx = draft.result.findIndex(
(item: any) => get(event.payload.data, idAccessor) === get(item, idAccessor),
);
if (idx !== -1) {
draft.result.splice(idx, 1);
}
break;
break;
}
case 'DELETE': {
const idx = result.findIndex((item: any) => get(item, idAccessor) === eventId);
if (idx !== -1) {
result.splice(idx, 1);
}
break;
}
}
});
}

return { ...base, result };
}

/**
Expand Down
31 changes: 14 additions & 17 deletions packages/omniviewdev-runtime/src/hooks/resource/useWatchState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useRef } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { produce } from 'immer';

import { GetWatchState } from '../../bindings/github.com/omniviewdev/omniview/backend/pkg/plugin/resource/servicewrapper';
import { GetWatchState} from '../../bindings/github.com/omniviewdev/omniview/backend/pkg/plugin/resource/servicewrapper';
import { Events } from '@wailsio/runtime';
import { useResolvedPluginId } from '../useResolvedPluginId';
import type {
Expand Down Expand Up @@ -43,20 +41,19 @@ export const useWatchState = ({
const applyEvent = useCallback((event: WatchStateEvent) => {
queryClient.setQueryData<WatchConnectionSummary>(queryKey, (old) => {
if (!old) return old;
return produce(old, (draft) => {
draft.resources[event.resourceKey] = event.state;
draft.resourceCounts[event.resourceKey] = event.resourceCount;

// Recompute aggregates
let synced = 0;
let errors = 0;
for (const state of Object.values(draft.resources)) {
if (state === WatchState.WatchStateSynced) synced++;
if (state === WatchState.WatchStateError || state === WatchState.WatchStateFailed) errors++;
}
draft.syncedCount = synced;
draft.errorCount = errors;
});

const resources = { ...old.resources, [event.resourceKey]: event.state };
const resourceCounts = { ...old.resourceCounts, [event.resourceKey]: event.resourceCount };

// Recompute aggregates
let synced = 0;
let errors = 0;
for (const state of Object.values(resources)) {
if (state === WatchState.WatchStateSynced) synced++;
if (state === WatchState.WatchStateError || state === WatchState.WatchStateFailed) errors++;
}

return { ...old, resources, resourceCounts, syncedCount: synced, errorCount: errors };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryClient, pluginID, connectionID]);
Expand Down
Loading