102 lines
6.9 KiB
HTML
102 lines
6.9 KiB
HTML
<!doctype html>
|
|
<meta charset="utf-8">
|
|
<title>Chrome OPFS migration harness</title>
|
|
<pre id="result">running</pre>
|
|
<script type="module">
|
|
import { createSqliteProjectPersistence } from '/src/facade/projectStore.ts'
|
|
|
|
const output = document.querySelector('#result')
|
|
const report = { schemaVersion: 1, browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
|
|
const documentId = `chrome-rel02-${crypto.randomUUID()}`
|
|
const resourceBytes = new TextEncoder().encode('legacy-opfs-resource')
|
|
const resourceHash = [...new Uint8Array(await crypto.subtle.digest('SHA-256', resourceBytes))].map((value) => value.toString(16).padStart(2, '0')).join('')
|
|
const databasePath = `/bitbybit-rel02-${crypto.randomUUID()}.sqlite3`
|
|
const removeDatabaseFiles = async () => {
|
|
const root = await navigator.storage.getDirectory()
|
|
const prefix = databasePath.slice(1)
|
|
const removed = []
|
|
for await (const [name] of root.entries()) {
|
|
if (name !== prefix && !name.startsWith(prefix)) continue
|
|
try { await root.removeEntry(name); removed.push(name) } catch {}
|
|
}
|
|
return removed
|
|
}
|
|
const seedLegacyDatabase = () => new Promise((resolve, reject) => {
|
|
const worker = new Worker('/src/facade/projectMigrationSeedWorker.ts', { type: 'module', name: 'qa-rel02-legacy-seed' })
|
|
const timeout = setTimeout(() => { worker.terminate(); reject(new Error('Legacy OPFS seed Worker timed out.')) }, 60_000)
|
|
worker.onmessage = (event) => { clearTimeout(timeout); worker.terminate(); if (event.data?.ok) resolve(event.data); else reject(new Error(event.data?.error || 'Legacy OPFS seed failed.')) }
|
|
worker.onerror = (event) => { clearTimeout(timeout); worker.terminate(); reject(new Error(event.message || 'Legacy OPFS seed Worker failed.')) }
|
|
worker.postMessage({ operation: 'seed', databasePath, documentId, resourceHash, resourceBytes: resourceBytes.buffer }, [resourceBytes.buffer])
|
|
})
|
|
const inspectDatabase = () => new Promise((resolve, reject) => {
|
|
const worker = new Worker('/src/facade/projectMigrationSeedWorker.ts', { type: 'module', name: 'qa-rel02-legacy-inspect' })
|
|
const timeout = setTimeout(() => { worker.terminate(); reject(new Error('Legacy OPFS inspect Worker timed out.')) }, 60_000)
|
|
worker.onmessage = (event) => { clearTimeout(timeout); worker.terminate(); if (event.data?.ok) resolve(event.data); else reject(new Error(event.data?.error || 'Legacy OPFS inspect failed.')) }
|
|
worker.onerror = (event) => { clearTimeout(timeout); worker.terminate(); reject(new Error(event.message || 'Legacy OPFS inspect Worker failed.')) }
|
|
worker.postMessage({ operation: 'inspect', databasePath })
|
|
})
|
|
|
|
let persistence
|
|
let reopened
|
|
let failedPersistence
|
|
try {
|
|
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome REL-02 harness requires isolated OPFS.')
|
|
await removeDatabaseFiles()
|
|
await seedLegacyDatabase()
|
|
failedPersistence = createSqliteProjectPersistence({ databasePath, migrationInterruptAfterVersion: 3 })
|
|
let rollbackError = ''
|
|
try { await failedPersistence.initialize() } catch (error) { rollbackError = error instanceof Error ? error.message : String(error) }
|
|
await failedPersistence.dispose()
|
|
failedPersistence = undefined
|
|
const afterRollback = await inspectDatabase()
|
|
persistence = createSqliteProjectPersistence({ databasePath })
|
|
const migrated = await persistence.initialize()
|
|
const loaded = await persistence.load(documentId)
|
|
const restoredResource = await persistence.resource.get(resourceHash)
|
|
const recovery = await persistence.recovery(documentId)
|
|
const migratedDocument = loaded ? { ...loaded, version: loaded.version + 1, dirty: false, label: 'Migrated OPFS project' } : null
|
|
if (!migratedDocument) throw new Error('Legacy document did not load after migration.')
|
|
await persistence.save(migratedDocument)
|
|
await persistence.dispose()
|
|
persistence = undefined
|
|
|
|
reopened = createSqliteProjectPersistence({ databasePath })
|
|
const reopenedCapabilities = await reopened.initialize()
|
|
const reopenedDocument = await reopened.load(documentId)
|
|
const checkpoint = await reopened.loadCheckpoint(documentId, migratedDocument.version)
|
|
const reopenedRecovery = await reopened.recovery(documentId)
|
|
await reopened.resource.release(resourceHash)
|
|
const releasedResource = await reopened.resource.get(resourceHash)
|
|
report.migration = {
|
|
sourceSchemaVersion: 1,
|
|
migratedSchemaVersion: migrated.schemaVersion,
|
|
migratedDocumentVersion: loaded?.version ?? null,
|
|
migratedLabel: loaded?.label ?? null,
|
|
migratedObjectCount: loaded?.objects.length ?? 0,
|
|
recomputeDefaulted: loaded?.recompute?.status === 'idle',
|
|
reopenedSchemaVersion: reopenedCapabilities.schemaVersion,
|
|
reopenedVersion: reopenedDocument?.version ?? null,
|
|
reopenedLabel: reopenedDocument?.label ?? null,
|
|
checkpointVersion: checkpoint?.version ?? null,
|
|
recoveryIntegrity: recovery.integrity,
|
|
reopenedRecoveryIntegrity: reopenedRecovery.integrity,
|
|
resourceRoundTrip: restoredResource ? new TextDecoder().decode(restoredResource) === 'legacy-opfs-resource' : false,
|
|
resourceReleased: releasedResource === null,
|
|
}
|
|
report.rollback = { rejected: rollbackError.includes('interruption after schema version 3'), interruptedAfterMarker: rollbackError.includes('interruption after schema version 3'), error: rollbackError, appliedVersionsAfterFailure: afterRollback.appliedVersions || [] }
|
|
report.cleanup = { databasePath, removedFiles: [] }
|
|
report.status = report.crossOriginIsolated && migrated.mode === 'sqlite-opfs' && migrated.schemaVersion === 6 && report.rollback.rejected && report.rollback.interruptedAfterMarker && report.rollback.appliedVersionsAfterFailure.length === 1 && report.rollback.appliedVersionsAfterFailure[0] === 1 && report.migration.migratedDocumentVersion === 3 && report.migration.migratedObjectCount === 1 && report.migration.recomputeDefaulted && report.migration.reopenedSchemaVersion === 6 && report.migration.reopenedVersion === 4 && report.migration.checkpointVersion === 4 && report.migration.recoveryIntegrity === 'ok' && report.migration.reopenedRecoveryIntegrity === 'ok' && report.migration.resourceRoundTrip && report.migration.resourceReleased ? 'pass' : 'failed'
|
|
} catch (error) {
|
|
report.status = 'failed'
|
|
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
|
} finally {
|
|
await persistence?.dispose().catch(() => {})
|
|
await reopened?.dispose().catch(() => {})
|
|
await failedPersistence?.dispose().catch(() => {})
|
|
try { report.cleanup = { databasePath, removedFiles: await removeDatabaseFiles() } } catch (error) { report.cleanupError = error instanceof Error ? error.message : String(error) }
|
|
}
|
|
output.textContent = JSON.stringify(report, null, 2)
|
|
await fetch('/__opfs-migration-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
|
if (report.status !== 'pass') throw new Error(report.error || 'Chrome OPFS migration harness failed.')
|
|
</script>
|