perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing cache, synchronization, and persistence correctness across runtimes, projects, directories, and worktrees. - prioritize selected and visible sessions during bootstrap and defer non-critical enrichment work - reduce redundant message loading, event processing, store publication, and hidden sidebar work - prevent stale session and message requests from overwriting newer authoritative state - preserve existing data when authoritative fetches fail instead of treating failures as successful empty responses - scope session materialization, messages, drafts, queues, todos, pins, permissions, folders, tabs, Git state, and pull request data by runtime and directory identity - harden runtime switching, reconnect, cleanup, mutation reconciliation, and persisted-state ordering - preserve live subagent Task linkage when metadata arrives after an older message request or while streaming parts are suspended - coalesce overlapping tail refreshes without losing newer refresh demand - improve cold-session loading by moving deferrable work out of the critical bootstrap path - isolate URL authentication, mobile credentials, native secrets, and other runtime-owned state across endpoint changes - bound long-lived caches and remove avoidable allocations from event and rendering hot paths - limit virtualization to archive collections where it improves rendering without disrupting active sidebar layout - stabilize session folders, pin ordering, expanded state, and persisted sidebar behavior - open skill files through the same secure editor and outside-workspace grant flow used by file navigation, including worktree sessions - expand regression coverage for stale completions, runtime collisions, reconnect behavior, persistence races, authoritative empty results, and subagent refresh ordering - document the updated synchronization, cache ownership, performance, and runtime-isolation invariants
This commit is contained in:
committed by
GitHub
parent
485efc7117
commit
85400459e9
@@ -1,105 +1,175 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
import { normalizePath } from '@/lib/pathNormalization';
|
||||
import { createDeferredSafeJSONStorage } from './utils/safeStorage';
|
||||
|
||||
export type InlineCommentSource = 'diff' | 'plan' | 'file' | 'preview-console' | 'preview-annotation' | 'terminal';
|
||||
|
||||
export type InlineCommentDraftTarget = {
|
||||
directory: string;
|
||||
sessionKey: string;
|
||||
};
|
||||
|
||||
export interface InlineCommentDraft {
|
||||
id: string;
|
||||
sessionKey: string; // sessionId or 'draft' for new sessions
|
||||
sessionKey: string;
|
||||
source: InlineCommentSource;
|
||||
fileLabel: string; // filename or 'plan'
|
||||
fileLabel: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
side?: 'original' | 'modified'; // diff only
|
||||
side?: 'original' | 'modified';
|
||||
code: string;
|
||||
language: string;
|
||||
text: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export const EMPTY_INLINE_COMMENT_DRAFTS: InlineCommentDraft[] = [];
|
||||
|
||||
interface InlineCommentDraftState {
|
||||
drafts: Record<string, InlineCommentDraft[]>; // sessionKey -> drafts
|
||||
drafts: Record<string, InlineCommentDraft[]>;
|
||||
touchedAt: Record<string, number>;
|
||||
}
|
||||
|
||||
interface InlineCommentDraftActions {
|
||||
addDraft: (draft: Omit<InlineCommentDraft, 'id' | 'createdAt'>) => void;
|
||||
updateDraft: (sessionKey: string, draftId: string, updates: Partial<Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>>) => void;
|
||||
removeDraft: (sessionKey: string, draftId: string) => void;
|
||||
clearDrafts: (sessionKey: string) => void;
|
||||
getDrafts: (sessionKey: string) => InlineCommentDraft[];
|
||||
consumeDrafts: (sessionKey: string) => InlineCommentDraft[];
|
||||
restoreDrafts: (sessionKey: string, drafts: InlineCommentDraft[]) => void;
|
||||
getDraftCount: (sessionKey: string) => number;
|
||||
hasDrafts: (sessionKey: string) => boolean;
|
||||
addDraft: (target: InlineCommentDraftTarget, draft: Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>) => string | null;
|
||||
updateDraft: (target: InlineCommentDraftTarget, draftId: string, updates: Partial<Omit<InlineCommentDraft, 'id' | 'createdAt' | 'sessionKey'>>) => void;
|
||||
removeDraft: (target: InlineCommentDraftTarget, draftId: string) => void;
|
||||
clearDrafts: (target: InlineCommentDraftTarget) => void;
|
||||
getDrafts: (target: InlineCommentDraftTarget) => InlineCommentDraft[];
|
||||
consumeDrafts: (target: InlineCommentDraftTarget) => InlineCommentDraft[];
|
||||
restoreDrafts: (target: InlineCommentDraftTarget, drafts: InlineCommentDraft[]) => void;
|
||||
getDraftCount: (target: InlineCommentDraftTarget) => number;
|
||||
hasDrafts: (target: InlineCommentDraftTarget) => boolean;
|
||||
clearSessionDrafts: (runtimeKey: string, directory: string, sessionId: string) => void;
|
||||
}
|
||||
|
||||
type InlineCommentDraftStore = InlineCommentDraftState & InlineCommentDraftActions;
|
||||
|
||||
const isValidSource = (value: unknown): value is InlineCommentSource =>
|
||||
value === 'diff' || value === 'plan' || value === 'file' || value === 'preview-console' || value === 'preview-annotation' || value === 'terminal';
|
||||
const MAX_SESSIONS = 50;
|
||||
const MAX_DRAFTS_PER_SESSION = 20;
|
||||
const MAX_PERSISTED_BYTES = 1024 * 1024;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const isValidSide = (value: unknown): value is 'original' | 'modified' =>
|
||||
value === 'original' || value === 'modified';
|
||||
|
||||
const toPositiveLine = (value: unknown): number | null => {
|
||||
const parsed = typeof value === 'number' ? value : Number(value);
|
||||
if (!Number.isFinite(parsed)) return null;
|
||||
return Math.max(1, Math.floor(parsed));
|
||||
type SerializedSizeIndex = {
|
||||
draftEntries: Map<string, number>;
|
||||
touchedEntries: Map<string, number>;
|
||||
total: number;
|
||||
};
|
||||
|
||||
const sanitizeDraft = (input: unknown): InlineCommentDraft | null => {
|
||||
if (!input || typeof input !== 'object') return null;
|
||||
const draft = input as Partial<InlineCommentDraft>;
|
||||
const serializedSizeByDrafts = new WeakMap<Record<string, InlineCommentDraft[]>, SerializedSizeIndex>();
|
||||
const EMPTY_ENVELOPE_BYTES = encoder.encode(JSON.stringify({ drafts: {}, touchedAt: {} })).byteLength;
|
||||
|
||||
if (typeof draft.sessionKey !== 'string' || draft.sessionKey.trim().length === 0) return null;
|
||||
if (!isValidSource(draft.source)) return null;
|
||||
|
||||
const startLine = toPositiveLine(draft.startLine);
|
||||
const endLine = toPositiveLine(draft.endLine);
|
||||
if (!startLine || !endLine) return null;
|
||||
|
||||
const id = typeof draft.id === 'string' && draft.id.trim().length > 0
|
||||
? draft.id
|
||||
: `icd-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
const code = typeof draft.code === 'string' ? draft.code : '';
|
||||
if (draft.source === 'terminal' && !code.trim()) return null;
|
||||
return {
|
||||
id,
|
||||
sessionKey: draft.sessionKey,
|
||||
source: draft.source,
|
||||
fileLabel: typeof draft.fileLabel === 'string' ? draft.fileLabel : 'unknown',
|
||||
startLine,
|
||||
endLine,
|
||||
side: isValidSide(draft.side) ? draft.side : undefined,
|
||||
code,
|
||||
language: typeof draft.language === 'string' ? draft.language : 'text',
|
||||
text: typeof draft.text === 'string' ? draft.text : '',
|
||||
createdAt: Number.isFinite(draft.createdAt) ? Number(draft.createdAt) : Date.now(),
|
||||
};
|
||||
export const getInlineCommentDraftKey = (runtimeKey: string, directory: string, sessionKey: string): string | null => {
|
||||
const normalizedDirectory = normalizePath(directory);
|
||||
if (!runtimeKey || !normalizedDirectory || !sessionKey) return null;
|
||||
return JSON.stringify([runtimeKey, normalizedDirectory, sessionKey]);
|
||||
};
|
||||
|
||||
const sanitizeDraftMap = (input: unknown): Record<string, InlineCommentDraft[]> => {
|
||||
if (!input || typeof input !== 'object') return {};
|
||||
const getCurrentKey = (target: InlineCommentDraftTarget): string | null =>
|
||||
getInlineCommentDraftKey(getRuntimeKey(), target.directory, target.sessionKey);
|
||||
|
||||
const entries = Object.entries(input as Record<string, unknown>);
|
||||
const result: Record<string, InlineCommentDraft[]> = {};
|
||||
const serializedEntryBytes = (key: string, value: unknown): number =>
|
||||
encoder.encode(`${JSON.stringify(key)}:${JSON.stringify(value)}`).byteLength;
|
||||
|
||||
for (const [sessionKey, sessionDrafts] of entries) {
|
||||
if (!Array.isArray(sessionDrafts)) continue;
|
||||
const sumEntryBytes = (entries: Map<string, number>): number => {
|
||||
let total = 0;
|
||||
for (const bytes of entries.values()) total += bytes;
|
||||
return total;
|
||||
};
|
||||
|
||||
const sanitized = sessionDrafts
|
||||
.map(sanitizeDraft)
|
||||
.filter((draft): draft is InlineCommentDraft => Boolean(draft))
|
||||
.filter((draft) => draft.sessionKey === sessionKey);
|
||||
const indexedTotal = (draftEntries: Map<string, number>, touchedEntries: Map<string, number>): number => (
|
||||
EMPTY_ENVELOPE_BYTES
|
||||
+ sumEntryBytes(draftEntries)
|
||||
+ Math.max(0, draftEntries.size - 1)
|
||||
+ sumEntryBytes(touchedEntries)
|
||||
+ Math.max(0, touchedEntries.size - 1)
|
||||
);
|
||||
|
||||
if (sanitized.length > 0) {
|
||||
result[sessionKey] = sanitized;
|
||||
const buildSerializedSizeIndex = (
|
||||
drafts: Record<string, InlineCommentDraft[]>,
|
||||
touchedAt: Record<string, number>,
|
||||
): SerializedSizeIndex => {
|
||||
const draftEntries = new Map<string, number>();
|
||||
const touchedEntries = new Map<string, number>();
|
||||
for (const [key, value] of Object.entries(drafts)) draftEntries.set(key, serializedEntryBytes(key, value));
|
||||
for (const [key, value] of Object.entries(touchedAt)) touchedEntries.set(key, serializedEntryBytes(key, value));
|
||||
const index = { draftEntries, touchedEntries, total: indexedTotal(draftEntries, touchedEntries) };
|
||||
serializedSizeByDrafts.set(drafts, index);
|
||||
return index;
|
||||
};
|
||||
|
||||
const updateSerializedSizeIndex = (
|
||||
previous: InlineCommentDraftState,
|
||||
drafts: Record<string, InlineCommentDraft[]>,
|
||||
touchedAt: Record<string, number>,
|
||||
changedKey: string,
|
||||
): SerializedSizeIndex => {
|
||||
const previousIndex = serializedSizeByDrafts.get(previous.drafts)
|
||||
?? buildSerializedSizeIndex(previous.drafts, previous.touchedAt);
|
||||
const draftEntries = new Map(previousIndex.draftEntries);
|
||||
const touchedEntries = new Map(previousIndex.touchedEntries);
|
||||
const bucket = drafts[changedKey];
|
||||
if (bucket) draftEntries.set(changedKey, serializedEntryBytes(changedKey, bucket));
|
||||
else draftEntries.delete(changedKey);
|
||||
const touched = touchedAt[changedKey];
|
||||
if (typeof touched === 'number') touchedEntries.set(changedKey, serializedEntryBytes(changedKey, touched));
|
||||
else touchedEntries.delete(changedKey);
|
||||
return { draftEntries, touchedEntries, total: indexedTotal(draftEntries, touchedEntries) };
|
||||
};
|
||||
|
||||
const boundState = (
|
||||
previous: InlineCommentDraftState,
|
||||
drafts: Record<string, InlineCommentDraft[]>,
|
||||
touchedAt: Record<string, number>,
|
||||
changedKey: string,
|
||||
): { drafts: Record<string, InlineCommentDraft[]>; touchedAt: Record<string, number> } | null => {
|
||||
const keys = Object.keys(drafts).sort((left, right) => (touchedAt[right] ?? 0) - (touchedAt[left] ?? 0));
|
||||
const retainedKeys = keys.slice(0, MAX_SESSIONS);
|
||||
const retainedDrafts: Record<string, InlineCommentDraft[]> = {};
|
||||
const retainedTouchedAt: Record<string, number> = {};
|
||||
for (const key of retainedKeys) {
|
||||
retainedDrafts[key] = drafts[key].length > MAX_DRAFTS_PER_SESSION
|
||||
? drafts[key].slice(-MAX_DRAFTS_PER_SESSION)
|
||||
: drafts[key];
|
||||
retainedTouchedAt[key] = touchedAt[key] ?? Date.now();
|
||||
}
|
||||
const sizeIndex = updateSerializedSizeIndex(previous, retainedDrafts, retainedTouchedAt, changedKey);
|
||||
for (const key of keys) {
|
||||
if (!(key in retainedDrafts)) {
|
||||
sizeIndex.draftEntries.delete(key);
|
||||
sizeIndex.touchedEntries.delete(key);
|
||||
} else if (retainedDrafts[key] !== drafts[key]) {
|
||||
sizeIndex.draftEntries.set(key, serializedEntryBytes(key, retainedDrafts[key]));
|
||||
}
|
||||
if (key !== changedKey && retainedTouchedAt[key] !== previous.touchedAt[key]) {
|
||||
sizeIndex.touchedEntries.set(key, serializedEntryBytes(key, retainedTouchedAt[key]));
|
||||
}
|
||||
}
|
||||
sizeIndex.total = indexedTotal(sizeIndex.draftEntries, sizeIndex.touchedEntries);
|
||||
const evictionKeys = [...retainedKeys];
|
||||
while (evictionKeys.length > 0 && sizeIndex.total > MAX_PERSISTED_BYTES) {
|
||||
const oldest = evictionKeys.pop()!;
|
||||
delete retainedDrafts[oldest];
|
||||
delete retainedTouchedAt[oldest];
|
||||
sizeIndex.draftEntries.delete(oldest);
|
||||
sizeIndex.touchedEntries.delete(oldest);
|
||||
sizeIndex.total = indexedTotal(sizeIndex.draftEntries, sizeIndex.touchedEntries);
|
||||
}
|
||||
if (sizeIndex.draftEntries.size === 0 && keys.length > 0) return null;
|
||||
serializedSizeByDrafts.set(retainedDrafts, sizeIndex);
|
||||
return { drafts: retainedDrafts, touchedAt: retainedTouchedAt };
|
||||
};
|
||||
|
||||
return result;
|
||||
const removeDraftKey = (state: InlineCommentDraftState, key: string): InlineCommentDraftState => {
|
||||
if (!(key in state.drafts)) return state;
|
||||
|
||||
const drafts = { ...state.drafts };
|
||||
const touchedAt = { ...state.touchedAt };
|
||||
delete drafts[key];
|
||||
delete touchedAt[key];
|
||||
return { drafts, touchedAt };
|
||||
};
|
||||
|
||||
export const useInlineCommentDraftStore = create<InlineCommentDraftStore>()(
|
||||
@@ -107,143 +177,115 @@ export const useInlineCommentDraftStore = create<InlineCommentDraftStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
drafts: {},
|
||||
|
||||
addDraft: (draft) => {
|
||||
if (draft.source === 'terminal' && !draft.code.trim()) return;
|
||||
touchedAt: {},
|
||||
addDraft: (target, draft) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key || (draft.source === 'terminal' && !draft.code.trim())) return null;
|
||||
const id = `icd-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const newDraft: InlineCommentDraft = {
|
||||
...draft,
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
const nextDraft: InlineCommentDraft = { ...draft, sessionKey: target.sessionKey, id, createdAt: Date.now() };
|
||||
let accepted = false;
|
||||
set((state) => {
|
||||
const currentDrafts = state.drafts[draft.sessionKey] ?? [];
|
||||
if (draft.source === 'terminal' && currentDrafts.some((current) => current.source === 'terminal' && current.fileLabel === draft.fileLabel && current.startLine === draft.startLine && current.endLine === draft.endLine && current.code === draft.code)) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[draft.sessionKey]: [...currentDrafts, newDraft],
|
||||
},
|
||||
};
|
||||
const current = state.drafts[key] ?? [];
|
||||
const isDuplicateTerminalDraft = draft.source === 'terminal' && current.some((item) => (
|
||||
item.source === 'terminal'
|
||||
&& item.fileLabel === draft.fileLabel
|
||||
&& item.startLine === draft.startLine
|
||||
&& item.endLine === draft.endLine
|
||||
&& item.code === draft.code
|
||||
));
|
||||
if (isDuplicateTerminalDraft) return state;
|
||||
|
||||
const bounded = boundState(
|
||||
state,
|
||||
{ ...state.drafts, [key]: [...current, nextDraft] },
|
||||
{ ...state.touchedAt, [key]: Date.now() },
|
||||
key,
|
||||
);
|
||||
if (!bounded || !bounded.drafts[key]?.some((item) => item.id === id)) return state;
|
||||
accepted = true;
|
||||
return bounded;
|
||||
});
|
||||
|
||||
return id;
|
||||
return accepted ? id : null;
|
||||
},
|
||||
|
||||
updateDraft: (sessionKey, draftId, updates) => {
|
||||
updateDraft: (target, draftId, updates) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const currentDrafts = state.drafts[sessionKey] ?? [];
|
||||
const newDrafts = currentDrafts.map((draft) => {
|
||||
if (draft.id !== draftId) {
|
||||
return draft;
|
||||
}
|
||||
return {
|
||||
...draft,
|
||||
...updates,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[sessionKey]: newDrafts,
|
||||
},
|
||||
};
|
||||
const current = state.drafts[key] ?? [];
|
||||
if (!current.some((draft) => draft.id === draftId)) return state;
|
||||
const bounded = boundState(
|
||||
state,
|
||||
{ ...state.drafts, [key]: current.map((draft) => draft.id === draftId ? { ...draft, ...updates } : draft) },
|
||||
{ ...state.touchedAt, [key]: Date.now() },
|
||||
key,
|
||||
);
|
||||
return bounded ?? state;
|
||||
});
|
||||
},
|
||||
|
||||
removeDraft: (sessionKey, draftId) => {
|
||||
removeDraft: (target, draftId) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key) return;
|
||||
set((state) => {
|
||||
const currentDrafts = state.drafts[sessionKey] ?? [];
|
||||
const newDrafts = currentDrafts.filter((d) => d.id !== draftId);
|
||||
const current = state.drafts[key] ?? [];
|
||||
const remaining = current.filter((draft) => draft.id !== draftId);
|
||||
if (remaining.length === current.length) return state;
|
||||
if (remaining.length === 0) return removeDraftKey(state, key);
|
||||
|
||||
if (newDrafts.length === 0) {
|
||||
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
||||
void _removed;
|
||||
return { drafts: rest };
|
||||
}
|
||||
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[sessionKey]: newDrafts,
|
||||
},
|
||||
};
|
||||
const drafts = { ...state.drafts };
|
||||
const touchedAt = { ...state.touchedAt };
|
||||
drafts[key] = remaining;
|
||||
touchedAt[key] = Date.now();
|
||||
return { drafts, touchedAt };
|
||||
});
|
||||
},
|
||||
|
||||
clearDrafts: (sessionKey) => {
|
||||
set((state) => {
|
||||
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
||||
void _removed;
|
||||
return { drafts: rest };
|
||||
});
|
||||
clearDrafts: (target) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key) return;
|
||||
set((state) => removeDraftKey(state, key));
|
||||
},
|
||||
|
||||
getDrafts: (sessionKey) => {
|
||||
return get().drafts[sessionKey] ?? [];
|
||||
getDrafts: (target) => {
|
||||
const key = getCurrentKey(target);
|
||||
return key ? get().drafts[key] ?? EMPTY_INLINE_COMMENT_DRAFTS : EMPTY_INLINE_COMMENT_DRAFTS;
|
||||
},
|
||||
|
||||
consumeDrafts: (sessionKey) => {
|
||||
const drafts = get().drafts[sessionKey] ?? [];
|
||||
if (drafts.length === 0) return [];
|
||||
|
||||
// Sort by creation time to maintain order
|
||||
const sortedDrafts = [...drafts].sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
// Clear drafts after consuming
|
||||
set((state) => {
|
||||
const { [sessionKey]: _removed, ...rest } = state.drafts;
|
||||
void _removed;
|
||||
return { drafts: rest };
|
||||
});
|
||||
|
||||
return sortedDrafts;
|
||||
consumeDrafts: (target) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key) return [];
|
||||
const drafts = [...(get().drafts[key] ?? [])].sort((left, right) => left.createdAt - right.createdAt);
|
||||
if (drafts.length > 0) set((state) => removeDraftKey(state, key));
|
||||
return drafts;
|
||||
},
|
||||
|
||||
restoreDrafts: (sessionKey, drafts) => {
|
||||
if (drafts.length === 0) return;
|
||||
restoreDrafts: (target, draftsToRestore) => {
|
||||
const key = getCurrentKey(target);
|
||||
if (!key || draftsToRestore.length === 0) return;
|
||||
set((state) => {
|
||||
const current = state.drafts[sessionKey] ?? [];
|
||||
const current = state.drafts[key] ?? [];
|
||||
const currentIds = new Set(current.map((draft) => draft.id));
|
||||
const restored = drafts.filter((draft) => draft.sessionKey === sessionKey && !currentIds.has(draft.id));
|
||||
const restored = draftsToRestore.filter((draft) => draft.sessionKey === target.sessionKey && !currentIds.has(draft.id));
|
||||
if (restored.length === 0) return state;
|
||||
return {
|
||||
drafts: {
|
||||
...state.drafts,
|
||||
[sessionKey]: [...restored, ...current].sort((a, b) => a.createdAt - b.createdAt),
|
||||
},
|
||||
};
|
||||
return boundState(
|
||||
state,
|
||||
{ ...state.drafts, [key]: [...restored, ...current].sort((left, right) => left.createdAt - right.createdAt) },
|
||||
{ ...state.touchedAt, [key]: Date.now() },
|
||||
key,
|
||||
) ?? state;
|
||||
});
|
||||
},
|
||||
|
||||
getDraftCount: (sessionKey) => {
|
||||
return (get().drafts[sessionKey] ?? []).length;
|
||||
},
|
||||
|
||||
hasDrafts: (sessionKey) => {
|
||||
return (get().drafts[sessionKey] ?? []).length > 0;
|
||||
getDraftCount: (target) => get().getDrafts(target).length,
|
||||
hasDrafts: (target) => get().getDrafts(target).length > 0,
|
||||
clearSessionDrafts: (runtimeKey, directory, sessionId) => {
|
||||
const key = getInlineCommentDraftKey(runtimeKey, directory, sessionId);
|
||||
if (!key) return;
|
||||
set((state) => removeDraftKey(state, key));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'openchamber-inline-comment-drafts',
|
||||
storage: createDeferredSafeJSONStorage(),
|
||||
version: 1,
|
||||
migrate: (persistedState: unknown) => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return { drafts: {} };
|
||||
}
|
||||
|
||||
const state = persistedState as { drafts?: unknown };
|
||||
return {
|
||||
drafts: sanitizeDraftMap(state.drafts),
|
||||
};
|
||||
},
|
||||
}
|
||||
version: 2,
|
||||
partialize: (state) => ({ drafts: state.drafts, touchedAt: state.touchedAt }),
|
||||
migrate: () => ({ drafts: {}, touchedAt: {} }),
|
||||
},
|
||||
),
|
||||
{ name: 'inline-comment-draft-store' }
|
||||
)
|
||||
{ name: 'inline-comment-draft-store' },
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user