Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions editor/grida-canvas-hosted/playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,18 +499,14 @@ export default function CanvasPlayground({
} catch (error) {
if (error instanceof Error && error.message.includes("not found")) {
// File doesn't exist yet - this is fine, continue to fallback
} else if (
error instanceof Error &&
/decode|schema|corrupt|unreadable|invalid|flatbuffers/i.test(
error.message
)
) {
// Decode failure (likely a schema change) or other corruption.
// Quarantine the stale files so the user's data is preserved
// for possible future migration, then fall through to the
// default document.
} else {
// Any decode, structural validation, or state-init failure
Comment on lines +502 to +503
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict OPFS quarantine to incompatible-document errors

This new else branch quarantines on every non-not found exception from the OPFS load path, so transient failures (for example Failed to read OPFS document.grida: ... from storage/runtime issues) and editor runtime errors from reset()/loadImages() now trigger opfs.quarantine(). Because quarantine() moves files and removes the originals (removeEntry in packages/grida-canvas-io/index.ts), a temporary failure can effectively hide otherwise valid user data and force fallback to an empty/default document. Quarantine should remain limited to decode/schema incompatibility signals, not all load-time exceptions.

Useful? React with 👍 / 👎.

// means the persisted data is incompatible with the current
// version. Quarantine the stale files so the user's bytes are
// preserved for possible future migration, then fall through
// to the default document.
console.warn(
"OPFS document unreadable (possible schema change), quarantining:",
"OPFS document unusable (possible schema change), quarantining:",
error
);
try {
Expand All @@ -521,10 +517,6 @@ export default function CanvasPlayground({
quarantineError
);
}
} else {
// Transient or runtime error (e.g. from reset() / loadImages()).
// Do not quarantine - rethrow so the caller can handle it.
console.error("OPFS load error (not quarantining):", error);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/grida-canvas-io/__tests__/archive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function createFile(filename: string, content: Uint8Array): File {
}

describe("archive (.grida zip)", () => {
const schemaVersion = "0.0.0-test+00000000";
const schemaVersion = grida.program.document.SCHEMA_VERSION;

// Helper to create a minimal test document
function createTestDocument(): grida.program.document.Document {
Expand Down
9 changes: 7 additions & 2 deletions packages/grida-canvas-io/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5436,8 +5436,13 @@ export namespace format {
);
}

const schemaVersion =
document.schemaVersion() || grida.program.document.SCHEMA_VERSION;
const schemaVersion = document.schemaVersion() ?? "";

if (!grida.program.document.isSchemaCompatible(schemaVersion)) {
throw new Error(
`schema incompatible: file version "${schemaVersion}" is not compatible with current "${grida.program.document.SCHEMA_VERSION}"`
);
}

// Decode nodes array and collect parent references
const nodes: Record<string, grida.program.nodes.Node> = {};
Expand Down
35 changes: 35 additions & 0 deletions packages/grida-canvas-schema/grida.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,41 @@ export namespace grida {
export namespace grida.program.document {
export const SCHEMA_VERSION = "0.91.0-beta+20260311";

/**
* Schema version compatibility check.
*
* ## Versioning policy
*
* We follow semver-ish `MAJOR.MINOR.PATCH-prerelease+build`.
*
* **While MAJOR is 0** (current), MINOR is the breaking-change boundary:
* - 0.90.x → 0.91.x is a **breaking** change (e.g. FlatBuffers field-ID renumbering).
* - 0.91.0 → 0.91.1 is a **compatible** (evolution) change.
*
* **Once MAJOR ≥ 1**, MAJOR becomes the breaking-change boundary (standard semver).
*
* @returns `true` if the file version is compatible with the current reader.
*/
export function isSchemaCompatible(fileVersion: string): boolean {
const current = parseVersion(SCHEMA_VERSION);
const file = parseVersion(fileVersion);
if (!current || !file) return false;

if (current.major === 0) {
// Pre-1.0: minor is the breaking boundary
return file.major === current.major && file.minor === current.minor;
}
// Post-1.0: major is the breaking boundary (standard semver)
return file.major === current.major;
}

function parseVersion(
version: string
): { major: number; minor: number } | null {
const match = version.match(/^(\d+)\.(\d+)\./);
return match ? { major: Number(match[1]), minor: Number(match[2]) } : null;
}

/**
* JSON-serializable value type
* Represents all valid JSON values (no functions, undefined, etc.)
Expand Down
Loading