Skip to content

Commit 2431b41

Browse files
fix(plugin-stack-persistence): report metadata parse failures (#737)
1 parent b44aba9 commit 2431b41

6 files changed

Lines changed: 76 additions & 22 deletions

File tree

.changeset/fep-2612-parse-metadata.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@stackflow/plugin-stack-persistence": major
33
---
44

5-
`StackSnapshotStrategy` now exposes `metadata.create` and `metadata.parse`, storage loads unknown metadata, and composed strategies persist and validate schema/version envelopes.
5+
`StackSnapshotStrategy` now exposes `metadata.create` and `metadata.parse`, storage loads unknown metadata, and composed strategies persist and validate schema/version envelopes. Metadata parse failures can include details and are reported directly through `onRecordLoadError`.

extensions/plugin-stack-persistence/src/StackSnapshotMetadataDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Result<Value> =
77
}
88
| {
99
ok: false;
10+
detail?: unknown;
1011
};
1112

1213
export interface StackSnapshotMetadataDefinition<Metadata> {

extensions/plugin-stack-persistence/src/composeStrategies.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,43 @@ export function composeStrategies<
3737
},
3838
parse(data) {
3939
if (data === null || typeof data !== "object") {
40-
return { ok: false };
40+
return {
41+
ok: false,
42+
detail: new Error("composed strategy metadata must be an object"),
43+
};
4144
}
4245

4346
const metadata = data as Record<PropertyKey, unknown>;
4447

4548
if (
4649
!Object.hasOwn(metadata, "schema") ||
50+
metadata.schema !== COMPOSED_METADATA_SCHEMA
51+
) {
52+
return {
53+
ok: false,
54+
detail: new Error("invalid composed strategy metadata schema"),
55+
};
56+
}
57+
58+
if (
4759
!Object.hasOwn(metadata, "version") ||
60+
metadata.version !== COMPOSED_METADATA_VERSION
61+
) {
62+
return {
63+
ok: false,
64+
detail: new Error("unsupported composed strategy metadata version"),
65+
};
66+
}
67+
68+
if (
4869
!Object.hasOwn(metadata, "data") ||
49-
metadata.schema !== COMPOSED_METADATA_SCHEMA ||
50-
metadata.version !== COMPOSED_METADATA_VERSION ||
5170
metadata.data === null ||
5271
typeof metadata.data !== "object"
5372
) {
54-
return { ok: false };
73+
return {
74+
ok: false,
75+
detail: new Error("composed strategy metadata data must be an object"),
76+
};
5577
}
5678

5779
const metadataData = metadata.data as Record<PropertyKey, unknown>;
@@ -60,7 +82,10 @@ export function composeStrategies<
6082
Object.keys(metadataData).length !== keys.length ||
6183
!keys.every((key) => Object.hasOwn(metadataData, key))
6284
) {
63-
return { ok: false };
85+
return {
86+
ok: false,
87+
detail: new Error("composed strategy metadata keys do not match"),
88+
};
6489
}
6590

6691
const parsedEntries: Array<[PropertyKey, unknown]> = [];
@@ -69,7 +94,13 @@ export function composeStrategies<
6994
const result = strategies[key].metadata.parse(metadataData[key]);
7095

7196
if (!result.ok) {
72-
return { ok: false };
97+
return {
98+
ok: false,
99+
detail: {
100+
strategy: String(key),
101+
detail: result.detail,
102+
},
103+
};
73104
}
74105

75106
parsedEntries.push([key, result.value]);

extensions/plugin-stack-persistence/src/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@ export class StackSnapshotRecordLoadError extends Error {
1717
this.cause = cause;
1818
}
1919
}
20+
21+
export class StackSnapshotMetadataParseError extends Error {
22+
detail?: unknown;
23+
24+
constructor(detail?: unknown) {
25+
super("failed to parse stack snapshot metadata");
26+
this.name = "StackSnapshotMetadataParseError";
27+
this.detail = detail;
28+
}
29+
}

extensions/plugin-stack-persistence/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {
33
type StrategiesMetadata,
44
} from "./composeStrategies";
55
export {
6+
StackSnapshotMetadataParseError,
67
StackSnapshotRecordLoadError,
78
StackSnapshotRecordSaveError,
89
} from "./errors";

extensions/plugin-stack-persistence/src/stackPersistencePlugin.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { StackflowActions, StackflowPlugin } from "@stackflow/core";
22
import {
3+
StackSnapshotMetadataParseError,
34
StackSnapshotRecordLoadError,
45
StackSnapshotRecordSaveError,
56
} from "./errors";
@@ -9,7 +10,9 @@ import type { StackSnapshotStrategy } from "./StackSnapshotStrategy";
910
export type StackPersistencePluginOptions<Metadata> = {
1011
storage: StackSnapshotStorage<Metadata>;
1112
strategy: StackSnapshotStrategy<Metadata>;
12-
onRecordLoadError?: (error: StackSnapshotRecordLoadError) => void;
13+
onRecordLoadError?: (
14+
error: StackSnapshotRecordLoadError | StackSnapshotMetadataParseError,
15+
) => void;
1316
onRecordSaveError?: (error: StackSnapshotRecordSaveError) => void;
1417
onLoadError?: NonNullable<ReturnType<StackflowPlugin>["onLoadError"]>;
1518
};
@@ -46,30 +49,38 @@ export function stackPersistencePlugin<Metadata>({
4649
return {
4750
key: "@stackflow/plugin-stack-persistence",
4851
provideSnapshot({ initialContext }) {
52+
let record: ReturnType<typeof storage.load>;
53+
4954
try {
50-
const record = storage.load();
55+
record = storage.load();
56+
} catch (error) {
57+
onRecordLoadError?.(new StackSnapshotRecordLoadError(error));
5158

52-
if (!record) return null;
59+
return null;
60+
}
5361

54-
const parsedMetadata = strategy.metadata.parse(record.metadata);
62+
if (!record) return null;
5563

56-
if (!parsedMetadata.ok) return null;
64+
const parsedMetadata = strategy.metadata.parse(record.metadata);
5765

58-
const parsedRecord = {
59-
...record,
60-
metadata: parsedMetadata.value,
61-
};
66+
if (!parsedMetadata.ok) {
67+
onRecordLoadError?.(
68+
new StackSnapshotMetadataParseError(parsedMetadata.detail),
69+
);
6270

63-
if (!strategy.shouldReuse({ record: parsedRecord, initialContext })) {
64-
return null;
65-
}
71+
return null;
72+
}
6673

67-
return parsedRecord.snapshot;
68-
} catch (error) {
69-
onRecordLoadError?.(new StackSnapshotRecordLoadError(error));
74+
const parsedRecord = {
75+
...record,
76+
metadata: parsedMetadata.value,
77+
};
7078

79+
if (!strategy.shouldReuse({ record: parsedRecord, initialContext })) {
7180
return null;
7281
}
82+
83+
return parsedRecord.snapshot;
7384
},
7485
onLoadError(...args) {
7586
return onLoadError?.(...args) ?? { policy: "recover" };

0 commit comments

Comments
 (0)