Files
iptvnator/libs/services/src/lib/server-storage/server-migration-xtream.ts
T
4grayandClaude Opus 5.5 f49ad59549 fix(server-storage): per-tab saved writes, memory overflow cache, snapshot aliases
- Unconfirmed writes and early edits are kept in the tab's own
  sessionStorage, so tabs never clear each other's; a shared registry of
  unconfirmed entries (one record per tab, ignored after ten minutes)
  keeps a re-read in one tab from replacing another tab's values.
- A cache value too big for localStorage stays readable from memory, so
  later edits are diffed against what the app reads.
- Typing legacy Xtream ids keeps each snapshot's bare key as an alias, so
  a reference not yet rewritten still resolves and a retry can type it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:33:32 +02:00

66 lines
2.5 KiB
TypeScript

import type { ServerUserStateNamespace } from '@iptvnator/shared/interfaces';
/** One playlist's Xtream user data from one place (browser or server). */
export type XtreamUserData = Partial<Record<ServerUserStateNamespace, unknown>>;
const SNAPSHOTS = 'xtream-collection-items';
/**
* Older favorites and history name content by its bare Xtream id, which
* repeats across live, movie and series. Before two copies are combined,
* each one's bare ids become typed keys (`live:42`) using that copy's own
* collection snapshots, the same evidence the PWA reads them with; ids
* without a snapshot stay as they are. Snapshots gain the typed key and
* keep the bare one.
*/
export function typeLegacyXtreamRefs(data: XtreamUserData): XtreamUserData {
const snapshots = isRecord(data[SNAPSHOTS]) ? data[SNAPSHOTS] : {};
const typed = (key: unknown): unknown => {
// Bare ids were stored as numbers and, older still, numeric strings.
const id =
typeof key === 'number'
? key
: typeof key === 'string' && /^\d+$/.test(key)
? Number(key)
: null;
if (id === null) {
return key;
}
const snapshot = snapshots[String(id)];
return isRecord(snapshot) &&
snapshot['xtream_id'] === id &&
typeof snapshot['type'] === 'string'
? `${snapshot['type']}:${id}`
: key;
};
const result: XtreamUserData = { ...data };
const favorites = data['xtream-favorites'];
if (Array.isArray(favorites)) {
result['xtream-favorites'] = favorites.map(typed);
}
const recent = data['xtream-recent-items'];
if (Array.isArray(recent)) {
result['xtream-recent-items'] = recent.map((item) =>
isRecord(item) ? { ...item, id: typed(item['id']) } : item
);
}
if (isRecord(data[SNAPSHOTS])) {
// The bare key stays as an alias: a reference not yet rewritten (a
// failed write, an older client) still finds its snapshot, and a
// retry can type it again.
const aliased: Record<string, unknown> = { ...snapshots };
for (const [key, snapshot] of Object.entries(snapshots)) {
if (/^\d+$/.test(key)) {
aliased[String(typed(Number(key)))] = snapshot;
}
}
result[SNAPSHOTS] = aliased;
}
return result;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}