Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

102 lines
3.8 KiB
TypeScript

/**
* SlideContent schema versioning. Slide-surface PRs will iterate the
* on-disk shape; this module is the single chokepoint for normalizing
* any incoming SlideContent (API response, snapshot restore, future
* localStorage restore, PPTX reimport) to the current version.
*
* Conventions:
* - `migrateSlideContent` is pure (returns a new reference only when
* it has to change something) and idempotent (running it twice is
* identical to running it once).
* - Each schema bump appends a step keyed by the previous version's
* number. v1 (current) needs no per-step migration body — just the
* guarantee that the field is present.
*/
import {
makeScene,
type InteractiveContent,
type Scene,
type SceneContent,
type SlideContent,
} from '@/lib/types/stage';
export const CURRENT_SLIDE_CONTENT_SCHEMA_VERSION = 1;
export function migrateSlideContent(content: SlideContent): SlideContent {
// Forward-compatibility: if a future client has written content with a
// newer schemaVersion than we know about, return it untouched rather
// than silently downgrading. The slide may not render correctly here,
// but its on-disk shape stays intact for the next compatible client.
if (
content.schemaVersion !== undefined &&
content.schemaVersion >= CURRENT_SLIDE_CONTENT_SCHEMA_VERSION
) {
return content;
}
// Legacy data (no schemaVersion) and any older intermediate versions
// fall through here. As schema versions accumulate, walk versions in
// order and apply each step's body before stamping the final version.
return {
...content,
schemaVersion: CURRENT_SLIDE_CONTENT_SCHEMA_VERSION,
};
}
/**
* InteractiveContent migration. The legacy widget-actions pipeline persisted a
* `teacherActions` authoring layer alongside the materialized `actions` stream;
* that field is now dead (playback reads only `scene.actions`). Legacy documents
* carry it in two places — at the top level and nested inside `widgetConfig`
* (every WidgetConfig variant used to declare it) — so drop both on load. Pure +
* idempotent: returns the same reference when there's nothing to drop. No
* schemaVersion is needed yet — the sole change is removing an inert field,
* which is naturally idempotent.
*/
export function migrateInteractiveContent(content: InteractiveContent): InteractiveContent {
const legacy = content as InteractiveContent & {
teacherActions?: unknown;
widgetConfig?: Record<string, unknown> & { teacherActions?: unknown };
};
const hasTop = 'teacherActions' in legacy;
const hasNested = legacy.widgetConfig != null && 'teacherActions' in legacy.widgetConfig;
if (!hasTop && !hasNested) {
return content;
}
const { teacherActions: _top, widgetConfig, ...rest } = legacy;
const next = rest as InteractiveContent & { widgetConfig?: Record<string, unknown> };
if (widgetConfig !== undefined) {
if (hasNested) {
const { teacherActions: _nested, ...widgetRest } = widgetConfig;
next.widgetConfig = widgetRest;
} else {
next.widgetConfig = widgetConfig;
}
}
return next;
}
/**
* Top-level scene migrator — dispatches by scene-content type. SlideContent is
* versioned; InteractiveContent drops its legacy `teacherActions` field; other
* content types pass through. Future surfaces declare their own migrators and
* wire them in here.
*/
export function migrateScene(scene: Scene): Scene {
const migratedContent = migrateSceneContent(scene.content);
if (migratedContent === scene.content) {
return scene;
}
return makeScene(scene, migratedContent);
}
function migrateSceneContent(content: SceneContent): SceneContent {
if (content.type === 'slide') {
return migrateSlideContent(content);
}
if (content.type === 'interactive') {
return migrateInteractiveContent(content);
}
return content;
}