feat(mcp): add headless scene bridge with RAF polyfill
SceneBridge class wraps @pascal-app/core's Zustand store for Node, exposing a clean programmatic API for scene load/mutate/export plus Zundo undo/redo. Requires a requestAnimationFrame polyfill loaded before any core import to work around the store's RAF-batched dirty marking. - 51 tests, 99.68% line coverage on scene-bridge.ts - All-or-nothing applyPatch with Zod dry-run validation - Safeguards against prototype-polluting keys in loadJSON - Resolves children through the flat nodes dict (handles the SiteNode.children-as-objects inconsistency) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
eebbef502d
commit
07ed429d58
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Node-compatibility shims for `@pascal-app/core`.
|
||||
*
|
||||
* The core store uses `requestAnimationFrame` inside `updateNodesAction` (to batch
|
||||
* dirty-marking) and inside the temporal undo/redo subscribe callback. Both are
|
||||
* load-reachable — the subscribe callback registers at module import time.
|
||||
*
|
||||
* This file installs a no-op-if-already-defined polyfill that works both in
|
||||
* Node and in the browser. It MUST be imported FIRST from any module that
|
||||
* transitively loads `@pascal-app/core/store`, otherwise the core module will
|
||||
* throw at import time.
|
||||
*
|
||||
* Side-effectful on import: there is no exported API — just import this file.
|
||||
*/
|
||||
|
||||
type RafCallback = (timestamp: number) => void
|
||||
|
||||
type GlobalWithRaf = typeof globalThis & {
|
||||
requestAnimationFrame?: (cb: RafCallback) => number
|
||||
cancelAnimationFrame?: (id: number) => void
|
||||
}
|
||||
|
||||
const g = globalThis as GlobalWithRaf
|
||||
|
||||
if (typeof g.requestAnimationFrame === 'undefined') {
|
||||
g.requestAnimationFrame = (cb: RafCallback): number => {
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
||||
return setTimeout(() => cb(now), 0) as unknown as number
|
||||
}
|
||||
g.cancelAnimationFrame = (id: number) => {
|
||||
clearTimeout(id as unknown as ReturnType<typeof setTimeout>)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user