@forgeax/engine-app 0.1.2
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/LICENSE +202 -0
- package/README.md +418 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/app-rebuild.test.d.ts +2 -0
- package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
- package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-features.test.d.ts +2 -0
- package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
- package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-stop.test.d.ts +2 -0
- package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app.test-d.d.ts +2 -0
- package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
- package/dist/__tests__/create-app.test.d.ts +2 -0
- package/dist/__tests__/create-app.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +2 -0
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-import.unit.test.d.ts +2 -0
- package/dist/__tests__/ecs-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts +2 -0
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
- package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/remote-integration.test.d.ts +2 -0
- package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
- package/dist/__tests__/remote-serve-flag.test.d.ts +2 -0
- package/dist/__tests__/remote-serve-flag.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
- package/dist/__tests__/surface-handoff.test.d.ts +2 -0
- package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
- package/dist/__tests__/types-m2.test-d.d.ts +2 -0
- package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
- package/dist/animation-asset-lookup.d.ts +8 -0
- package/dist/animation-asset-lookup.d.ts.map +1 -0
- package/dist/assets-runtime-assembly.d.ts +19 -0
- package/dist/assets-runtime-assembly.d.ts.map +1 -0
- package/dist/canvas-policy.d.ts +4 -0
- package/dist/canvas-policy.d.ts.map +1 -0
- package/dist/create-app.d.ts +57 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/engine-worker-runtime.mjs +884 -0
- package/dist/engine-worker-runtime.mjs.map +1 -0
- package/dist/errors.d.ts +263 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/attached-world-swap.d.ts +21 -0
- package/dist/execution/attached-world-swap.d.ts.map +1 -0
- package/dist/execution/bootstrap-entry.d.ts +30 -0
- package/dist/execution/bootstrap-entry.d.ts.map +1 -0
- package/dist/execution/capabilities.d.ts +5 -0
- package/dist/execution/capabilities.d.ts.map +1 -0
- package/dist/execution/control.d.ts +14 -0
- package/dist/execution/control.d.ts.map +1 -0
- package/dist/execution/engine-worker-runtime.d.ts +2 -0
- package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
- package/dist/execution/engine-worker.d.ts +23 -0
- package/dist/execution/engine-worker.d.ts.map +1 -0
- package/dist/execution/host-controller.d.ts +16 -0
- package/dist/execution/host-controller.d.ts.map +1 -0
- package/dist/execution/index.d.ts +10 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/kernel-pool.d.ts +14 -0
- package/dist/execution/kernel-pool.d.ts.map +1 -0
- package/dist/execution/kernel-worker-runtime.d.ts +2 -0
- package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
- package/dist/execution/measurement.d.ts +7 -0
- package/dist/execution/measurement.d.ts.map +1 -0
- package/dist/execution/protocol.d.ts +84 -0
- package/dist/execution/protocol.d.ts.map +1 -0
- package/dist/execution/report.d.ts +5 -0
- package/dist/execution/report.d.ts.map +1 -0
- package/dist/execution/schema.d.ts +4 -0
- package/dist/execution/schema.d.ts.map +1 -0
- package/dist/execution/selector.d.ts +10 -0
- package/dist/execution/selector.d.ts.map +1 -0
- package/dist/execution/types.d.ts +107 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/fallback-camera.d.ts +8 -0
- package/dist/fallback-camera.d.ts.map +1 -0
- package/dist/game-context.d.ts +98 -0
- package/dist/game-context.d.ts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3985 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-map-plugin.d.ts +5 -0
- package/dist/input-map-plugin.d.ts.map +1 -0
- package/dist/input-plugin.d.ts +4 -0
- package/dist/input-plugin.d.ts.map +1 -0
- package/dist/internal/assembled-engine-profile.d.ts +5 -0
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
- package/dist/internal/assets-world-plugin.d.ts +21 -0
- package/dist/internal/assets-world-plugin.d.ts.map +1 -0
- package/dist/internal/browser-remote-bridge.d.ts +25 -0
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
- package/dist/internal/component-introspection.d.ts +24 -0
- package/dist/internal/component-introspection.d.ts.map +1 -0
- package/dist/internal/ecs-import.d.ts +6 -0
- package/dist/internal/ecs-import.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +9 -0
- package/dist/internal/engine-profile-common.d.ts.map +1 -0
- package/dist/internal/error-fanout.d.ts +66 -0
- package/dist/internal/error-fanout.d.ts.map +1 -0
- package/dist/internal/frame-loop.d.ts +35 -0
- package/dist/internal/frame-loop.d.ts.map +1 -0
- package/dist/internal/input-attach.d.ts +44 -0
- package/dist/internal/input-attach.d.ts.map +1 -0
- package/dist/internal/main-engine-profile.d.ts +15 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -0
- package/dist/internal/remote-serve-flag.d.ts +23 -0
- package/dist/internal/remote-serve-flag.d.ts.map +1 -0
- package/dist/internal/remote-server-plugin.d.ts +8 -0
- package/dist/internal/remote-server-plugin.d.ts.map +1 -0
- package/dist/internal/rhi-capture.d.ts +19 -0
- package/dist/internal/rhi-capture.d.ts.map +1 -0
- package/dist/internal/rhi-debug-flag.d.ts +18 -0
- package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
- package/dist/internal/worker-engine-profile.d.ts +13 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -0
- package/dist/kernel-worker-runtime.mjs +69 -0
- package/dist/kernel-worker-runtime.mjs.map +1 -0
- package/dist/load-game-errors.d.ts +131 -0
- package/dist/load-game-errors.d.ts.map +1 -0
- package/dist/load-game.d.ts +34 -0
- package/dist/load-game.d.ts.map +1 -0
- package/dist/plugin-factories.d.ts +3 -0
- package/dist/plugin-factories.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts +113 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -0
- package/dist/tool-preview/evidence.d.ts +20 -0
- package/dist/tool-preview/evidence.d.ts.map +1 -0
- package/dist/tool-preview/framing.d.ts +13 -0
- package/dist/tool-preview/framing.d.ts.map +1 -0
- package/dist/tool-preview/recipe.d.ts +54 -0
- package/dist/tool-preview/recipe.d.ts.map +1 -0
- package/dist/types.d.ts +444 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +86 -0
- package/schema/execution-report.schema.json +168 -0
- package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
- package/src/__tests__/app-rebuild.test.ts +31 -0
- package/src/__tests__/asset-registry-types.test-d.ts +16 -0
- package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
- package/src/__tests__/callback-deletion.unit.test.ts +14 -0
- package/src/__tests__/callback-inference.test-d.ts +32 -0
- package/src/__tests__/component-introspection.unit.test.ts +56 -0
- package/src/__tests__/consumer-migration.unit.test.ts +21 -0
- package/src/__tests__/create-app-features.test.ts +40 -0
- package/src/__tests__/create-app-listener-sync.test.ts +377 -0
- package/src/__tests__/create-app-lock-provider.test.ts +212 -0
- package/src/__tests__/create-app-plugin.test.ts +217 -0
- package/src/__tests__/create-app-stop.test.ts +617 -0
- package/src/__tests__/create-app-time.unit.test.ts +26 -0
- package/src/__tests__/create-app.test-d.ts +82 -0
- package/src/__tests__/create-app.test.ts +346 -0
- package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
- package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
- package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
- package/src/__tests__/ecs-import.unit.test.ts +40 -0
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
- package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -0
- package/src/__tests__/execution-error-detail.test-d.ts +13 -0
- package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
- package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
- package/src/__tests__/execution-frame-protocol.unit.test.ts +61 -0
- package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
- package/src/__tests__/execution-measurement.unit.test.ts +20 -0
- package/src/__tests__/execution-public-api.test-d.ts +10 -0
- package/src/__tests__/execution-public-api.unit.test.ts +9 -0
- package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
- package/src/__tests__/execution-report.contract.test.ts +50 -0
- package/src/__tests__/execution-report.types.test-d.ts +16 -0
- package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
- package/src/__tests__/execution-selector.unit.test.ts +72 -0
- package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
- package/src/__tests__/execution-world-health-owner.test-d.ts +9 -0
- package/src/__tests__/fallback-camera.unit.test.ts +32 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
- package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
- package/src/__tests__/frame-loop-world-array.test.ts +441 -0
- package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
- package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
- package/src/__tests__/plugin-policy.integration.test.ts +15 -0
- package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
- package/src/__tests__/profiler-default-off.test.ts +53 -0
- package/src/__tests__/profiler-frame-token.test.ts +248 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
- package/src/__tests__/profiler-phase-integration.test.ts +129 -0
- package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
- package/src/__tests__/remote-integration.test.ts +224 -0
- package/src/__tests__/remote-serve-flag.test.ts +24 -0
- package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
- package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
- package/src/__tests__/rhi-capture.browser.test.ts +51 -0
- package/src/__tests__/surface-handoff.test.ts +92 -0
- package/src/__tests__/types-m2.test-d.ts +81 -0
- package/src/__tests__/visibility-remote.integration.test.ts +119 -0
- package/src/__tests__/worker-execution-stop.test.ts +116 -0
- package/src/animation-asset-lookup.ts +13 -0
- package/src/assets-runtime-assembly.ts +70 -0
- package/src/canvas-policy.ts +16 -0
- package/src/create-app.ts +1351 -0
- package/src/errors.ts +454 -0
- package/src/execution/attached-world-swap.ts +67 -0
- package/src/execution/bootstrap-entry.ts +122 -0
- package/src/execution/capabilities.ts +102 -0
- package/src/execution/control.ts +52 -0
- package/src/execution/engine-worker-runtime.ts +400 -0
- package/src/execution/engine-worker.ts +161 -0
- package/src/execution/host-controller.ts +454 -0
- package/src/execution/index.ts +43 -0
- package/src/execution/kernel-pool.ts +236 -0
- package/src/execution/kernel-worker-runtime.ts +103 -0
- package/src/execution/measurement.ts +39 -0
- package/src/execution/protocol.ts +131 -0
- package/src/execution/report.ts +66 -0
- package/src/execution/schema.ts +168 -0
- package/src/execution/selector.ts +98 -0
- package/src/execution/types.ts +146 -0
- package/src/fallback-camera.ts +20 -0
- package/src/game-context.ts +126 -0
- package/src/index.ts +176 -0
- package/src/input-map-plugin.ts +21 -0
- package/src/input-plugin.ts +26 -0
- package/src/internal/assembled-engine-profile.ts +22 -0
- package/src/internal/assets-world-plugin.ts +79 -0
- package/src/internal/browser-remote-bridge.ts +267 -0
- package/src/internal/browser-rhi-debug-runtime.ts +67 -0
- package/src/internal/component-introspection.ts +116 -0
- package/src/internal/ecs-import.ts +41 -0
- package/src/internal/engine-profile-common.ts +10 -0
- package/src/internal/error-fanout.ts +129 -0
- package/src/internal/frame-loop.ts +525 -0
- package/src/internal/input-attach.ts +104 -0
- package/src/internal/main-engine-profile.ts +80 -0
- package/src/internal/remote-serve-flag.ts +50 -0
- package/src/internal/remote-server-plugin.ts +16 -0
- package/src/internal/rhi-capture.ts +211 -0
- package/src/internal/rhi-debug-flag.ts +40 -0
- package/src/internal/worker-engine-profile.ts +47 -0
- package/src/load-game-errors.ts +211 -0
- package/src/load-game.ts +115 -0
- package/src/plugin-factories.ts +2 -0
- package/src/tool-preview/bootstrap.ts +848 -0
- package/src/tool-preview/evidence.ts +59 -0
- package/src/tool-preview/framing.ts +52 -0
- package/src/tool-preview/recipe.ts +137 -0
- package/src/types.ts +505 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createMeasurementSeries } from '../execution/measurement';
|
|
3
|
+
|
|
4
|
+
describe('execution measurement series', () => {
|
|
5
|
+
it('reports bounded percentiles while retaining the total sample count', () => {
|
|
6
|
+
const series = createMeasurementSeries(3);
|
|
7
|
+
series.add(100);
|
|
8
|
+
series.add(2);
|
|
9
|
+
series.add(3);
|
|
10
|
+
const report = series.add(4);
|
|
11
|
+
expect(report).toEqual({ samples: 4, p50: 3, p95: 4, p99: 4, jitter: 1 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('clears samples for a rebuilt World identity', () => {
|
|
15
|
+
const series = createMeasurementSeries();
|
|
16
|
+
series.add(1);
|
|
17
|
+
series.clear();
|
|
18
|
+
expect(series.add(5).samples).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { App, CreateAppOptions, ExecutionControl, ExecutionReport } from '../index';
|
|
3
|
+
|
|
4
|
+
it('discovers execution from the public App surface', () => {
|
|
5
|
+
const app = null as unknown as App;
|
|
6
|
+
const options = null as unknown as CreateAppOptions;
|
|
7
|
+
expectTypeOf(app.execution).toEqualTypeOf<ExecutionControl>();
|
|
8
|
+
expectTypeOf(app.execution.report()).toEqualTypeOf<ExecutionReport>();
|
|
9
|
+
expectTypeOf(options.execution?.bootstrap).toEqualTypeOf<string | URL | undefined>();
|
|
10
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EXECUTION_REQUESTED_TIERS, EXECUTION_TIERS } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('execution public affordance', () => {
|
|
5
|
+
it('advertises only stable tier names', () => {
|
|
6
|
+
expect(EXECUTION_TIERS).toEqual(['main-serial', 'engine-worker', 'shared']);
|
|
7
|
+
expect(EXECUTION_REQUESTED_TIERS).toEqual(['auto', ...EXECUTION_TIERS]);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createExecutionReport, unavailableExecutionCapabilities } from '../execution';
|
|
3
|
+
import { createLocalExecutionControl } from '../execution/control';
|
|
4
|
+
|
|
5
|
+
describe('execution rebuild contract', () => {
|
|
6
|
+
it('keeps low-level local assembly explicit when no bootstrap exists', async () => {
|
|
7
|
+
const control = createLocalExecutionControl(
|
|
8
|
+
createExecutionReport('main-serial', unavailableExecutionCapabilities('local')),
|
|
9
|
+
);
|
|
10
|
+
const before = control.report();
|
|
11
|
+
const result = await control.rebuild();
|
|
12
|
+
expect(result.ok).toBe(false);
|
|
13
|
+
if (!result.ok) expect(result.error.code).toBe('app-execution-rebuild-failed');
|
|
14
|
+
expect(control.report()).toEqual(before);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('projects live local lifecycle and World state without a duplicate ledger', () => {
|
|
18
|
+
let world: ReturnType<typeof createExecutionReport>['world'] = {
|
|
19
|
+
identity: 'world-1',
|
|
20
|
+
health: 'healthy',
|
|
21
|
+
partialWrite: false,
|
|
22
|
+
retryable: true,
|
|
23
|
+
};
|
|
24
|
+
const control = createLocalExecutionControl(
|
|
25
|
+
createExecutionReport('main-serial', unavailableExecutionCapabilities('local')),
|
|
26
|
+
{ world: () => world },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
control.setEngineHealth('running');
|
|
30
|
+
expect(control.report().engine.health).toBe('running');
|
|
31
|
+
world = { ...world, health: 'poisoned', partialWrite: true, retryable: false };
|
|
32
|
+
expect(control.report().world).toEqual(world);
|
|
33
|
+
control.setEngineHealth('stopped');
|
|
34
|
+
expect(control.report().engine.health).toBe('stopped');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createExecutionReport,
|
|
4
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
5
|
+
type ExecutionCapabilities,
|
|
6
|
+
isExecutionReport,
|
|
7
|
+
selectExecutionTier,
|
|
8
|
+
} from '../index';
|
|
9
|
+
|
|
10
|
+
function capabilities(available = true): ExecutionCapabilities {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
EXECUTION_CAPABILITY_NAMES.map((name) => [
|
|
13
|
+
name,
|
|
14
|
+
{ available, reason: available ? 'observed' : 'missing' },
|
|
15
|
+
]),
|
|
16
|
+
) as unknown as ExecutionCapabilities;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ExecutionReport contract', () => {
|
|
20
|
+
it('is the closed requested/actual/capability/health/performance/fault snapshot', () => {
|
|
21
|
+
const selection = selectExecutionTier({
|
|
22
|
+
requestedTier: 'auto',
|
|
23
|
+
capabilities: capabilities(),
|
|
24
|
+
sharedEvidencePassed: true,
|
|
25
|
+
}).unwrap();
|
|
26
|
+
const report = createExecutionReport('auto', capabilities(), selection);
|
|
27
|
+
expect(report.requestedTier).toBe('auto');
|
|
28
|
+
expect(report.actualTier).toBe('shared');
|
|
29
|
+
expect(report.capabilities.worker.available).toBe(true);
|
|
30
|
+
expect(report.engine.health).toBe('idle');
|
|
31
|
+
expect(report.world.health).toBe('healthy');
|
|
32
|
+
expect(report.kernelDispatch.reason).toBe('no-eligible-kernel');
|
|
33
|
+
expect(report.performance.kernelWaitMs).toBeNull();
|
|
34
|
+
expect(report.audio).toEqual({
|
|
35
|
+
owner: 'host',
|
|
36
|
+
contextState: 'suspended',
|
|
37
|
+
activeSourceCount: 0,
|
|
38
|
+
lastError: null,
|
|
39
|
+
});
|
|
40
|
+
expect(report.fault).toBeNull();
|
|
41
|
+
expect(isExecutionReport(report)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects missing and extra schema fields', () => {
|
|
45
|
+
const report = createExecutionReport('main-serial', capabilities(false));
|
|
46
|
+
const { world: _world, ...missing } = report;
|
|
47
|
+
expect(isExecutionReport(missing)).toBe(false);
|
|
48
|
+
expect(isExecutionReport({ ...report, workerId: 1 })).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { ExecutionReport, ExecutionTier } from '../index';
|
|
3
|
+
|
|
4
|
+
it('exposes stable report fields without transport internals', () => {
|
|
5
|
+
const report = null as unknown as ExecutionReport;
|
|
6
|
+
expectTypeOf(report.actualTier).toEqualTypeOf<ExecutionTier | null>();
|
|
7
|
+
expectTypeOf(report.world.partialWrite).toBeBoolean();
|
|
8
|
+
expectTypeOf(report.world.retryable).toBeBoolean();
|
|
9
|
+
expectTypeOf(report.kernelDispatch.dispatched).toBeNumber();
|
|
10
|
+
expectTypeOf(report.performance.hostFrameMs).toMatchTypeOf<object | null>();
|
|
11
|
+
expectTypeOf(report.audio.owner).toEqualTypeOf<'host'>();
|
|
12
|
+
// @ts-expect-error transport identities are private
|
|
13
|
+
void report.workerId;
|
|
14
|
+
// @ts-expect-error SAB shards are private
|
|
15
|
+
void report.shards;
|
|
16
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
5
|
+
EXECUTION_REPORT_SCHEMA_VERSION,
|
|
6
|
+
EXECUTION_REQUESTED_TIERS,
|
|
7
|
+
EXECUTION_TIERS,
|
|
8
|
+
} from '../index';
|
|
9
|
+
|
|
10
|
+
describe('execution schema freshness', () => {
|
|
11
|
+
it('keeps runtime catalogs aligned with the JSON schema authority', () => {
|
|
12
|
+
const schema = JSON.parse(
|
|
13
|
+
readFileSync(new URL('../../schema/execution-report.schema.json', import.meta.url), 'utf8'),
|
|
14
|
+
) as { properties: Record<string, { const?: number; enum?: unknown[]; required?: string[] }> };
|
|
15
|
+
expect(schema.properties.schemaVersion?.const).toBe(EXECUTION_REPORT_SCHEMA_VERSION);
|
|
16
|
+
expect(schema.properties.requestedTier?.enum).toEqual(EXECUTION_REQUESTED_TIERS);
|
|
17
|
+
expect(schema.properties.actualTier?.enum).toEqual([null, ...EXECUTION_TIERS]);
|
|
18
|
+
expect(schema.properties.capabilities?.required).toEqual(EXECUTION_CAPABILITY_NAMES);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
4
|
+
type ExecutionCapabilities,
|
|
5
|
+
type ExecutionCapabilityName,
|
|
6
|
+
selectExecutionTier,
|
|
7
|
+
} from '../index';
|
|
8
|
+
|
|
9
|
+
function facts(missing: readonly ExecutionCapabilityName[] = []): ExecutionCapabilities {
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
EXECUTION_CAPABILITY_NAMES.map((name) => [
|
|
12
|
+
name,
|
|
13
|
+
{
|
|
14
|
+
available: !missing.includes(name),
|
|
15
|
+
reason: missing.includes(name) ? 'fixture missing' : 'fixture observed',
|
|
16
|
+
},
|
|
17
|
+
]),
|
|
18
|
+
) as unknown as ExecutionCapabilities;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('execution tier selector', () => {
|
|
22
|
+
it('orders auto shared to engine-worker to main-serial', () => {
|
|
23
|
+
expect(
|
|
24
|
+
selectExecutionTier({
|
|
25
|
+
requestedTier: 'auto',
|
|
26
|
+
capabilities: facts(),
|
|
27
|
+
sharedEvidencePassed: true,
|
|
28
|
+
}).unwrap().actualTier,
|
|
29
|
+
).toBe('shared');
|
|
30
|
+
expect(
|
|
31
|
+
selectExecutionTier({
|
|
32
|
+
requestedTier: 'auto',
|
|
33
|
+
capabilities: facts(['sharedArrayBuffer']),
|
|
34
|
+
sharedEvidencePassed: true,
|
|
35
|
+
}).unwrap().actualTier,
|
|
36
|
+
).toBe('engine-worker');
|
|
37
|
+
expect(
|
|
38
|
+
selectExecutionTier({
|
|
39
|
+
requestedTier: 'auto',
|
|
40
|
+
capabilities: facts(['workerWebGpu']),
|
|
41
|
+
sharedEvidencePassed: true,
|
|
42
|
+
}).unwrap().actualTier,
|
|
43
|
+
).toBe('main-serial');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not conflate failed shipped evidence with a missing browser capability', () => {
|
|
47
|
+
const result = selectExecutionTier({
|
|
48
|
+
requestedTier: 'shared',
|
|
49
|
+
capabilities: facts(),
|
|
50
|
+
sharedEvidencePassed: false,
|
|
51
|
+
});
|
|
52
|
+
expect(result.ok).toBe(false);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
if (result.error.code !== 'app-execution-tier-unavailable') throw result.error;
|
|
55
|
+
expect(result.error.detail.missingCapabilities).toEqual([]);
|
|
56
|
+
expect(result.error.detail.sharedEvidencePassed).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('never silently downgrades an explicit tier', () => {
|
|
61
|
+
const result = selectExecutionTier({
|
|
62
|
+
requestedTier: 'engine-worker',
|
|
63
|
+
capabilities: facts(['offscreenCanvas']),
|
|
64
|
+
sharedEvidencePassed: true,
|
|
65
|
+
});
|
|
66
|
+
expect(result.ok).toBe(false);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
if (result.error.code !== 'app-execution-tier-unavailable') throw result.error;
|
|
69
|
+
expect(result.error.detail.missingCapabilities).toEqual(['offscreenCanvas']);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { defineComponent, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { defineSharedKernel, type KernelDispatchSpan } from '@forgeax/engine-ecs/shared';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { createKernelPool } from '../execution/kernel-pool';
|
|
5
|
+
|
|
6
|
+
const Position = defineComponent('ExecutionKernelPosition', { x: 'f32' });
|
|
7
|
+
|
|
8
|
+
function runPositionKernel(spans: readonly import('@forgeax/engine-ecs').QuerySpan[]): void {
|
|
9
|
+
for (const span of spans) {
|
|
10
|
+
const position = span.mut(Position);
|
|
11
|
+
for (let index = 0; index < span.length; index += 1) {
|
|
12
|
+
position.x[index] = (position.x[index] ?? 0) + 2;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const kernel = defineSharedKernel('https://example.test/position-kernel.mjs', {
|
|
18
|
+
name: 'execution-position-kernel',
|
|
19
|
+
queries: [{ write: [Position] }],
|
|
20
|
+
minimumRows: 1,
|
|
21
|
+
run: runPositionKernel,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
interface TestMessage {
|
|
25
|
+
readonly kind: 'kernel-init' | 'kernel-preload' | 'kernel-job';
|
|
26
|
+
readonly ready?: Int32Array;
|
|
27
|
+
readonly control?: Int32Array;
|
|
28
|
+
readonly status?: Int32Array;
|
|
29
|
+
readonly jobIndex?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TestJob {
|
|
33
|
+
readonly kind: 'kernel-job';
|
|
34
|
+
readonly binding: {
|
|
35
|
+
readonly length: number;
|
|
36
|
+
readonly write: Readonly<Record<string, Readonly<Record<string, Float32Array>>>>;
|
|
37
|
+
};
|
|
38
|
+
readonly control: Int32Array;
|
|
39
|
+
readonly status: Int32Array;
|
|
40
|
+
readonly jobIndex: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sharedDispatchSpans(): {
|
|
44
|
+
readonly world: World;
|
|
45
|
+
readonly spans: readonly KernelDispatchSpan[];
|
|
46
|
+
} {
|
|
47
|
+
const world = new World({ storage: 'shared' });
|
|
48
|
+
for (let index = 0; index < 8; index += 1) {
|
|
49
|
+
world.spawn({ component: Position, data: { x: index } }).unwrap();
|
|
50
|
+
}
|
|
51
|
+
const query = world.query({ write: [Position] }).unwrap();
|
|
52
|
+
return {
|
|
53
|
+
world,
|
|
54
|
+
spans: [...query.spans().unwrap()].map((span) => ({ queryIndex: 0, span })),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function workerFactory(run: (job: TestJob) => void, preloadStatus = 1): () => Worker {
|
|
59
|
+
return () =>
|
|
60
|
+
({
|
|
61
|
+
postMessage(message: TestMessage): void {
|
|
62
|
+
if (message.kind === 'kernel-init' && message.ready !== undefined) {
|
|
63
|
+
Atomics.add(message.ready, 0, 1);
|
|
64
|
+
Atomics.notify(message.ready, 0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
message.kind === 'kernel-preload' &&
|
|
69
|
+
message.control !== undefined &&
|
|
70
|
+
message.status !== undefined &&
|
|
71
|
+
message.jobIndex !== undefined
|
|
72
|
+
) {
|
|
73
|
+
Atomics.store(message.status, message.jobIndex, preloadStatus);
|
|
74
|
+
Atomics.add(message.control, 0, 1);
|
|
75
|
+
Atomics.notify(message.control, 0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
run(message as TestJob);
|
|
79
|
+
Atomics.add(message.control as Int32Array, 0, 1);
|
|
80
|
+
Atomics.notify(message.control as Int32Array, 0);
|
|
81
|
+
},
|
|
82
|
+
terminate(): void {},
|
|
83
|
+
}) as unknown as Worker;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('shared kernel pool', () => {
|
|
87
|
+
it('shards SAB spans without copying and joins all lanes', () => {
|
|
88
|
+
const { world, spans } = sharedDispatchSpans();
|
|
89
|
+
const pool = createKernelPool({
|
|
90
|
+
lanes: 2,
|
|
91
|
+
workerFactory: workerFactory((job) => {
|
|
92
|
+
const x = job.binding.write.ExecutionKernelPosition?.x;
|
|
93
|
+
if (x === undefined) throw new Error('position binding missing');
|
|
94
|
+
for (let index = 0; index < job.binding.length; index += 1) {
|
|
95
|
+
x[index] = (x[index] ?? 0) + 2;
|
|
96
|
+
}
|
|
97
|
+
Atomics.store(job.status, job.jobIndex, 1);
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
pool.warmup?.(kernel);
|
|
101
|
+
|
|
102
|
+
const result = pool.execute(kernel, spans);
|
|
103
|
+
expect(result).toMatchObject({ mode: 'shared', dispatched: 2, completed: 2 });
|
|
104
|
+
expect(pool.takeLastDispatch()).toEqual(result);
|
|
105
|
+
expect(pool.takeLastDispatch()).toBeNull();
|
|
106
|
+
expect(
|
|
107
|
+
Array.from(world.query({ read: [Position] }).unwrap(), (row) => row.get(Position).x),
|
|
108
|
+
).toEqual([2, 3, 4, 5, 6, 7, 8, 9]);
|
|
109
|
+
pool.dispose();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reports a partial write when one lane fails after another completes', () => {
|
|
113
|
+
const { spans } = sharedDispatchSpans();
|
|
114
|
+
const pool = createKernelPool({
|
|
115
|
+
lanes: 2,
|
|
116
|
+
workerFactory: workerFactory((job) => {
|
|
117
|
+
Atomics.store(job.status, job.jobIndex, job.jobIndex === 0 ? 1 : -1);
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
pool.warmup?.(kernel);
|
|
121
|
+
|
|
122
|
+
expect(pool.execute(kernel, spans)).toMatchObject({
|
|
123
|
+
dispatched: 2,
|
|
124
|
+
completed: 1,
|
|
125
|
+
partialWrite: true,
|
|
126
|
+
});
|
|
127
|
+
pool.dispose();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('fails a non-responsive lane at the bounded Atomics deadline', () => {
|
|
131
|
+
const { spans } = sharedDispatchSpans();
|
|
132
|
+
const pool = createKernelPool({
|
|
133
|
+
lanes: 1,
|
|
134
|
+
timeoutMs: 1,
|
|
135
|
+
workerFactory: () =>
|
|
136
|
+
({
|
|
137
|
+
postMessage(message: TestMessage): void {
|
|
138
|
+
if (message.kind === 'kernel-init' && message.ready !== undefined) {
|
|
139
|
+
Atomics.add(message.ready, 0, 1);
|
|
140
|
+
Atomics.notify(message.ready, 0);
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
message.kind === 'kernel-preload' &&
|
|
144
|
+
message.control !== undefined &&
|
|
145
|
+
message.status !== undefined &&
|
|
146
|
+
message.jobIndex !== undefined
|
|
147
|
+
) {
|
|
148
|
+
Atomics.store(message.status, message.jobIndex, 1);
|
|
149
|
+
Atomics.add(message.control, 0, 1);
|
|
150
|
+
Atomics.notify(message.control, 0);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
terminate(): void {},
|
|
154
|
+
}) as unknown as Worker,
|
|
155
|
+
});
|
|
156
|
+
pool.warmup?.(kernel);
|
|
157
|
+
|
|
158
|
+
expect(pool.execute(kernel, spans)).toMatchObject({
|
|
159
|
+
dispatched: 1,
|
|
160
|
+
completed: 0,
|
|
161
|
+
partialWrite: true,
|
|
162
|
+
});
|
|
163
|
+
pool.dispose();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects an invalid module during readiness before any kernel dispatch', async () => {
|
|
167
|
+
let dispatched = false;
|
|
168
|
+
const pool = createKernelPool({
|
|
169
|
+
lanes: 1,
|
|
170
|
+
workerFactory: workerFactory(() => {
|
|
171
|
+
dispatched = true;
|
|
172
|
+
}, -1),
|
|
173
|
+
});
|
|
174
|
+
pool.warmup?.(kernel);
|
|
175
|
+
|
|
176
|
+
await expect(pool.ready()).rejects.toThrow('module preflight failed');
|
|
177
|
+
expect(dispatched).toBe(false);
|
|
178
|
+
pool.dispose();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorldExecutionHealth } from '@forgeax/engine-ecs/shared';
|
|
2
|
+
import { expectTypeOf, it } from 'vitest';
|
|
3
|
+
import type { ExecutionReport, ExecutionWorldHealth } from '../index';
|
|
4
|
+
|
|
5
|
+
it('projects the public App health type from the ECS World owner', () => {
|
|
6
|
+
expectTypeOf<ExecutionWorldHealth>().toEqualTypeOf<WorldExecutionHealth>();
|
|
7
|
+
expectTypeOf<WorldExecutionHealth>().toEqualTypeOf<ExecutionWorldHealth>();
|
|
8
|
+
expectTypeOf<ExecutionReport['world']['health']>().toEqualTypeOf<WorldExecutionHealth>();
|
|
9
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { Camera } from '@forgeax/engine-render';
|
|
3
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureFallbackCamera } from '../fallback-camera';
|
|
6
|
+
|
|
7
|
+
describe('ensureFallbackCamera', () => {
|
|
8
|
+
it('installs one neutral host fallback with the requested aspect', () => {
|
|
9
|
+
const world = new World();
|
|
10
|
+
const spawned = ensureFallbackCamera(world, 16 / 9);
|
|
11
|
+
expect(spawned).toBeDefined();
|
|
12
|
+
if (spawned === undefined) return;
|
|
13
|
+
expect(spawned.ok).toBe(true);
|
|
14
|
+
if (!spawned.ok) return;
|
|
15
|
+
|
|
16
|
+
expect(world.get(spawned.value, Transform).unwrap().pos).toEqual(new Float32Array([0, 0.6, 5]));
|
|
17
|
+
expect(world.get(spawned.value, Camera).unwrap().aspect).toBeCloseTo(16 / 9);
|
|
18
|
+
expect([...world.query({ with: [Camera] }).unwrap()]).toHaveLength(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('preserves an authored scene camera', () => {
|
|
22
|
+
const world = new World();
|
|
23
|
+
const authored = world.spawn(
|
|
24
|
+
{ component: Transform, data: { pos: [1, 2, 3] } },
|
|
25
|
+
{ component: Camera, data: {} },
|
|
26
|
+
);
|
|
27
|
+
expect(authored.ok).toBe(true);
|
|
28
|
+
|
|
29
|
+
expect(ensureFallbackCamera(world, 16 / 9)).toBeUndefined();
|
|
30
|
+
expect([...world.query({ with: [Camera] }).unwrap()]).toHaveLength(1);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// frame-loop-time-elapsed.test.ts — Time resource `elapsed` accumulator
|
|
2
|
+
// (solo bevy-examples round 20260713-212920).
|
|
3
|
+
//
|
|
4
|
+
// Regression guard for the friction that motivated the field: the Time resource
|
|
5
|
+
// was `{ delta }` only, so absolute-time-keyed behavior (pulsing, sin(elapsed))
|
|
6
|
+
// forced each system to hand-accumulate delta (drift + re-derivation). The frame-loop
|
|
7
|
+
// now writes `{ delta, elapsed }` where elapsed = Σ(clamped delta). These tests pin:
|
|
8
|
+
// 1. elapsed accumulates: after N frames of delta, elapsed ≈ N·delta,
|
|
9
|
+
// 2. elapsed is monotonic non-decreasing,
|
|
10
|
+
// 3. elapsed uses the CLAMPED delta (a huge raw gap advances elapsed by the ceiling,
|
|
11
|
+
// not the raw gap — no time jump),
|
|
12
|
+
// 4. delta is still present + correct (existing consumers unaffected).
|
|
13
|
+
|
|
14
|
+
import { Time, type TimeResource, World } from '@forgeax/engine-ecs';
|
|
15
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
import { createFrameLoop } from '../internal/frame-loop';
|
|
18
|
+
|
|
19
|
+
function makeSpyRenderer(): Renderer {
|
|
20
|
+
return {
|
|
21
|
+
backend: 'webgpu' as const,
|
|
22
|
+
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
23
|
+
attachWorld: () => ({ ok: true, value: undefined }),
|
|
24
|
+
detachWorld: () => {},
|
|
25
|
+
draw(): { ok: true; value: undefined } {
|
|
26
|
+
return { ok: true, value: undefined };
|
|
27
|
+
},
|
|
28
|
+
onError(): () => void {
|
|
29
|
+
return () => {};
|
|
30
|
+
},
|
|
31
|
+
dispose(): void {},
|
|
32
|
+
} as unknown as Renderer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Scheduler whose `now` advances by a fixed step per tick (deterministic delta).
|
|
36
|
+
function makeSyncScheduler(stepMs: number) {
|
|
37
|
+
let pending: ((t: number) => void) | null = null;
|
|
38
|
+
let clock = 0;
|
|
39
|
+
const raf = (cb: (t: number) => void): number => {
|
|
40
|
+
pending = cb;
|
|
41
|
+
return 1;
|
|
42
|
+
};
|
|
43
|
+
const caf = (): void => {
|
|
44
|
+
pending = null;
|
|
45
|
+
};
|
|
46
|
+
const now = (): number => {
|
|
47
|
+
clock += stepMs;
|
|
48
|
+
return clock;
|
|
49
|
+
};
|
|
50
|
+
const pump = (frames: number): void => {
|
|
51
|
+
for (let i = 0; i < frames; i++) {
|
|
52
|
+
const cb = pending;
|
|
53
|
+
pending = null;
|
|
54
|
+
if (cb === null) break;
|
|
55
|
+
cb(clock);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return { raf, caf, now, pump };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readTime(world: World): TimeResource {
|
|
62
|
+
return world.getResource(Time);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Time.elapsed — accumulation', () => {
|
|
66
|
+
it('after N frames of fixed delta, elapsed ≈ N·delta', () => {
|
|
67
|
+
const world = new World();
|
|
68
|
+
// 16 ms/tick → delta = 0.016 s (well under the default clamp ceiling).
|
|
69
|
+
const { raf, caf, now, pump } = makeSyncScheduler(16);
|
|
70
|
+
const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
|
|
71
|
+
loop.start();
|
|
72
|
+
pump(10);
|
|
73
|
+
const time = readTime(world);
|
|
74
|
+
expect(time.delta).toBeCloseTo(0.016, 6);
|
|
75
|
+
// 10 ticks, but start() sets the baseline at t=16 (first now()), so the first
|
|
76
|
+
// tick's delta is measured from there; elapsed is the running sum of clamped delta.
|
|
77
|
+
// Assert it equals delta * (number of ticks that ran) within a tick's slack.
|
|
78
|
+
expect(time.elapsed).toBeGreaterThan(0.1); // ~10 * 0.016 = 0.16
|
|
79
|
+
expect(time.elapsed).toBeLessThanOrEqual(0.016 * 10 + 1e-9);
|
|
80
|
+
loop.stop();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('elapsed is monotonic non-decreasing across frames', () => {
|
|
84
|
+
const world = new World();
|
|
85
|
+
const { raf, caf, now, pump } = makeSyncScheduler(16);
|
|
86
|
+
const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
|
|
87
|
+
loop.start();
|
|
88
|
+
let prev = -1;
|
|
89
|
+
for (let i = 0; i < 20; i++) {
|
|
90
|
+
pump(1);
|
|
91
|
+
const e = readTime(world).elapsed;
|
|
92
|
+
expect(e).toBeGreaterThanOrEqual(prev);
|
|
93
|
+
prev = e;
|
|
94
|
+
}
|
|
95
|
+
loop.stop();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('elapsed uses the CLAMPED delta — a huge raw frame gap does not jump elapsed', () => {
|
|
99
|
+
const world = new World();
|
|
100
|
+
// 5000 ms/tick → raw delta = 5 s, far above the default clamp ceiling. elapsed
|
|
101
|
+
// must advance by the CEILING per tick, not 5 s (no time jump — same clamp the
|
|
102
|
+
// delta field already applies).
|
|
103
|
+
const { raf, caf, now, pump } = makeSyncScheduler(5000);
|
|
104
|
+
const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
|
|
105
|
+
loop.start();
|
|
106
|
+
pump(3);
|
|
107
|
+
const time = readTime(world);
|
|
108
|
+
// delta is clamped, so a single frame's delta is well under the raw 5 s.
|
|
109
|
+
expect(time.delta).toBeLessThan(1);
|
|
110
|
+
// 3 clamped ticks → elapsed is a few * ceiling, nowhere near the raw 15 s.
|
|
111
|
+
expect(time.elapsed).toBeLessThan(3);
|
|
112
|
+
expect(time.elapsed).toBeCloseTo(time.delta * 3, 6);
|
|
113
|
+
loop.stop();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Time, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createFrameLoop } from '../internal/frame-loop';
|
|
6
|
+
|
|
7
|
+
function renderer(): Renderer {
|
|
8
|
+
return {
|
|
9
|
+
backend: 'webgpu',
|
|
10
|
+
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
11
|
+
attachWorld: () => ({ ok: true, value: undefined }),
|
|
12
|
+
detachWorld: () => {},
|
|
13
|
+
draw: () => ({ ok: true, value: undefined }),
|
|
14
|
+
onError: () => () => {},
|
|
15
|
+
onLost: () => () => {},
|
|
16
|
+
dispose: () => {},
|
|
17
|
+
} as unknown as Renderer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scheduler() {
|
|
21
|
+
let callback: ((timestamp: number) => void) | undefined;
|
|
22
|
+
return {
|
|
23
|
+
raf: (next: (timestamp: number) => void): number => {
|
|
24
|
+
callback = next;
|
|
25
|
+
return 1;
|
|
26
|
+
},
|
|
27
|
+
caf: (): void => {
|
|
28
|
+
callback = undefined;
|
|
29
|
+
},
|
|
30
|
+
tick(timestamp: number): void {
|
|
31
|
+
const next = callback;
|
|
32
|
+
callback = undefined;
|
|
33
|
+
next?.(timestamp);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('frame-loop Time forwarding', () => {
|
|
39
|
+
it('forwards one measured delta to own and injected Worlds', () => {
|
|
40
|
+
const own = new World();
|
|
41
|
+
const injected = new World();
|
|
42
|
+
const clock = scheduler();
|
|
43
|
+
const loop = createFrameLoop({
|
|
44
|
+
world: own,
|
|
45
|
+
renderer: renderer(),
|
|
46
|
+
now: (() => {
|
|
47
|
+
const values = [1000, 1016];
|
|
48
|
+
return () => values.shift() ?? 1016;
|
|
49
|
+
})(),
|
|
50
|
+
raf: clock.raf,
|
|
51
|
+
caf: clock.caf,
|
|
52
|
+
drawSource: () => ({ worlds: [own, injected], cameraOwner: 0, resourceOwner: 0 }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
loop.start().unwrap();
|
|
56
|
+
clock.tick(1016);
|
|
57
|
+
|
|
58
|
+
expect(own.getResource(Time).delta).toBeCloseTo(0.016);
|
|
59
|
+
expect(injected.getResource(Time).delta).toBeCloseTo(0.016);
|
|
60
|
+
});
|
|
61
|
+
});
|