@volter/editor-core 0.5.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
import { emitHistoryElement, historyDelegate, historyDelegateInstalled } from './history-delegate';
|
|
2
|
+
import { PersistenceCoordinator } from './persistence-coordinator';
|
|
3
|
+
import { ResourceRegistry } from './resource-registry';
|
|
4
|
+
import { SnapshotStore } from './snapshot-store';
|
|
5
|
+
import type {
|
|
6
|
+
HistoryBudget,
|
|
7
|
+
HistoryError,
|
|
8
|
+
HistoryEviction,
|
|
9
|
+
HistoryLimitWarning,
|
|
10
|
+
HistoryStatus,
|
|
11
|
+
HistoryTransaction,
|
|
12
|
+
ResourceChange,
|
|
13
|
+
ResourceDriver,
|
|
14
|
+
ResourceKey,
|
|
15
|
+
ResourceSnapshot,
|
|
16
|
+
SnapshotRef,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_TRANSACTIONS = 2000;
|
|
20
|
+
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const DEFAULT_MAX_TRANSACTION_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fraction of the nearest-binding budget at which the user is told they are
|
|
25
|
+
* running out of undo depth. Early enough to raise a limit before anything is
|
|
26
|
+
* lost; late enough that ordinary sessions never see it.
|
|
27
|
+
*/
|
|
28
|
+
const LIMIT_WARNING_THRESHOLD = 0.8;
|
|
29
|
+
|
|
30
|
+
/** What `ResourceRegistry.registerProject` prefixes a project-relative path
|
|
31
|
+
* with. Spelled once, beside the only reader that has to strip it. */
|
|
32
|
+
const PROJECT_LOCATION_PREFIX = 'project://';
|
|
33
|
+
|
|
34
|
+
export interface HistoryLimits {
|
|
35
|
+
readonly maxTransactions: number;
|
|
36
|
+
readonly maxBytes: number;
|
|
37
|
+
readonly maxTransactionBytes: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TransactionOptions {
|
|
41
|
+
readonly label: string;
|
|
42
|
+
readonly detail?: string;
|
|
43
|
+
readonly resources: readonly ResourceKey[];
|
|
44
|
+
readonly scope: 'project' | 'session';
|
|
45
|
+
readonly sessionId?: string;
|
|
46
|
+
readonly mergeKey?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HistoryTransactionContext {
|
|
50
|
+
read(resource: ResourceKey): Promise<ResourceSnapshot>;
|
|
51
|
+
write(resource: ResourceKey, bytes: Uint8Array, contentType?: string): Promise<void>;
|
|
52
|
+
writeText(resource: ResourceKey, text: string, contentType?: string): Promise<void>;
|
|
53
|
+
writeJson(resource: ResourceKey, value: unknown): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AppliedResourceChange {
|
|
57
|
+
readonly resource: ResourceKey;
|
|
58
|
+
readonly beforeBytes: Uint8Array;
|
|
59
|
+
readonly afterBytes: Uint8Array;
|
|
60
|
+
readonly contentType: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface HistorySnapshot {
|
|
64
|
+
readonly version: number;
|
|
65
|
+
readonly transactions: readonly HistoryTransaction[];
|
|
66
|
+
readonly cursor: number;
|
|
67
|
+
readonly busy: boolean;
|
|
68
|
+
readonly blocked: boolean;
|
|
69
|
+
readonly canUndo: boolean;
|
|
70
|
+
readonly canRedo: boolean;
|
|
71
|
+
readonly undoLabel: string | null;
|
|
72
|
+
readonly redoLabel: string | null;
|
|
73
|
+
readonly uniqueSnapshotBytes: number;
|
|
74
|
+
readonly lastError: HistoryError | null;
|
|
75
|
+
/**
|
|
76
|
+
* Set once per crossing of {@link LIMIT_WARNING_THRESHOLD} on whichever budget
|
|
77
|
+
* is nearest to binding, and cleared when pressure falls back below it. The
|
|
78
|
+
* same warning object persists across later commits, so a caller can compare
|
|
79
|
+
* identity to tell a fresh crossing from a still-standing one.
|
|
80
|
+
*/
|
|
81
|
+
readonly limitWarning: HistoryLimitWarning | null;
|
|
82
|
+
/** The most recent trim that actually dropped entries, if any. */
|
|
83
|
+
readonly lastEviction: HistoryEviction | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HistoryServiceOptions {
|
|
87
|
+
readonly registry?: ResourceRegistry;
|
|
88
|
+
readonly snapshots?: SnapshotStore;
|
|
89
|
+
readonly persistence?: PersistenceCoordinator;
|
|
90
|
+
readonly limits?: Partial<HistoryLimits>;
|
|
91
|
+
readonly makeId?: () => string;
|
|
92
|
+
readonly now?: () => number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface StagedResource {
|
|
96
|
+
readonly driver: ResourceDriver;
|
|
97
|
+
readonly beforeSnapshot: ResourceSnapshot;
|
|
98
|
+
readonly before: SnapshotRef;
|
|
99
|
+
afterBytes: Uint8Array;
|
|
100
|
+
afterContentType: string;
|
|
101
|
+
beforeOwned: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface MutableTransaction extends Omit<HistoryTransaction, 'status' | 'error'> {
|
|
105
|
+
status: HistoryStatus;
|
|
106
|
+
error: HistoryError | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class HistoryOperationError extends Error {
|
|
110
|
+
constructor(readonly historyError: HistoryError) {
|
|
111
|
+
super(historyError.message);
|
|
112
|
+
this.name = 'HistoryOperationError';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function defaultId(): string {
|
|
117
|
+
return crypto.randomUUID();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* THE REFUSING SENTENCE behind a failure, or `null` when the cause carries
|
|
122
|
+
* none.
|
|
123
|
+
*
|
|
124
|
+
* Measured 2026-09-19 under the Code-OSS frame: `vgaiFiles.write` refused to
|
|
125
|
+
* write over a Monaco model holding unsaved keystrokes — by name, naming the
|
|
126
|
+
* file and what to do about it — and the only thing any door said was
|
|
127
|
+
* *"Source write rolled back: Failed to apply “Set position”; prior state was
|
|
128
|
+
* restored. (apply-failed)"*. The refusal was RIGHT THERE, as this error's
|
|
129
|
+
* `cause`, and every reader of the failure — the console ledger, `vgai
|
|
130
|
+
* console`, the document's own message — restated the generic half and threw
|
|
131
|
+
* the specific half away. A rollback line that cannot say WHY is a line that
|
|
132
|
+
* sends its reader to look somewhere else.
|
|
133
|
+
*
|
|
134
|
+
* So the cause's own sentence is carried in `message`, at the one place every
|
|
135
|
+
* `HistoryError` is built. `cause` stays too: it is the object, this is the
|
|
136
|
+
* words.
|
|
137
|
+
*/
|
|
138
|
+
function causeSentence(cause: unknown): string | null {
|
|
139
|
+
const raw = cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : null;
|
|
140
|
+
const text = raw?.trim();
|
|
141
|
+
return text ? text : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function historyError(
|
|
145
|
+
code: HistoryError['code'],
|
|
146
|
+
message: string,
|
|
147
|
+
resources: readonly ResourceKey[],
|
|
148
|
+
cause?: unknown,
|
|
149
|
+
): HistoryError {
|
|
150
|
+
const why = causeSentence(cause);
|
|
151
|
+
const said =
|
|
152
|
+
why && !message.includes(why) ? `${message} ${why.endsWith('.') ? why : `${why}.`}` : message;
|
|
153
|
+
const error: HistoryError = { code, message: said, resources };
|
|
154
|
+
return cause === undefined ? error : { ...error, cause };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatMegabytes(bytes: number): string {
|
|
158
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function plural(count: number, noun: string): string {
|
|
162
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* History lives only in this tab's memory, so the honest subject of both
|
|
167
|
+
* messages is undo depth in the current editing session — never saved files,
|
|
168
|
+
* which nothing here touches. The byte wording states the step count too,
|
|
169
|
+
* because "you are nowhere near 2000 steps and are still about to lose depth"
|
|
170
|
+
* is the fact a count-only warning would hide.
|
|
171
|
+
*/
|
|
172
|
+
function limitWarningMessage(
|
|
173
|
+
budget: HistoryBudget,
|
|
174
|
+
entryCount: number,
|
|
175
|
+
bytes: number,
|
|
176
|
+
limits: HistoryLimits,
|
|
177
|
+
): string {
|
|
178
|
+
return budget === 'transactions'
|
|
179
|
+
? `Undo history is near its step limit — ${entryCount} of ${limits.maxTransactions} steps ` +
|
|
180
|
+
'in this editing session. Older steps will start dropping; raise maxTransactions to keep more.'
|
|
181
|
+
: `Undo history is near its memory limit — ${formatMegabytes(bytes)} of ` +
|
|
182
|
+
`${formatMegabytes(limits.maxBytes)} of in-memory snapshots, at only ${entryCount} of ` +
|
|
183
|
+
`${limits.maxTransactions} steps. Memory binds first here; raise maxBytes to keep more.`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function evictionMessage(
|
|
187
|
+
budget: HistoryBudget,
|
|
188
|
+
droppedCount: number,
|
|
189
|
+
limits: HistoryLimits,
|
|
190
|
+
): string {
|
|
191
|
+
const ceiling =
|
|
192
|
+
budget === 'transactions'
|
|
193
|
+
? `${limits.maxTransactions}-step limit`
|
|
194
|
+
: `${formatMegabytes(limits.maxBytes)} memory limit`;
|
|
195
|
+
return (
|
|
196
|
+
`Undo history dropped ${plural(droppedCount, 'older step')} to stay within its ${ceiling}. ` +
|
|
197
|
+
'Those edits can no longer be undone; saved files are unaffected.'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function transactionBytes(changes: readonly ResourceChange[]): number {
|
|
202
|
+
const blobs = new Map<string, number>();
|
|
203
|
+
for (const change of changes) {
|
|
204
|
+
blobs.set(change.before.blobKey, change.before.byteLength);
|
|
205
|
+
blobs.set(change.after.blobKey, change.after.byteLength);
|
|
206
|
+
}
|
|
207
|
+
let total = 0;
|
|
208
|
+
for (const length of blobs.values()) total += length;
|
|
209
|
+
return total;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** One authoritative, async undo/redo timeline for an open project session. */
|
|
213
|
+
export class HistoryService {
|
|
214
|
+
readonly registry: ResourceRegistry;
|
|
215
|
+
readonly snapshots: SnapshotStore;
|
|
216
|
+
readonly persistence: PersistenceCoordinator;
|
|
217
|
+
readonly limits: HistoryLimits;
|
|
218
|
+
|
|
219
|
+
private readonly drivers = new Map<ResourceKey, ResourceDriver>();
|
|
220
|
+
private readonly listeners = new Set<() => void>();
|
|
221
|
+
private readonly commitListeners = new Set<(transaction: HistoryTransaction) => void>();
|
|
222
|
+
private readonly makeId: () => string;
|
|
223
|
+
private readonly now: () => number;
|
|
224
|
+
private entries: MutableTransaction[] = [];
|
|
225
|
+
private cursorValue = 0;
|
|
226
|
+
private version = 0;
|
|
227
|
+
private pendingExecutions = 0;
|
|
228
|
+
private pendingProducers = 0;
|
|
229
|
+
private activeTransaction = false;
|
|
230
|
+
private disposeRequested = false;
|
|
231
|
+
private disposeComplete = false;
|
|
232
|
+
private blockedValue = false;
|
|
233
|
+
private lastErrorValue: HistoryError | null = null;
|
|
234
|
+
private limitWarningValue: HistoryLimitWarning | null = null;
|
|
235
|
+
private lastEvictionValue: HistoryEviction | null = null;
|
|
236
|
+
private droppedTotal = 0;
|
|
237
|
+
private executionTail: Promise<void> = Promise.resolve();
|
|
238
|
+
private publicSnapshot: HistorySnapshot;
|
|
239
|
+
|
|
240
|
+
constructor(options: HistoryServiceOptions = {}) {
|
|
241
|
+
this.registry = options.registry ?? new ResourceRegistry();
|
|
242
|
+
this.snapshots = options.snapshots ?? new SnapshotStore();
|
|
243
|
+
this.persistence = options.persistence ?? new PersistenceCoordinator();
|
|
244
|
+
this.makeId = options.makeId ?? defaultId;
|
|
245
|
+
this.now = options.now ?? Date.now;
|
|
246
|
+
this.limits = {
|
|
247
|
+
maxTransactions: options.limits?.maxTransactions ?? DEFAULT_MAX_TRANSACTIONS,
|
|
248
|
+
maxBytes: options.limits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
249
|
+
maxTransactionBytes: options.limits?.maxTransactionBytes ?? DEFAULT_MAX_TRANSACTION_BYTES,
|
|
250
|
+
};
|
|
251
|
+
if (
|
|
252
|
+
this.limits.maxTransactions < 1 ||
|
|
253
|
+
this.limits.maxBytes < 1 ||
|
|
254
|
+
this.limits.maxTransactionBytes < 1 ||
|
|
255
|
+
this.limits.maxTransactionBytes > this.limits.maxBytes
|
|
256
|
+
) {
|
|
257
|
+
throw new Error('History limits must be positive.');
|
|
258
|
+
}
|
|
259
|
+
this.publicSnapshot = this.buildSnapshot();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
263
|
+
if (this.disposeRequested) return () => undefined;
|
|
264
|
+
this.listeners.add(listener);
|
|
265
|
+
return () => this.listeners.delete(listener);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
getSnapshot = (): HistorySnapshot => this.publicSnapshot;
|
|
269
|
+
|
|
270
|
+
subscribeCommits(listener: (transaction: HistoryTransaction) => void): () => void {
|
|
271
|
+
this.assertNotDisposed();
|
|
272
|
+
this.commitListeners.add(listener);
|
|
273
|
+
return () => this.commitListeners.delete(listener);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Reserves shell ordering while an async producer prepares a transaction. */
|
|
277
|
+
reserveProducer(): () => void {
|
|
278
|
+
this.assertNotDisposed();
|
|
279
|
+
this.pendingProducers++;
|
|
280
|
+
this.notify();
|
|
281
|
+
let released = false;
|
|
282
|
+
return () => {
|
|
283
|
+
if (released) return;
|
|
284
|
+
released = true;
|
|
285
|
+
this.pendingProducers--;
|
|
286
|
+
this.finishDisposeIfIdle();
|
|
287
|
+
if (!this.disposeComplete) this.notify();
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
registerDriver(driver: ResourceDriver): () => void {
|
|
292
|
+
this.assertNotDisposed();
|
|
293
|
+
const key = driver.descriptor.key;
|
|
294
|
+
const registered = this.registry.get(key);
|
|
295
|
+
if (!registered) throw new Error(`Driver resource "${key}" is not registered.`);
|
|
296
|
+
if (this.drivers.has(key)) throw new Error(`Resource "${key}" already has a driver.`);
|
|
297
|
+
this.drivers.set(key, driver);
|
|
298
|
+
return () => {
|
|
299
|
+
if (this.drivers.get(key) === driver) this.drivers.delete(key);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
transaction<T>(
|
|
304
|
+
options: TransactionOptions,
|
|
305
|
+
operation: (tx: HistoryTransactionContext) => Promise<T> | T,
|
|
306
|
+
): Promise<T> {
|
|
307
|
+
this.validateOptions(options);
|
|
308
|
+
if (this.activeTransaction) {
|
|
309
|
+
return Promise.reject(
|
|
310
|
+
new HistoryOperationError(
|
|
311
|
+
historyError('busy', 'A history transaction cannot nest another transaction.', []),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return this.enqueue(() => this.transactionNow(options, operation));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async transactionNow<T>(
|
|
319
|
+
options: TransactionOptions,
|
|
320
|
+
operation: (tx: HistoryTransactionContext) => Promise<T> | T,
|
|
321
|
+
): Promise<T> {
|
|
322
|
+
this.assertCanStartTransaction();
|
|
323
|
+
|
|
324
|
+
this.activeTransaction = true;
|
|
325
|
+
this.notify();
|
|
326
|
+
const allowed = new Set(options.resources);
|
|
327
|
+
const staged = new Map<ResourceKey, StagedResource>();
|
|
328
|
+
const ensureStaged = async (resource: ResourceKey): Promise<StagedResource> => {
|
|
329
|
+
if (!allowed.has(resource)) {
|
|
330
|
+
throw new Error(`Transaction did not declare resource "${resource}".`);
|
|
331
|
+
}
|
|
332
|
+
const existing = staged.get(resource);
|
|
333
|
+
if (existing) return existing;
|
|
334
|
+
const driver = this.requireDriver(resource);
|
|
335
|
+
this.validateResourceScope(driver, options);
|
|
336
|
+
const beforeSnapshot = await driver.capture();
|
|
337
|
+
const before = await this.storeCapturedSnapshot(beforeSnapshot);
|
|
338
|
+
const state: StagedResource = {
|
|
339
|
+
driver,
|
|
340
|
+
beforeSnapshot,
|
|
341
|
+
before,
|
|
342
|
+
afterBytes: Uint8Array.from(beforeSnapshot.bytes),
|
|
343
|
+
afterContentType: beforeSnapshot.contentType,
|
|
344
|
+
beforeOwned: true,
|
|
345
|
+
};
|
|
346
|
+
staged.set(resource, state);
|
|
347
|
+
return state;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const context: HistoryTransactionContext = {
|
|
351
|
+
read: async (resource) => {
|
|
352
|
+
const state = await ensureStaged(resource);
|
|
353
|
+
return {
|
|
354
|
+
revision: state.beforeSnapshot.revision,
|
|
355
|
+
contentType: state.afterContentType,
|
|
356
|
+
bytes: Uint8Array.from(state.afterBytes),
|
|
357
|
+
sha256: state.beforeSnapshot.sha256,
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
write: async (resource, bytes, contentType) => {
|
|
361
|
+
const state = await ensureStaged(resource);
|
|
362
|
+
state.afterBytes = Uint8Array.from(bytes);
|
|
363
|
+
state.afterContentType = contentType ?? state.afterContentType;
|
|
364
|
+
},
|
|
365
|
+
writeText: async (resource, text, contentType = 'text/plain') => {
|
|
366
|
+
await context.write(resource, new TextEncoder().encode(text), contentType);
|
|
367
|
+
},
|
|
368
|
+
writeJson: async (resource, value) => {
|
|
369
|
+
await context.writeText(resource, JSON.stringify(value, null, 2), 'application/json');
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const result = await operation(context);
|
|
375
|
+
const changes = await this.finishStaging(staged);
|
|
376
|
+
if (changes.length === 0) return result;
|
|
377
|
+
await this.commitStagedTransaction(options, changes);
|
|
378
|
+
return result;
|
|
379
|
+
} finally {
|
|
380
|
+
// Any before refs left in a staging map with no matching committed change
|
|
381
|
+
// are released by finishStaging. If the callback throws before that point,
|
|
382
|
+
// release them here.
|
|
383
|
+
for (const state of staged.values()) {
|
|
384
|
+
if (state.beforeOwned) this.snapshots.release(state.before);
|
|
385
|
+
}
|
|
386
|
+
this.activeTransaction = false;
|
|
387
|
+
this.finishDisposeIfIdle();
|
|
388
|
+
if (!this.disposeComplete) this.notify();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* THE HOST SHAPE'S undo: one step back along this service's own cursor.
|
|
394
|
+
* Under frame ownership there is no such step to take — the one ordered
|
|
395
|
+
* stack is the frame's, and an entry is reverted through the element the
|
|
396
|
+
* frame holds ({@link executeEntry}) — so this refuses by name rather than
|
|
397
|
+
* quietly walking a cursor nobody is driving.
|
|
398
|
+
*/
|
|
399
|
+
undo(): Promise<boolean> {
|
|
400
|
+
if (historyDelegateInstalled()) return Promise.resolve(this.delegateOrRefuse('undo'));
|
|
401
|
+
return this.enqueue(() => this.undoNow());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
redo(): Promise<boolean> {
|
|
405
|
+
if (historyDelegateInstalled()) return Promise.resolve(this.delegateOrRefuse('redo'));
|
|
406
|
+
return this.enqueue(() => this.redoNow());
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Revert or reapply ONE recorded entry, addressed by id rather than by
|
|
411
|
+
* cursor. This is what a {@link HistoryElement} the frame holds calls: VS
|
|
412
|
+
* Code's `IUndoRedoService` decides WHICH element is next (per resource, in
|
|
413
|
+
* its own order), and this service only knows how to put that entry's
|
|
414
|
+
* before-bytes back. The cursor is deliberately not moved — under frame
|
|
415
|
+
* ownership it means nothing, and under host ownership nothing calls this.
|
|
416
|
+
*/
|
|
417
|
+
executeEntry(id: string, direction: 'undo' | 'redo'): Promise<boolean> {
|
|
418
|
+
return this.enqueue(async () => {
|
|
419
|
+
const transaction = this.entries.find((entry) => entry.id === id);
|
|
420
|
+
if (!transaction) {
|
|
421
|
+
this.lastErrorValue = historyError(
|
|
422
|
+
'expired',
|
|
423
|
+
`History entry "${id}" is no longer recorded, so it cannot be ${direction}ne.`,
|
|
424
|
+
[],
|
|
425
|
+
);
|
|
426
|
+
this.notify();
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
if (transaction.status === 'expired') {
|
|
430
|
+
const error = historyError(
|
|
431
|
+
'expired',
|
|
432
|
+
`“${transaction.label}” belongs to an expired session.`,
|
|
433
|
+
[],
|
|
434
|
+
);
|
|
435
|
+
transaction.error = error;
|
|
436
|
+
this.lastErrorValue = error;
|
|
437
|
+
this.notify();
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return this.executeExisting(transaction, direction, false);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Every editor affordance that says "undo" — the Edit menu, the palette,
|
|
446
|
+
* `vgai eval` — goes through here, so under the frame they all reach the
|
|
447
|
+
* ONE stack instead of walking a cursor nobody is driving. A frame that has
|
|
448
|
+
* not installed its undo yet gets a named refusal rather than silence.
|
|
449
|
+
*/
|
|
450
|
+
private delegateOrRefuse(direction: 'undo' | 'redo'): boolean {
|
|
451
|
+
const delegate = historyDelegate();
|
|
452
|
+
if (delegate) {
|
|
453
|
+
// A handled keyboard command is not evidence that history moved. In
|
|
454
|
+
// particular, a native document may have no entry on Code-OSS's stack.
|
|
455
|
+
if (!(direction === 'undo' ? delegate.canUndo() : delegate.canRedo())) return false;
|
|
456
|
+
if (direction === 'undo') delegate.undo();
|
|
457
|
+
else delegate.redo();
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
this.lastErrorValue = historyError(
|
|
461
|
+
'busy',
|
|
462
|
+
`History ${direction} belongs to the frame while the editor runs inside one, ` +
|
|
463
|
+
'and the frame has not installed its undo yet.',
|
|
464
|
+
[],
|
|
465
|
+
);
|
|
466
|
+
this.notify();
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Journals an editor mutation whose exact before/after bytes were captured in
|
|
472
|
+
* the same synchronous UI turn. The command queue is shared with undo/redo, so
|
|
473
|
+
* an immediate shortcut always runs after this entry becomes authoritative.
|
|
474
|
+
*/
|
|
475
|
+
recordAppliedTransaction(
|
|
476
|
+
options: TransactionOptions,
|
|
477
|
+
applied: readonly AppliedResourceChange[],
|
|
478
|
+
): Promise<boolean> {
|
|
479
|
+
this.assertCanRecordAppliedChange();
|
|
480
|
+
this.validateOptions(options);
|
|
481
|
+
if (applied.length === 0) return Promise.resolve(false);
|
|
482
|
+
const declared = new Set(options.resources);
|
|
483
|
+
if (
|
|
484
|
+
applied.length !== declared.size ||
|
|
485
|
+
applied.some((change) => !declared.has(change.resource))
|
|
486
|
+
) {
|
|
487
|
+
return Promise.reject(
|
|
488
|
+
new Error('Applied history changes must exactly match declared resources.'),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
// Resolve drivers before queueing. A synchronous document switch may retire
|
|
492
|
+
// the resource before this journal task runs, but the already-applied edit
|
|
493
|
+
// must still be recorded (as expired), never rejected or retargeted.
|
|
494
|
+
for (const change of applied) {
|
|
495
|
+
const driver = this.requireDriver(change.resource);
|
|
496
|
+
this.validateResourceScope(driver, options);
|
|
497
|
+
}
|
|
498
|
+
return this.enqueue(async () => {
|
|
499
|
+
this.assertCanRecordAppliedChange();
|
|
500
|
+
const changes: ResourceChange[] = [];
|
|
501
|
+
try {
|
|
502
|
+
for (const change of applied) {
|
|
503
|
+
const before = await this.snapshots.put({
|
|
504
|
+
revision: 0,
|
|
505
|
+
contentType: change.contentType,
|
|
506
|
+
bytes: change.beforeBytes,
|
|
507
|
+
});
|
|
508
|
+
const after = await this.snapshots.put({
|
|
509
|
+
revision: 1,
|
|
510
|
+
contentType: change.contentType,
|
|
511
|
+
bytes: change.afterBytes,
|
|
512
|
+
});
|
|
513
|
+
if (before.sha256 === after.sha256) {
|
|
514
|
+
this.snapshots.release(before);
|
|
515
|
+
this.snapshots.release(after);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
changes.push({ resource: change.resource, before, after });
|
|
519
|
+
}
|
|
520
|
+
if (changes.length === 0) return false;
|
|
521
|
+
this.appendAppliedTransaction(options, changes);
|
|
522
|
+
return true;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
this.releaseChanges(changes);
|
|
525
|
+
this.lastErrorValue =
|
|
526
|
+
error instanceof HistoryOperationError
|
|
527
|
+
? error.historyError
|
|
528
|
+
: historyError(
|
|
529
|
+
'apply-failed',
|
|
530
|
+
`Failed to journal “${options.label.trim()}”.`,
|
|
531
|
+
applied.map((change) => change.resource),
|
|
532
|
+
error,
|
|
533
|
+
);
|
|
534
|
+
this.notify();
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Preflight for producers that mutate state before journaling it. */
|
|
541
|
+
assertCanRecordAppliedChange(): void {
|
|
542
|
+
this.assertNotDisposed();
|
|
543
|
+
if (this.blockedValue) {
|
|
544
|
+
if (this.lastErrorValue?.code === 'compensation-failed') {
|
|
545
|
+
throw new HistoryOperationError(this.lastErrorValue);
|
|
546
|
+
}
|
|
547
|
+
throw new HistoryOperationError(
|
|
548
|
+
historyError('compensation-failed', 'History is blocked pending recovery.', []),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Surface a recovered producer failure without blocking later safe edits. */
|
|
554
|
+
reportAppliedChangeFailure(
|
|
555
|
+
label: string,
|
|
556
|
+
resources: readonly ResourceKey[],
|
|
557
|
+
cause: unknown,
|
|
558
|
+
): void {
|
|
559
|
+
if (this.blockedValue) return;
|
|
560
|
+
this.lastErrorValue = historyError(
|
|
561
|
+
'apply-failed',
|
|
562
|
+
`Failed to apply “${label.trim()}”; prior state was restored.`,
|
|
563
|
+
resources,
|
|
564
|
+
cause,
|
|
565
|
+
);
|
|
566
|
+
this.notify();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Permanently blocks history after an already-applied producer could not
|
|
571
|
+
* restore exact prior bytes. Producers outside HistoryService's own apply
|
|
572
|
+
* loop use this to surface the same loud recovery state as undo/redo.
|
|
573
|
+
*/
|
|
574
|
+
blockForCompensationFailure(
|
|
575
|
+
label: string,
|
|
576
|
+
resources: readonly ResourceKey[],
|
|
577
|
+
cause: unknown,
|
|
578
|
+
): HistoryOperationError {
|
|
579
|
+
if (this.blockedValue && this.lastErrorValue?.code === 'compensation-failed') {
|
|
580
|
+
return new HistoryOperationError(this.lastErrorValue);
|
|
581
|
+
}
|
|
582
|
+
const problem = historyError(
|
|
583
|
+
'compensation-failed',
|
|
584
|
+
`History recovery failed while applying “${label.trim()}”.`,
|
|
585
|
+
resources,
|
|
586
|
+
cause,
|
|
587
|
+
);
|
|
588
|
+
this.blockedValue = true;
|
|
589
|
+
this.lastErrorValue = problem;
|
|
590
|
+
this.notify();
|
|
591
|
+
return new HistoryOperationError(problem);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
jumpTo(cursor: number): Promise<boolean> {
|
|
595
|
+
if (!Number.isInteger(cursor) || cursor < 0 || cursor > this.entries.length) {
|
|
596
|
+
return Promise.reject(new Error(`History cursor ${cursor} is out of range.`));
|
|
597
|
+
}
|
|
598
|
+
return this.enqueue(async () => {
|
|
599
|
+
while (this.cursorValue > cursor) {
|
|
600
|
+
if (!(await this.undoNow())) return false;
|
|
601
|
+
}
|
|
602
|
+
while (this.cursorValue < cursor) {
|
|
603
|
+
if (!(await this.redoNow())) return false;
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
expireSession(sessionId: string): void {
|
|
610
|
+
if (this.disposeRequested) return;
|
|
611
|
+
const resources = new Set(this.registry.expireSession(sessionId));
|
|
612
|
+
if (resources.size === 0) return;
|
|
613
|
+
this.entries = this.entries.map((entry) =>
|
|
614
|
+
entry.sessionId === sessionId ||
|
|
615
|
+
entry.changes.some((change) => resources.has(change.resource))
|
|
616
|
+
? { ...entry, status: 'expired', error: null }
|
|
617
|
+
: entry,
|
|
618
|
+
);
|
|
619
|
+
this.notify();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
expireResources(resources: Iterable<ResourceKey>): void {
|
|
623
|
+
if (this.disposeRequested) return;
|
|
624
|
+
const expired = new Set(this.registry.expireResources(resources));
|
|
625
|
+
if (expired.size === 0) return;
|
|
626
|
+
this.entries = this.entries.map((entry) =>
|
|
627
|
+
entry.changes.some((change) => expired.has(change.resource))
|
|
628
|
+
? { ...entry, status: 'expired', error: null }
|
|
629
|
+
: entry,
|
|
630
|
+
);
|
|
631
|
+
this.notify();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
dispose(): void {
|
|
635
|
+
if (this.disposeRequested) return;
|
|
636
|
+
this.disposeRequested = true;
|
|
637
|
+
this.listeners.clear();
|
|
638
|
+
this.commitListeners.clear();
|
|
639
|
+
this.finishDisposeIfIdle();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private finishDisposeIfIdle(): void {
|
|
643
|
+
if (
|
|
644
|
+
!this.disposeRequested ||
|
|
645
|
+
this.disposeComplete ||
|
|
646
|
+
this.activeTransaction ||
|
|
647
|
+
this.pendingExecutions > 0 ||
|
|
648
|
+
this.pendingProducers > 0
|
|
649
|
+
) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
for (const entry of this.entries) this.releaseChanges(entry.changes);
|
|
653
|
+
this.entries = [];
|
|
654
|
+
this.cursorValue = 0;
|
|
655
|
+
this.drivers.clear();
|
|
656
|
+
this.registry.clear();
|
|
657
|
+
this.snapshots.clear();
|
|
658
|
+
this.blockedValue = false;
|
|
659
|
+
this.lastErrorValue = null;
|
|
660
|
+
this.limitWarningValue = null;
|
|
661
|
+
this.lastEvictionValue = null;
|
|
662
|
+
this.droppedTotal = 0;
|
|
663
|
+
this.disposeComplete = true;
|
|
664
|
+
this.notify();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private async finishStaging(staged: Map<ResourceKey, StagedResource>): Promise<ResourceChange[]> {
|
|
668
|
+
const changes: ResourceChange[] = [];
|
|
669
|
+
try {
|
|
670
|
+
for (const [resource, state] of staged) {
|
|
671
|
+
const after = await this.snapshots.put({
|
|
672
|
+
revision: state.beforeSnapshot.revision + 1,
|
|
673
|
+
contentType: state.afterContentType,
|
|
674
|
+
bytes: state.afterBytes,
|
|
675
|
+
});
|
|
676
|
+
if (after.sha256 === state.before.sha256) {
|
|
677
|
+
this.snapshots.release(after);
|
|
678
|
+
this.snapshots.release(state.before);
|
|
679
|
+
state.beforeOwned = false;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
changes.push({ resource, before: state.before, after });
|
|
683
|
+
state.beforeOwned = false;
|
|
684
|
+
}
|
|
685
|
+
return changes;
|
|
686
|
+
} catch (error) {
|
|
687
|
+
this.releaseChanges(changes);
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private async commitStagedTransaction(
|
|
693
|
+
options: TransactionOptions,
|
|
694
|
+
changes: readonly ResourceChange[],
|
|
695
|
+
): Promise<void> {
|
|
696
|
+
const byteSize = transactionBytes(changes);
|
|
697
|
+
if (byteSize > this.limits.maxTransactionBytes) {
|
|
698
|
+
this.releaseChanges(changes);
|
|
699
|
+
throw new HistoryOperationError(
|
|
700
|
+
historyError(
|
|
701
|
+
'history-limit',
|
|
702
|
+
`Transaction requires ${byteSize} bytes; the limit is ${this.limits.maxTransactionBytes}.`,
|
|
703
|
+
changes.map((change) => change.resource),
|
|
704
|
+
),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const transaction: MutableTransaction = {
|
|
709
|
+
id: this.makeId(),
|
|
710
|
+
label: options.label.trim(),
|
|
711
|
+
detail: options.detail?.trim() || null,
|
|
712
|
+
timestamp: this.now(),
|
|
713
|
+
scope: options.scope,
|
|
714
|
+
sessionId: options.scope === 'session' ? options.sessionId! : null,
|
|
715
|
+
changes,
|
|
716
|
+
byteSize,
|
|
717
|
+
mergeKey: options.mergeKey?.trim() || null,
|
|
718
|
+
status: 'committed',
|
|
719
|
+
error: null,
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
this.assertCanRecordAppliedChange();
|
|
724
|
+
await this.persistence.runMany(
|
|
725
|
+
changes.map((change) => change.resource),
|
|
726
|
+
async () => {
|
|
727
|
+
this.assertCanRecordAppliedChange();
|
|
728
|
+
await this.preflight(transaction, 'redo');
|
|
729
|
+
await this.applyWithCompensation(transaction, 'redo');
|
|
730
|
+
},
|
|
731
|
+
);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
const problem =
|
|
734
|
+
error instanceof HistoryOperationError
|
|
735
|
+
? error.historyError
|
|
736
|
+
: historyError(
|
|
737
|
+
'apply-failed',
|
|
738
|
+
`Failed to commit “${transaction.label}”.`,
|
|
739
|
+
changes.map((change) => change.resource),
|
|
740
|
+
error,
|
|
741
|
+
);
|
|
742
|
+
this.lastErrorValue = problem;
|
|
743
|
+
if (problem.code === 'compensation-failed') this.blockedValue = true;
|
|
744
|
+
this.releaseChanges(changes);
|
|
745
|
+
this.notify();
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
this.truncateRedo();
|
|
750
|
+
this.entries.push(transaction);
|
|
751
|
+
this.cursorValue = this.entries.length;
|
|
752
|
+
if (!this.blockedValue) this.lastErrorValue = null;
|
|
753
|
+
this.trimToLimits();
|
|
754
|
+
this.notify();
|
|
755
|
+
for (const listener of this.commitListeners) listener(transaction);
|
|
756
|
+
this.offerElement(transaction);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private appendAppliedTransaction(
|
|
760
|
+
options: TransactionOptions,
|
|
761
|
+
changes: readonly ResourceChange[],
|
|
762
|
+
): void {
|
|
763
|
+
const byteSize = transactionBytes(changes);
|
|
764
|
+
if (byteSize > this.limits.maxTransactionBytes) {
|
|
765
|
+
this.releaseChanges(changes);
|
|
766
|
+
throw new HistoryOperationError(
|
|
767
|
+
historyError(
|
|
768
|
+
'history-limit',
|
|
769
|
+
`Transaction requires ${byteSize} bytes; the limit is ${this.limits.maxTransactionBytes}.`,
|
|
770
|
+
changes.map((change) => change.resource),
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
const transaction: MutableTransaction = {
|
|
775
|
+
id: this.makeId(),
|
|
776
|
+
label: options.label.trim(),
|
|
777
|
+
detail: options.detail?.trim() || null,
|
|
778
|
+
timestamp: this.now(),
|
|
779
|
+
scope: options.scope,
|
|
780
|
+
sessionId: options.scope === 'session' ? options.sessionId! : null,
|
|
781
|
+
changes,
|
|
782
|
+
byteSize,
|
|
783
|
+
mergeKey: options.mergeKey?.trim() || null,
|
|
784
|
+
status: changes.every((change) => this.registry.isActive(change.resource))
|
|
785
|
+
? 'committed'
|
|
786
|
+
: 'expired',
|
|
787
|
+
error: null,
|
|
788
|
+
};
|
|
789
|
+
this.truncateRedo();
|
|
790
|
+
this.entries.push(transaction);
|
|
791
|
+
this.cursorValue = this.entries.length;
|
|
792
|
+
if (!this.blockedValue) this.lastErrorValue = null;
|
|
793
|
+
this.trimToLimits();
|
|
794
|
+
this.notify();
|
|
795
|
+
for (const listener of this.commitListeners) listener(transaction);
|
|
796
|
+
this.offerElement(transaction);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Offer a freshly committed transaction to whoever owns undo. A no-op under
|
|
801
|
+
* host ownership (`history-delegate.ts` drops it), so the standalone shape
|
|
802
|
+
* pays nothing for this seam.
|
|
803
|
+
*
|
|
804
|
+
* `resources` is the project-relative path per change, in the transaction's
|
|
805
|
+
* own order, taken from each resource's registered `project://<path>`
|
|
806
|
+
* location — that path is what lets the frame key the element on the SAME
|
|
807
|
+
* `file:` URI Monaco holds for the file. A SESSION-scoped resource (a live
|
|
808
|
+
* journal) has no such path and contributes none; a transaction made only of
|
|
809
|
+
* those arrives with an empty list, which is a fact for the frame to decide
|
|
810
|
+
* about rather than one this service may paper over.
|
|
811
|
+
*/
|
|
812
|
+
private offerElement(transaction: MutableTransaction): void {
|
|
813
|
+
if (!historyDelegateInstalled()) return;
|
|
814
|
+
const resources: string[] = [];
|
|
815
|
+
for (const change of transaction.changes) {
|
|
816
|
+
const location = this.registry.get(change.resource)?.location;
|
|
817
|
+
if (!location?.startsWith(PROJECT_LOCATION_PREFIX)) continue;
|
|
818
|
+
const path = location.slice(PROJECT_LOCATION_PREFIX.length);
|
|
819
|
+
if (!resources.includes(path)) resources.push(path);
|
|
820
|
+
}
|
|
821
|
+
emitHistoryElement({
|
|
822
|
+
id: transaction.id,
|
|
823
|
+
label: transaction.label,
|
|
824
|
+
resources,
|
|
825
|
+
undo: () => this.executeEntry(transaction.id, 'undo'),
|
|
826
|
+
redo: () => this.executeEntry(transaction.id, 'redo'),
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private async undoNow(): Promise<boolean> {
|
|
831
|
+
if (this.cursorValue === 0 || this.blockedValue) return false;
|
|
832
|
+
const transaction = this.entries[this.cursorValue - 1]!;
|
|
833
|
+
if (transaction.status === 'expired') {
|
|
834
|
+
const error = historyError(
|
|
835
|
+
'expired',
|
|
836
|
+
`“${transaction.label}” belongs to an expired session.`,
|
|
837
|
+
[],
|
|
838
|
+
);
|
|
839
|
+
transaction.error = error;
|
|
840
|
+
this.lastErrorValue = error;
|
|
841
|
+
this.notify();
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
return this.executeExisting(transaction, 'undo');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private async redoNow(): Promise<boolean> {
|
|
848
|
+
if (this.cursorValue >= this.entries.length || this.blockedValue) return false;
|
|
849
|
+
const transaction = this.entries[this.cursorValue]!;
|
|
850
|
+
if (transaction.status === 'expired') {
|
|
851
|
+
const error = historyError(
|
|
852
|
+
'expired',
|
|
853
|
+
`“${transaction.label}” belongs to an expired session.`,
|
|
854
|
+
[],
|
|
855
|
+
);
|
|
856
|
+
transaction.error = error;
|
|
857
|
+
this.lastErrorValue = error;
|
|
858
|
+
this.notify();
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
return this.executeExisting(transaction, 'redo');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private async executeExisting(
|
|
865
|
+
transaction: MutableTransaction,
|
|
866
|
+
direction: 'undo' | 'redo',
|
|
867
|
+
moveCursor = true,
|
|
868
|
+
): Promise<boolean> {
|
|
869
|
+
const resources = transaction.changes.map((change) => change.resource);
|
|
870
|
+
try {
|
|
871
|
+
await this.persistence.runMany(resources, async () => {
|
|
872
|
+
await this.preflight(transaction, direction);
|
|
873
|
+
transaction.status = direction === 'undo' ? 'undoing' : 'redoing';
|
|
874
|
+
transaction.error = null;
|
|
875
|
+
this.notify();
|
|
876
|
+
await this.applyWithCompensation(transaction, direction);
|
|
877
|
+
});
|
|
878
|
+
if (direction === 'undo') {
|
|
879
|
+
if (moveCursor) this.cursorValue--;
|
|
880
|
+
transaction.status = 'undone';
|
|
881
|
+
} else {
|
|
882
|
+
if (moveCursor) this.cursorValue++;
|
|
883
|
+
transaction.status = 'committed';
|
|
884
|
+
}
|
|
885
|
+
transaction.error = null;
|
|
886
|
+
this.lastErrorValue = null;
|
|
887
|
+
this.notify();
|
|
888
|
+
return true;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
const problem =
|
|
891
|
+
error instanceof HistoryOperationError
|
|
892
|
+
? error.historyError
|
|
893
|
+
: historyError(
|
|
894
|
+
'apply-failed',
|
|
895
|
+
`Failed to ${direction} “${transaction.label}”.`,
|
|
896
|
+
resources,
|
|
897
|
+
error,
|
|
898
|
+
);
|
|
899
|
+
transaction.error = problem;
|
|
900
|
+
transaction.status = problem.code === 'compensation-failed' ? 'failed-partial' : 'failed';
|
|
901
|
+
this.lastErrorValue = problem;
|
|
902
|
+
if (problem.code === 'compensation-failed') this.blockedValue = true;
|
|
903
|
+
this.notify();
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private async preflight(
|
|
909
|
+
transaction: MutableTransaction,
|
|
910
|
+
direction: 'undo' | 'redo',
|
|
911
|
+
): Promise<void> {
|
|
912
|
+
for (const change of transaction.changes) {
|
|
913
|
+
if (!this.registry.isActive(change.resource)) {
|
|
914
|
+
throw new HistoryOperationError(
|
|
915
|
+
historyError('expired', `Resource "${change.resource}" is unavailable.`, [
|
|
916
|
+
change.resource,
|
|
917
|
+
]),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
const expected = direction === 'undo' ? change.after : change.before;
|
|
921
|
+
const result = await this.requireDriver(change.resource).preflight(expected);
|
|
922
|
+
if (!result.ok) {
|
|
923
|
+
throw new HistoryOperationError(
|
|
924
|
+
historyError(
|
|
925
|
+
result.reason === 'expired' ? 'expired' : 'conflict',
|
|
926
|
+
`Resource "${change.resource}" no longer matches “${transaction.label}”.`,
|
|
927
|
+
[change.resource],
|
|
928
|
+
),
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private async applyWithCompensation(
|
|
935
|
+
transaction: MutableTransaction,
|
|
936
|
+
direction: 'undo' | 'redo',
|
|
937
|
+
): Promise<void> {
|
|
938
|
+
const ordered =
|
|
939
|
+
direction === 'undo' ? [...transaction.changes].reverse() : [...transaction.changes];
|
|
940
|
+
const applied: ResourceChange[] = [];
|
|
941
|
+
try {
|
|
942
|
+
for (const change of ordered) {
|
|
943
|
+
const target = direction === 'undo' ? change.before : change.after;
|
|
944
|
+
await this.requireDriver(change.resource).restore(target);
|
|
945
|
+
applied.push(change);
|
|
946
|
+
}
|
|
947
|
+
} catch (applyError) {
|
|
948
|
+
try {
|
|
949
|
+
for (const change of [...applied].reverse()) {
|
|
950
|
+
const compensation = direction === 'undo' ? change.after : change.before;
|
|
951
|
+
await this.requireDriver(change.resource).restore(compensation);
|
|
952
|
+
}
|
|
953
|
+
} catch (compensationError) {
|
|
954
|
+
throw new HistoryOperationError(
|
|
955
|
+
historyError(
|
|
956
|
+
'compensation-failed',
|
|
957
|
+
`History recovery failed while applying “${transaction.label}”.`,
|
|
958
|
+
transaction.changes.map((change) => change.resource),
|
|
959
|
+
{ applyError, compensationError },
|
|
960
|
+
),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
throw new HistoryOperationError(
|
|
964
|
+
historyError(
|
|
965
|
+
'apply-failed',
|
|
966
|
+
`Failed to apply “${transaction.label}”; prior state was restored.`,
|
|
967
|
+
transaction.changes.map((change) => change.resource),
|
|
968
|
+
applyError,
|
|
969
|
+
),
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private enqueue<T>(operation: () => Promise<T>): Promise<T> {
|
|
975
|
+
if (this.disposeRequested) return Promise.reject(new Error('History service is disposed.'));
|
|
976
|
+
this.pendingExecutions++;
|
|
977
|
+
this.notify();
|
|
978
|
+
const run = () => {
|
|
979
|
+
this.assertNotDisposed();
|
|
980
|
+
return operation();
|
|
981
|
+
};
|
|
982
|
+
const result = this.executionTail.then(run, run);
|
|
983
|
+
this.executionTail = result.then(
|
|
984
|
+
() => undefined,
|
|
985
|
+
() => undefined,
|
|
986
|
+
);
|
|
987
|
+
return result.finally(() => {
|
|
988
|
+
this.pendingExecutions--;
|
|
989
|
+
this.finishDisposeIfIdle();
|
|
990
|
+
if (!this.disposeComplete) this.notify();
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private async storeCapturedSnapshot(snapshot: ResourceSnapshot): Promise<SnapshotRef> {
|
|
995
|
+
const ref = await this.snapshots.put({
|
|
996
|
+
revision: snapshot.revision,
|
|
997
|
+
contentType: snapshot.contentType,
|
|
998
|
+
bytes: snapshot.bytes,
|
|
999
|
+
});
|
|
1000
|
+
if (ref.sha256 !== snapshot.sha256) {
|
|
1001
|
+
this.snapshots.release(ref);
|
|
1002
|
+
throw new Error('Resource driver returned bytes that do not match its SHA-256.');
|
|
1003
|
+
}
|
|
1004
|
+
return ref;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private truncateRedo(): void {
|
|
1008
|
+
if (this.cursorValue === this.entries.length) return;
|
|
1009
|
+
for (const entry of this.entries.slice(this.cursorValue)) this.releaseChanges(entry.changes);
|
|
1010
|
+
this.entries = this.entries.slice(0, this.cursorValue);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private trimToLimits(): void {
|
|
1014
|
+
// THE SERVICE CAPS NOTHING UNDER THE FRAME (ARCHITECTURE-CORE §The core is
|
|
1015
|
+
// Code-OSS). Dropping an entry the frame still holds an element for would
|
|
1016
|
+
// make that element's undo refuse for a reason the person never caused, so
|
|
1017
|
+
// eviction is the host shape's alone. The PRESSURE is still reported — the
|
|
1018
|
+
// limit warning below reaches the vgai console through
|
|
1019
|
+
// `history-limit-notices.ts` — because a standing warning naming its
|
|
1020
|
+
// mechanism is the doctrine, and a silent unbounded stack is not.
|
|
1021
|
+
if (historyDelegateInstalled()) {
|
|
1022
|
+
this.evaluateLimitWarning();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
let dropped = 0;
|
|
1026
|
+
let budget: HistoryBudget | null = null;
|
|
1027
|
+
while (this.entries.length > 1) {
|
|
1028
|
+
const overBytes = this.snapshots.uniqueByteLength > this.limits.maxBytes;
|
|
1029
|
+
const overCount = this.entries.length > this.limits.maxTransactions;
|
|
1030
|
+
if (!overBytes && !overCount) break;
|
|
1031
|
+
// The budget still binding on the final pass is the one that cost the user
|
|
1032
|
+
// this depth, so report that one rather than whichever tripped first.
|
|
1033
|
+
budget = overBytes ? 'bytes' : 'transactions';
|
|
1034
|
+
const [oldest, ...rest] = this.entries;
|
|
1035
|
+
this.entries = rest;
|
|
1036
|
+
this.releaseChanges(oldest!.changes);
|
|
1037
|
+
this.cursorValue = Math.max(0, this.cursorValue - 1);
|
|
1038
|
+
dropped++;
|
|
1039
|
+
}
|
|
1040
|
+
if (dropped > 0) {
|
|
1041
|
+
this.droppedTotal += dropped;
|
|
1042
|
+
this.lastEvictionValue = {
|
|
1043
|
+
budget: budget!,
|
|
1044
|
+
droppedCount: dropped,
|
|
1045
|
+
totalDropped: this.droppedTotal,
|
|
1046
|
+
// Deliberately free of the running total so repeats collapse in any
|
|
1047
|
+
// deduplicating log; the total stays available as a field.
|
|
1048
|
+
message: evictionMessage(budget!, dropped, this.limits),
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
this.evaluateLimitWarning();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Warns before depth is lost, naming the budget actually closest to binding.
|
|
1056
|
+
* Two budgets evict, and for snapshot-heavy work bytes bind long before the
|
|
1057
|
+
* step count does — a count-only warning would be wrong in exactly that case.
|
|
1058
|
+
*/
|
|
1059
|
+
private evaluateLimitWarning(): void {
|
|
1060
|
+
const transactionRatio = this.entries.length / this.limits.maxTransactions;
|
|
1061
|
+
const byteRatio = this.snapshots.uniqueByteLength / this.limits.maxBytes;
|
|
1062
|
+
if (Math.max(transactionRatio, byteRatio) < LIMIT_WARNING_THRESHOLD) {
|
|
1063
|
+
this.limitWarningValue = null;
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const budget: HistoryBudget = byteRatio > transactionRatio ? 'bytes' : 'transactions';
|
|
1067
|
+
// Re-issue only when the crossing is new, or when a different budget takes
|
|
1068
|
+
// over as the binding one — never once per commit while sitting over it.
|
|
1069
|
+
if (this.limitWarningValue?.budget === budget) return;
|
|
1070
|
+
const bytes = this.snapshots.uniqueByteLength;
|
|
1071
|
+
this.limitWarningValue = {
|
|
1072
|
+
budget,
|
|
1073
|
+
used: budget === 'bytes' ? bytes : this.entries.length,
|
|
1074
|
+
limit: budget === 'bytes' ? this.limits.maxBytes : this.limits.maxTransactions,
|
|
1075
|
+
message: limitWarningMessage(budget, this.entries.length, bytes, this.limits),
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private releaseChanges(changes: readonly ResourceChange[]): void {
|
|
1080
|
+
for (const change of changes) {
|
|
1081
|
+
if (this.snapshots.has(change.before)) this.snapshots.release(change.before);
|
|
1082
|
+
if (this.snapshots.has(change.after)) this.snapshots.release(change.after);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private requireDriver(resource: ResourceKey): ResourceDriver {
|
|
1087
|
+
const driver = this.drivers.get(resource);
|
|
1088
|
+
if (!driver) throw new Error(`No history resource driver is registered for "${resource}".`);
|
|
1089
|
+
return driver;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private validateOptions(options: TransactionOptions): void {
|
|
1093
|
+
if (!options.label.trim()) throw new Error('History transaction label cannot be empty.');
|
|
1094
|
+
if (options.resources.length === 0) throw new Error('History transaction needs a resource.');
|
|
1095
|
+
if (new Set(options.resources).size !== options.resources.length) {
|
|
1096
|
+
throw new Error('History transaction resources must be unique.');
|
|
1097
|
+
}
|
|
1098
|
+
if (options.scope === 'session' && !options.sessionId?.trim()) {
|
|
1099
|
+
throw new Error('A session transaction requires a session id.');
|
|
1100
|
+
}
|
|
1101
|
+
if (options.scope === 'project' && options.sessionId !== undefined) {
|
|
1102
|
+
throw new Error('A project transaction cannot have a session id.');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private assertCanStartTransaction(): void {
|
|
1107
|
+
this.assertNotDisposed();
|
|
1108
|
+
if (this.blockedValue) {
|
|
1109
|
+
throw new HistoryOperationError(
|
|
1110
|
+
historyError('compensation-failed', 'History is blocked pending recovery.', []),
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (this.activeTransaction) {
|
|
1114
|
+
throw new HistoryOperationError(
|
|
1115
|
+
historyError('busy', 'Another history operation is already in progress.', []),
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private assertNotDisposed(): void {
|
|
1121
|
+
if (this.disposeRequested) throw new Error('History service is disposed.');
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private validateResourceScope(driver: ResourceDriver, options: TransactionOptions): void {
|
|
1125
|
+
const descriptor = driver.descriptor;
|
|
1126
|
+
if (descriptor.scope !== options.scope) {
|
|
1127
|
+
throw new Error(`Resource "${descriptor.key}" does not match transaction scope.`);
|
|
1128
|
+
}
|
|
1129
|
+
if (options.scope === 'session' && descriptor.sessionId !== options.sessionId) {
|
|
1130
|
+
throw new Error(`Resource "${descriptor.key}" belongs to another session.`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private notify(): void {
|
|
1135
|
+
this.version++;
|
|
1136
|
+
this.publicSnapshot = this.buildSnapshot();
|
|
1137
|
+
for (const listener of this.listeners) listener();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private buildSnapshot(): HistorySnapshot {
|
|
1141
|
+
const undo = this.cursorValue > 0 ? this.entries[this.cursorValue - 1]! : null;
|
|
1142
|
+
const redo = this.cursorValue < this.entries.length ? this.entries[this.cursorValue]! : null;
|
|
1143
|
+
const idle =
|
|
1144
|
+
this.pendingExecutions === 0 && this.pendingProducers === 0 && !this.activeTransaction;
|
|
1145
|
+
return {
|
|
1146
|
+
version: this.version,
|
|
1147
|
+
transactions: this.entries.map((entry) => ({
|
|
1148
|
+
...entry,
|
|
1149
|
+
changes: entry.changes.map((change) => ({ ...change })),
|
|
1150
|
+
})),
|
|
1151
|
+
cursor: this.cursorValue,
|
|
1152
|
+
busy: !idle,
|
|
1153
|
+
blocked: this.blockedValue,
|
|
1154
|
+
// Under frame ownership this service's cursor is not undo, so the
|
|
1155
|
+
// question belongs to the frame: its delegate answers, and the editor's
|
|
1156
|
+
// own Edit menu is enabled exactly when the one stack has a step. The
|
|
1157
|
+
// cursor's own reachability would be the wrong answer in both
|
|
1158
|
+
// directions.
|
|
1159
|
+
canUndo: historyDelegateInstalled()
|
|
1160
|
+
? (historyDelegate()?.canUndo() ?? false)
|
|
1161
|
+
: idle && !!undo && undo.status !== 'expired' && !this.blockedValue,
|
|
1162
|
+
canRedo: historyDelegateInstalled()
|
|
1163
|
+
? (historyDelegate()?.canRedo() ?? false)
|
|
1164
|
+
: idle && !!redo && redo.status !== 'expired' && !this.blockedValue,
|
|
1165
|
+
undoLabel: undo?.label ?? null,
|
|
1166
|
+
redoLabel: redo?.label ?? null,
|
|
1167
|
+
uniqueSnapshotBytes: this.snapshots.uniqueByteLength,
|
|
1168
|
+
lastError: this.lastErrorValue,
|
|
1169
|
+
limitWarning: this.limitWarningValue,
|
|
1170
|
+
lastEviction: this.lastEvictionValue,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|