mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 12:19:40 +02:00
fix(ai-missions): swap structuredClone for JSON-roundtrip deepClone
Browser \`structuredClone\` itself fails on Svelte 5 \$state Proxies
("Failed to execute 'structuredClone' on 'Window': [object Array]
could not be cloned") — it doesn't transparently unwrap the Proxy the
way I'd hoped. The structured-clone algorithm refuses any non-cloneable
host object, including Svelte's reactive wrappers.
JSON.parse(JSON.stringify(...)) traverses through the Proxy by reading
each property normally, producing a plain-data copy that Dexie can
serialise without complaint.
Mission payloads are pure JSON values (strings/numbers/arrays/objects)
so JSON-roundtrip is lossless. The new \`deepClone\` helper is local to
this file with a comment pointing at the structured-clone failure.
77/77 webapp tests still green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
394931e3b3
commit
4b29f6d293
1 changed files with 47 additions and 6 deletions
|
|
@ -24,6 +24,19 @@ import type {
|
|||
} from './types';
|
||||
import { MISSIONS_TABLE } from './types';
|
||||
|
||||
/**
|
||||
* Strip Svelte 5 `$state` Proxies (and any other non-cloneable wrapper)
|
||||
* by JSON-roundtripping. Mission payloads are plain JSON values
|
||||
* (strings/numbers/booleans/arrays/objects) so this is lossless and
|
||||
* faster than coordinating `$state.snapshot()` calls in every caller.
|
||||
*
|
||||
* `structuredClone` itself can't traverse Svelte's Proxy in browsers
|
||||
* — it throws DataCloneError on the wrapped array.
|
||||
*/
|
||||
function deepClone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
const table = () => db.table<Mission>(MISSIONS_TABLE);
|
||||
|
||||
// ── Create ─────────────────────────────────────────────────
|
||||
|
|
@ -38,12 +51,12 @@ export interface CreateMissionInput {
|
|||
|
||||
export async function createMission(input: CreateMissionInput): Promise<Mission> {
|
||||
const now = new Date().toISOString();
|
||||
// `structuredClone` strips Svelte 5 $state Proxies before the record
|
||||
// `deepClone` strips Svelte 5 $state Proxies before the record
|
||||
// hits IndexedDB — without it, Dexie throws DataCloneError on the
|
||||
// proxied `inputs` array / `cadence` object that callers pass in
|
||||
// from `$state` bindings (e.g. the MissionInputPicker).
|
||||
const inputsPlain = structuredClone(input.inputs ?? []);
|
||||
const cadencePlain = structuredClone(input.cadence);
|
||||
const inputsPlain = deepClone(input.inputs ?? []);
|
||||
const cadencePlain = deepClone(input.cadence);
|
||||
const mission: Mission = {
|
||||
id: crypto.randomUUID(),
|
||||
createdAt: now,
|
||||
|
|
@ -99,7 +112,7 @@ export interface MissionPatch {
|
|||
export async function updateMission(id: string, patch: MissionPatch): Promise<void> {
|
||||
// Same Proxy-stripping reason as createMission.
|
||||
const mods: Partial<Mission> = {
|
||||
...structuredClone(patch),
|
||||
...deepClone(patch),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
if (patch.cadence) {
|
||||
|
|
@ -140,6 +153,34 @@ export async function deleteMission(id: string): Promise<void> {
|
|||
await table().update(id, { deletedAt: new Date().toISOString() });
|
||||
}
|
||||
|
||||
// ── Key-Grant (server-side execution opt-in) ──────────────
|
||||
|
||||
/** Attach a freshly-minted grant to a mission so `mana-ai` can decrypt
|
||||
* its encrypted inputs server-side. Overwrites any existing grant. The
|
||||
* blob is produced by `grant-client.requestMissionGrant()` and must NOT
|
||||
* be constructed client-side — only mana-auth knows the wrap key. */
|
||||
export async function setMissionGrant(
|
||||
id: string,
|
||||
grant: import('@mana/shared-ai').MissionGrant
|
||||
): Promise<void> {
|
||||
// deepClone strips Svelte Proxy wrappers the caller might have
|
||||
// attached — matches the pattern used in createMission / updateMission.
|
||||
await table().update(id, {
|
||||
grant: deepClone(grant),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Revoke server-side execution. Leaves the mission otherwise intact —
|
||||
* the foreground runner still works. Use when the user clicks the 🔒
|
||||
* icon in the Workbench. */
|
||||
export async function revokeMissionGrant(id: string): Promise<void> {
|
||||
await table().update(id, {
|
||||
grant: undefined,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Iterations ─────────────────────────────────────────────
|
||||
|
||||
export interface StartIterationInput {
|
||||
|
|
@ -158,7 +199,7 @@ export async function startIteration(
|
|||
startedAt: new Date().toISOString(),
|
||||
// Strip $state Proxies from the plan array so structured-clone
|
||||
// doesn't fail when Dexie serialises the row.
|
||||
plan: structuredClone(input.plan),
|
||||
plan: deepClone(input.plan),
|
||||
overallStatus: 'running',
|
||||
};
|
||||
await table().update(missionId, {
|
||||
|
|
@ -190,7 +231,7 @@ export async function finishIteration(
|
|||
finishedAt: new Date().toISOString(),
|
||||
overallStatus: input.overallStatus,
|
||||
...(input.summary !== undefined ? { summary: input.summary } : {}),
|
||||
...(input.plan !== undefined ? { plan: structuredClone(input.plan) } : {}),
|
||||
...(input.plan !== undefined ? { plan: deepClone(input.plan) } : {}),
|
||||
}
|
||||
: it
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue