Skip to content

Commit ce2ceb0

Browse files
committed
Refactor video history storage with namespaced keys and backward compatibility
This change improves LocalStorage organization by using namespaced keys for video details while maintaining full backward compatibility for existing clients. ## Changes: - Video details now stored as `view_history/<uuid>` instead of plain `<uuid>` - Maintains existing `view_history` array structure (stores UUIDs only) - Implements lazy migration strategy with automatic fallback to old format - All CRUD operations support both old and new storage formats - Added comprehensive cleanup for both formats during deletions ## Backward Compatibility: - Existing clients: Continue working seamlessly with gradual migration - New clients: Use improved storage format from start - No data loss: All existing view history preserved during transition - Migration happens transparently during normal app usage ## Technical Details: - Added fallback logic in queryFn to read from old format when new format unavailable - updateHistory checks old format if new format entry not found, then migrates - clearHistory and clearInstanceHistory clean up both storage formats - deleteVideoFromHistory removes entries from both formats - All migration code marked with TODO comments for easy removal in July 2025 ## Benefits: - Better LocalStorage organization with namespaced video data - Reduced storage key collisions - Easier debugging with structured key names - Gradual, seamless migration without user impact
1 parent c4e19f5 commit ce2ceb0

1 file changed

Lines changed: 71 additions & 10 deletions

File tree

OwnTube.tv/hooks/useViewHistory.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,41 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
3030
queryFn: async () => {
3131
const history: string[] | undefined = await readFromAsyncStorage(STORAGE.VIEW_HISTORY);
3232

33-
const entries = await multiGetFromAsyncStorage(history || []);
33+
// Try the new format first, fall back to the old format for backward compatibility
34+
const newFormatKeys = (history || []).map((uuid) => `view_history/${uuid}`);
3435

35-
return Object.fromEntries((entries || []).map(([key, value]) => [key, JSON.parse(value || "")]));
36+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
37+
const oldFormatKeys = history || [];
38+
39+
const newEntries = await multiGetFromAsyncStorage(newFormatKeys);
40+
41+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
42+
const oldEntries = await multiGetFromAsyncStorage(oldFormatKeys);
43+
44+
const result: ViewHistoryBase = {};
45+
46+
// Process new format entries
47+
newEntries?.forEach(([key, value]) => {
48+
if (value) {
49+
const uuid = key.replace("view_history/", "");
50+
result[uuid] = JSON.parse(value);
51+
}
52+
});
53+
54+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
55+
// Process old format entries (fallback) and migrate them
56+
for (const [uuid, value] of oldEntries || []) {
57+
if (value && !result[uuid]) {
58+
const entry = JSON.parse(value);
59+
result[uuid] = entry;
60+
61+
// Lazy migration: move old entry to new format
62+
await writeToAsyncStorage(`view_history/${uuid}`, entry);
63+
await deleteFromAsyncStorage([uuid]);
64+
}
65+
}
66+
67+
return result;
3668
},
3769
staleTime: 0,
3870
select: (data) =>
@@ -50,12 +82,23 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
5082
if (!history?.includes(data.uuid)) {
5183
const updatedHistory = [data.uuid, ...(history || [])];
5284
const staleEntries = updatedHistory.slice(maxItems);
53-
deleteFromAsyncStorage(staleEntries);
85+
await deleteFromAsyncStorage(staleEntries.map((uuid) => `view_history/${uuid}`));
5486

5587
await writeToAsyncStorage(STORAGE.VIEW_HISTORY, updatedHistory.slice(0, maxItems));
5688
}
5789

58-
const videoToUpdate = await readFromAsyncStorage(data.uuid);
90+
const videoKey = `view_history/${data.uuid}`;
91+
let videoToUpdate = await readFromAsyncStorage(videoKey);
92+
93+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
94+
// Fallback: check the old format if not found in the new format
95+
if (!videoToUpdate) {
96+
videoToUpdate = await readFromAsyncStorage(data.uuid);
97+
// If found in old format, clean it up after migration
98+
if (videoToUpdate) {
99+
await deleteFromAsyncStorage([data.uuid]);
100+
}
101+
}
59102

60103
const updatedVideoEntry = {
61104
...(videoToUpdate || {}),
@@ -64,7 +107,7 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
64107
timestamp: data.timestamp ?? 0,
65108
};
66109

67-
await writeToAsyncStorage(data.uuid, updatedVideoEntry);
110+
await writeToAsyncStorage(videoKey, updatedVideoEntry);
68111
},
69112
onSuccess: async () => {
70113
await queryClient.invalidateQueries({ queryKey: ["viewHistory"] });
@@ -75,7 +118,11 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
75118
mutationFn: async () => {
76119
const history: string[] = await readFromAsyncStorage(STORAGE.VIEW_HISTORY);
77120

78-
await deleteFromAsyncStorage(history);
121+
// Delete both new and old format entries
122+
await deleteFromAsyncStorage(history.map((uuid) => `view_history/${uuid}`));
123+
124+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
125+
await deleteFromAsyncStorage(history); // old format cleanup
79126
await deleteFromAsyncStorage([STORAGE.VIEW_HISTORY]);
80127
},
81128
onSuccess: () => {
@@ -90,16 +137,26 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
90137
const toKeep: string[] = [];
91138

92139
for (const uuid of history) {
93-
const entry: ViewHistoryEntry = await readFromAsyncStorage(uuid);
140+
let entry: ViewHistoryEntry = await readFromAsyncStorage(`view_history/${uuid}`);
94141

95-
if (entry.backend === backend) {
142+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
143+
// Fallback to the old format if it is not found in the new format
144+
if (!entry) {
145+
entry = await readFromAsyncStorage(uuid);
146+
}
147+
148+
if (entry?.backend === backend) {
96149
toDelete.push(uuid);
97150
} else {
98151
toKeep.push(uuid);
99152
}
100153
}
101154

102-
await deleteFromAsyncStorage(toDelete);
155+
// Delete both new and old format entries
156+
await deleteFromAsyncStorage(toDelete.map((uuid) => `view_history/${uuid}`));
157+
158+
// TODO(clean-up): Remove in July 2025 when all clients are migrated
159+
await deleteFromAsyncStorage(toDelete); // old format cleanup
103160
await writeToAsyncStorage(STORAGE.VIEW_HISTORY, toKeep);
104161
},
105162
onSuccess: () => {
@@ -120,7 +177,11 @@ export const useViewHistory = (queryArg?: { enabled?: boolean; maxItems?: number
120177
STORAGE.VIEW_HISTORY,
121178
history.filter((entry) => entry !== uuid),
122179
);
123-
await deleteFromAsyncStorage([uuid]);
180+
// Delete both new and old format entries
181+
await deleteFromAsyncStorage([
182+
`view_history/${uuid}`,
183+
uuid, // TODO(clean-up): Remove in July 2025 when all clients are migrated
184+
]);
124185
await queryClient.invalidateQueries({ queryKey: ["viewHistory"] });
125186
};
126187

0 commit comments

Comments
 (0)