feat(ms-ai-architect): playground v3 export/import with eager migrations
Step 3/17 av Playground v3-leveransen.
Eksport:
- buildEnvelope(): { appId, schemaVersion, exportedAt, shared, projects,
activeProjectId, activeSurface, preferences } — JSON.parse(JSON.stringify(...))
for å strippe Proxy-wrappere
- exportState(): Blob + URL.createObjectURL + programmatisk <a download>-klikk
+ revokeObjectURL etter 0ms timeout. File System Access API krever HTTPS
(secure context) og er ikke tilgjengelig på file:// — derfor Blob-pattern.
- Filnavn-format: ms-ai-architect-playground-<ISO-stamp>.json
Import:
- importState(File): file.text() -> JSON.parse -> envelope-validering (appId
+ schemaVersion required) -> migrateState() -> persistence.save() -> in-place
state-update (Proxy-binding må bevares — kan ikke bytte raw-referansen)
-> manuell 'change'-event-dispatch så subscribers re-rendrer
- file.text() er Promise<string> som fungerer på file:// uten secure context
MIGRATIONS-pipeline:
- Eager: alle migrasjoner kjøres sekvensielt fra fil-versjon til SCHEMA_VERSION
ved import (ikke lazy ved access)
- Nøkkel-format: 'N->M' (fortløpende). Aldri hopp over et steg.
- Kaster eksplisitt feil ved manglende migrasjons-funksjon eller ved
funksjon som ikke setter schemaVersion korrekt — silent corruption
unngås (brief Risk High).
Eksponerte globals: __buildEnvelope, __exportState, __importState, __MIGRATIONS.
Verify-assert: JSON.parse(JSON.stringify(window.__buildEnvelope())).schemaVersion === 1
Plan: .claude/projects/2026-05-03-playground-v3-architecture/plan.md (Step 3)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
483dad8049
commit
995f64ad8c
1 changed files with 138 additions and 0 deletions
|
|
@ -350,6 +350,144 @@
|
||||||
// Step 6+ vil trigger første render her.
|
// Step 6+ vil trigger første render her.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// EXPORT / IMPORT (Step 3)
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Brukeren kan eksportere hele state som JSON-fil og re-importere på en
|
||||||
|
// annen enhet (eller etter localStorage.clear()). Format er en envelope
|
||||||
|
// med schemaVersion + appId — så fremtidige versjoner kan lese gamle
|
||||||
|
// eksporter via MIGRATIONS-pipeline.
|
||||||
|
//
|
||||||
|
// File System Access API krever HTTPS (secure context) og er ikke
|
||||||
|
// tilgjengelig på file:// — vi bruker Blob + URL.createObjectURL +
|
||||||
|
// <a download> for eksport, og <input type="file"> + File.text() for
|
||||||
|
// import. Begge fungerer på file:// i alle target-browsers.
|
||||||
|
//
|
||||||
|
// MIGRATIONS er en eager pipeline: ved import (eller cold-load fra
|
||||||
|
// gammel state) kjøres alle migrasjoner sekvensielt fra fil-versjon til
|
||||||
|
// gjeldende SCHEMA_VERSION. Aldri hopp over et steg — selv tomme
|
||||||
|
// migrasjoner skal være registrert (no-op) for å bevise at hoppet er
|
||||||
|
// håndtert.
|
||||||
|
|
||||||
|
const APP_ID = 'ms-ai-architect-playground';
|
||||||
|
|
||||||
|
function buildEnvelope() {
|
||||||
|
// Snapshot av rå state. JSON.stringify(JSON.parse(...)) sørger for
|
||||||
|
// at Proxy-er er stripped; vi vil ikke at envelopet skal beholde
|
||||||
|
// wrapper-referanser.
|
||||||
|
const snapshot = store ? JSON.parse(JSON.stringify(store.raw)) : JSON.parse(JSON.stringify(INITIAL_STATE));
|
||||||
|
return {
|
||||||
|
appId: APP_ID,
|
||||||
|
schemaVersion: snapshot.schemaVersion || SCHEMA_VERSION,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
shared: snapshot.shared,
|
||||||
|
projects: snapshot.projects,
|
||||||
|
activeProjectId: snapshot.activeProjectId,
|
||||||
|
activeSurface: snapshot.activeSurface,
|
||||||
|
preferences: snapshot.preferences
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportState() {
|
||||||
|
const envelope = buildEnvelope();
|
||||||
|
const json = JSON.stringify(envelope, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const stamp = envelope.exportedAt.replace(/[:.]/g, '-');
|
||||||
|
a.href = url;
|
||||||
|
a.download = APP_ID + '-' + stamp + '.json';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
// revokeObjectURL etter at klikket har skjedd; setTimeout 0 er nok i
|
||||||
|
// alle target-browsers (Blob URL holdes så lenge nedlastningen står
|
||||||
|
// for å initieres).
|
||||||
|
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIGRATIONS-pipeline. Nøkkel-format: 'N->M' der N og M er fortløpende
|
||||||
|
// SCHEMA_VERSION-tall. Funksjon tar et state-objekt og returnerer et
|
||||||
|
// nytt state-objekt på neste versjon. Aldri muter input.
|
||||||
|
//
|
||||||
|
// Når SCHEMA_VERSION bumpes til 2: legg til '1->2'-funksjon som
|
||||||
|
// transformerer v1-state til v2-form. importState plukker opp
|
||||||
|
// migrasjonen automatisk.
|
||||||
|
const MIGRATIONS = {
|
||||||
|
// Eksempel for fremtid (no-op stub):
|
||||||
|
// '1->2': function (state) { return Object.assign({}, state, { schemaVersion: 2 }); }
|
||||||
|
};
|
||||||
|
|
||||||
|
function migrateState(state) {
|
||||||
|
let current = state;
|
||||||
|
let from = current.schemaVersion || 1;
|
||||||
|
while (from < SCHEMA_VERSION) {
|
||||||
|
const key = from + '->' + (from + 1);
|
||||||
|
const fn = MIGRATIONS[key];
|
||||||
|
if (!fn) {
|
||||||
|
throw new Error('[playground v3] mangler migrasjon ' + key + ' — kan ikke trygt oppgradere import-fil');
|
||||||
|
}
|
||||||
|
current = fn(current);
|
||||||
|
if (current.schemaVersion !== from + 1) {
|
||||||
|
throw new Error('[playground v3] migrasjon ' + key + ' satte ikke schemaVersion til ' + (from + 1));
|
||||||
|
}
|
||||||
|
from = current.schemaVersion;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importState(file) {
|
||||||
|
// file er File-objekt fra <input type="file"> change-event.
|
||||||
|
// file.text() er Promise<string> — fungerer på file:// uten secure context.
|
||||||
|
const text = await file.text();
|
||||||
|
let envelope;
|
||||||
|
try {
|
||||||
|
envelope = JSON.parse(text);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Ugyldig JSON: ' + err.message);
|
||||||
|
}
|
||||||
|
if (envelope.appId !== APP_ID) {
|
||||||
|
throw new Error('Fil-en er ikke en ' + APP_ID + '-eksport (appId=' + envelope.appId + ')');
|
||||||
|
}
|
||||||
|
if (typeof envelope.schemaVersion !== 'number') {
|
||||||
|
throw new Error('Mangler schemaVersion i envelope');
|
||||||
|
}
|
||||||
|
// Migrer envelope opp til gjeldende SCHEMA_VERSION før vi commit-er.
|
||||||
|
const migrated = migrateState({
|
||||||
|
schemaVersion: envelope.schemaVersion,
|
||||||
|
shared: envelope.shared || INITIAL_STATE.shared,
|
||||||
|
projects: envelope.projects || [],
|
||||||
|
activeProjectId: envelope.activeProjectId || null,
|
||||||
|
activeSurface: envelope.activeSurface || 'home',
|
||||||
|
preferences: envelope.preferences || INITIAL_STATE.preferences
|
||||||
|
});
|
||||||
|
// Skriv direkte til persistens for å unngå at debounce-vinduet
|
||||||
|
// svelger import-en ved en samtidig page-unload.
|
||||||
|
if (persistence) {
|
||||||
|
await persistence.save(migrated);
|
||||||
|
}
|
||||||
|
// Erstatt store-state in-place. Vi kan ikke bytte ut store.raw
|
||||||
|
// sin referanse fordi Proxy-en er bundet til den; muter feltvis.
|
||||||
|
const target = store.raw;
|
||||||
|
target.schemaVersion = migrated.schemaVersion;
|
||||||
|
target.shared = migrated.shared;
|
||||||
|
target.projects = migrated.projects;
|
||||||
|
target.activeProjectId = migrated.activeProjectId;
|
||||||
|
target.activeSurface = migrated.activeSurface;
|
||||||
|
target.preferences = migrated.preferences;
|
||||||
|
// Trigger en change-event manuelt så subscribers re-rendrer.
|
||||||
|
sharedBus.dispatchEvent(new CustomEvent('change', { detail: { paths: ['*'] } }));
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eksponer for UI-handlere (Step 5+) og DevTools-debugging.
|
||||||
|
window.__buildEnvelope = buildEnvelope;
|
||||||
|
window.__exportState = exportState;
|
||||||
|
window.__importState = importState;
|
||||||
|
window.__MIGRATIONS = MIGRATIONS;
|
||||||
|
|
||||||
// Auto-bootstrap. Kjør så snart DOM er parsed; vi er på slutten av <body>
|
// Auto-bootstrap. Kjør så snart DOM er parsed; vi er på slutten av <body>
|
||||||
// så DOM er allerede klar.
|
// så DOM er allerede klar.
|
||||||
bootstrap().catch(function (err) {
|
bootstrap().catch(function (err) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue