Files
Web_FreeCAD_Bitbybit/public/chrome-opfs-migration-harness.html
wangdequan 97967041e2
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: prepare FreeCAD private naming worker linkage
2026-08-11 23:16:22 -04:00

105 lines
7.3 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', objects: loaded.objects.map((object, index) => ({ ...object, nativeObjectTag: 17 + index })) } : 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,
migratedNativeObjectTag: loaded?.objects[0]?.nativeObjectTag ?? null,
recomputeDefaulted: loaded?.recompute?.status === 'idle',
reopenedSchemaVersion: reopenedCapabilities.schemaVersion,
reopenedVersion: reopenedDocument?.version ?? null,
reopenedNativeObjectTag: reopenedDocument?.objects[0]?.nativeObjectTag ?? null,
checkpointNativeObjectTag: checkpoint?.objects[0]?.nativeObjectTag ?? 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 === 7 && 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.migratedNativeObjectTag === 1 && report.migration.recomputeDefaulted && report.migration.reopenedSchemaVersion === 7 && report.migration.reopenedVersion === 4 && report.migration.reopenedNativeObjectTag === 17 && report.migration.checkpointNativeObjectTag === 17 && 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>