@forgeax/engine-app 0.0.0-dev.8d955ade1c79

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.
Files changed (348) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +424 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
  7. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  9. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  11. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  13. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
  15. package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  17. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  18. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  19. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  31. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test.d.ts +2 -0
  33. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  34. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  35. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  39. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  41. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  44. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  45. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  47. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  49. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  50. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  51. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  53. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  55. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  59. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  61. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  65. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  67. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  69. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  71. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  72. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  73. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  74. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  75. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  77. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  79. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  81. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  83. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  84. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  85. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  87. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  88. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  89. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  91. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  93. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  94. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  95. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  97. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  98. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  99. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  100. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  101. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  102. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  103. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  106. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  107. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  108. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  109. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  111. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  113. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  114. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  115. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  116. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  117. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  118. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  119. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  120. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  121. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  122. package/dist/animation-asset-lookup.d.ts +7 -0
  123. package/dist/animation-asset-lookup.d.ts.map +1 -0
  124. package/dist/browser-frame-signal.d.ts +17 -0
  125. package/dist/browser-frame-signal.d.ts.map +1 -0
  126. package/dist/canvas-policy.d.ts +4 -0
  127. package/dist/canvas-policy.d.ts.map +1 -0
  128. package/dist/create-app.d.ts +78 -0
  129. package/dist/create-app.d.ts.map +1 -0
  130. package/dist/engine-worker-runtime.mjs +827 -0
  131. package/dist/engine-worker-runtime.mjs.map +1 -0
  132. package/dist/errors.d.ts +254 -0
  133. package/dist/errors.d.ts.map +1 -0
  134. package/dist/execution/attached-world-swap.d.ts +10 -0
  135. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  136. package/dist/execution/bootstrap-entry.d.ts +30 -0
  137. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  138. package/dist/execution/bootstrap-url.d.ts +5 -0
  139. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  140. package/dist/execution/capabilities.d.ts +5 -0
  141. package/dist/execution/capabilities.d.ts.map +1 -0
  142. package/dist/execution/control.d.ts +12 -0
  143. package/dist/execution/control.d.ts.map +1 -0
  144. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  145. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  146. package/dist/execution/engine-worker.d.ts +24 -0
  147. package/dist/execution/engine-worker.d.ts.map +1 -0
  148. package/dist/execution/host-controller.d.ts +16 -0
  149. package/dist/execution/host-controller.d.ts.map +1 -0
  150. package/dist/execution/index.d.ts +10 -0
  151. package/dist/execution/index.d.ts.map +1 -0
  152. package/dist/execution/kernel-pool.d.ts +14 -0
  153. package/dist/execution/kernel-pool.d.ts.map +1 -0
  154. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  155. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  156. package/dist/execution/measurement.d.ts +7 -0
  157. package/dist/execution/measurement.d.ts.map +1 -0
  158. package/dist/execution/protocol.d.ts +87 -0
  159. package/dist/execution/protocol.d.ts.map +1 -0
  160. package/dist/execution/report.d.ts +5 -0
  161. package/dist/execution/report.d.ts.map +1 -0
  162. package/dist/execution/schema.d.ts +4 -0
  163. package/dist/execution/schema.d.ts.map +1 -0
  164. package/dist/execution/selector.d.ts +10 -0
  165. package/dist/execution/selector.d.ts.map +1 -0
  166. package/dist/execution/types.d.ts +123 -0
  167. package/dist/execution/types.d.ts.map +1 -0
  168. package/dist/fallback-camera.d.ts +8 -0
  169. package/dist/fallback-camera.d.ts.map +1 -0
  170. package/dist/game-context.d.ts +98 -0
  171. package/dist/game-context.d.ts.map +1 -0
  172. package/dist/index.d.ts +27 -0
  173. package/dist/index.d.ts.map +1 -0
  174. package/dist/index.mjs +3980 -0
  175. package/dist/index.mjs.map +1 -0
  176. package/dist/input-map-plugin.d.ts +5 -0
  177. package/dist/input-map-plugin.d.ts.map +1 -0
  178. package/dist/input-plugin.d.ts +4 -0
  179. package/dist/input-plugin.d.ts.map +1 -0
  180. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  181. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  182. package/dist/internal/assets-world-plugin.d.ts +7 -0
  183. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  184. package/dist/internal/browser-remote-bridge.d.ts +24 -0
  185. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  186. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  187. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  188. package/dist/internal/component-introspection.d.ts +24 -0
  189. package/dist/internal/component-introspection.d.ts.map +1 -0
  190. package/dist/internal/debug-draw.d.ts +9 -0
  191. package/dist/internal/debug-draw.d.ts.map +1 -0
  192. package/dist/internal/ecs-import.d.ts +6 -0
  193. package/dist/internal/ecs-import.d.ts.map +1 -0
  194. package/dist/internal/engine-profile-common.d.ts +9 -0
  195. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  196. package/dist/internal/error-fanout.d.ts +66 -0
  197. package/dist/internal/error-fanout.d.ts.map +1 -0
  198. package/dist/internal/frame-loop.d.ts +38 -0
  199. package/dist/internal/frame-loop.d.ts.map +1 -0
  200. package/dist/internal/input-attach.d.ts +44 -0
  201. package/dist/internal/input-attach.d.ts.map +1 -0
  202. package/dist/internal/main-engine-profile.d.ts +19 -0
  203. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  204. package/dist/internal/remote-serve-flag.d.ts +19 -0
  205. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  206. package/dist/internal/remote-server-plugin.d.ts +8 -0
  207. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  208. package/dist/internal/rhi-capture.d.ts +19 -0
  209. package/dist/internal/rhi-capture.d.ts.map +1 -0
  210. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  211. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  212. package/dist/internal/worker-engine-profile.d.ts +15 -0
  213. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  214. package/dist/kernel-worker-runtime.mjs +69 -0
  215. package/dist/kernel-worker-runtime.mjs.map +1 -0
  216. package/dist/load-game-errors.d.ts +131 -0
  217. package/dist/load-game-errors.d.ts.map +1 -0
  218. package/dist/load-game.d.ts +34 -0
  219. package/dist/load-game.d.ts.map +1 -0
  220. package/dist/plugin-factories.d.ts +3 -0
  221. package/dist/plugin-factories.d.ts.map +1 -0
  222. package/dist/renderer-plugin.d.ts +25 -0
  223. package/dist/renderer-plugin.d.ts.map +1 -0
  224. package/dist/tool-preview/bootstrap.d.ts +113 -0
  225. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  226. package/dist/tool-preview/evidence.d.ts +20 -0
  227. package/dist/tool-preview/evidence.d.ts.map +1 -0
  228. package/dist/tool-preview/framing.d.ts +13 -0
  229. package/dist/tool-preview/framing.d.ts.map +1 -0
  230. package/dist/tool-preview/recipe.d.ts +54 -0
  231. package/dist/tool-preview/recipe.d.ts.map +1 -0
  232. package/dist/types.d.ts +404 -0
  233. package/dist/types.d.ts.map +1 -0
  234. package/package.json +85 -0
  235. package/schema/execution-report.schema.json +168 -0
  236. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  237. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  238. package/src/__tests__/callback-inference.test-d.ts +36 -0
  239. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  240. package/src/__tests__/consumer-migration.unit.test.ts +34 -0
  241. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  242. package/src/__tests__/create-app-features.test.ts +51 -0
  243. package/src/__tests__/create-app-listener-sync.test.ts +381 -0
  244. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  245. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  246. package/src/__tests__/create-app-plugin.test.ts +217 -0
  247. package/src/__tests__/create-app-stop.test.ts +609 -0
  248. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  249. package/src/__tests__/create-app.test-d.ts +83 -0
  250. package/src/__tests__/create-app.test.ts +287 -0
  251. package/src/__tests__/create-renderer-error.integration.test.ts +25 -0
  252. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +24 -0
  253. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  254. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +176 -0
  255. package/src/__tests__/draw-source-single-world-regression.test.ts +184 -0
  256. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +82 -0
  257. package/src/__tests__/engine-worker-world-swap.unit.test.ts +69 -0
  258. package/src/__tests__/errors-pointer-lock-failed.test.ts +265 -0
  259. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +79 -0
  260. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  261. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  262. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  263. package/src/__tests__/execution-frame-protocol.unit.test.ts +60 -0
  264. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  265. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  266. package/src/__tests__/execution-public-api.test-d.ts +14 -0
  267. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  268. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  269. package/src/__tests__/execution-report.contract.test.ts +50 -0
  270. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  271. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  272. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  273. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  274. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  275. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  276. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  277. package/src/__tests__/frame-loop-world-array.test.ts +450 -0
  278. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  279. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  280. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  281. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  282. package/src/__tests__/profiler-default-off.test.ts +53 -0
  283. package/src/__tests__/profiler-frame-token.test.ts +235 -0
  284. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  285. package/src/__tests__/profiler-phase-integration.test.ts +117 -0
  286. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  287. package/src/__tests__/remote-integration.test.ts +224 -0
  288. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  289. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  290. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  291. package/src/__tests__/surface-handoff.test.ts +89 -0
  292. package/src/__tests__/types-m2.test-d.ts +81 -0
  293. package/src/__tests__/visibility-remote.integration.test.ts +116 -0
  294. package/src/__tests__/worker-execution-stop.test.ts +173 -0
  295. package/src/animation-asset-lookup.ts +13 -0
  296. package/src/browser-frame-signal.ts +44 -0
  297. package/src/canvas-policy.ts +16 -0
  298. package/src/create-app.ts +1264 -0
  299. package/src/errors.ts +442 -0
  300. package/src/execution/attached-world-swap.ts +33 -0
  301. package/src/execution/bootstrap-entry.ts +122 -0
  302. package/src/execution/bootstrap-url.ts +25 -0
  303. package/src/execution/capabilities.ts +102 -0
  304. package/src/execution/control.ts +48 -0
  305. package/src/execution/engine-worker-runtime.ts +383 -0
  306. package/src/execution/engine-worker.ts +163 -0
  307. package/src/execution/host-controller.ts +460 -0
  308. package/src/execution/index.ts +44 -0
  309. package/src/execution/kernel-pool.ts +236 -0
  310. package/src/execution/kernel-worker-runtime.ts +103 -0
  311. package/src/execution/measurement.ts +39 -0
  312. package/src/execution/protocol.ts +134 -0
  313. package/src/execution/report.ts +66 -0
  314. package/src/execution/schema.ts +168 -0
  315. package/src/execution/selector.ts +98 -0
  316. package/src/execution/types.ts +163 -0
  317. package/src/fallback-camera.ts +20 -0
  318. package/src/game-context.ts +126 -0
  319. package/src/index.ts +190 -0
  320. package/src/input-map-plugin.ts +21 -0
  321. package/src/input-plugin.ts +26 -0
  322. package/src/internal/assembled-engine-profile.ts +14 -0
  323. package/src/internal/assets-world-plugin.ts +33 -0
  324. package/src/internal/browser-remote-bridge.ts +269 -0
  325. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  326. package/src/internal/component-introspection.ts +116 -0
  327. package/src/internal/debug-draw.ts +38 -0
  328. package/src/internal/ecs-import.ts +41 -0
  329. package/src/internal/engine-profile-common.ts +9 -0
  330. package/src/internal/error-fanout.ts +129 -0
  331. package/src/internal/frame-loop.ts +539 -0
  332. package/src/internal/input-attach.ts +104 -0
  333. package/src/internal/main-engine-profile.ts +84 -0
  334. package/src/internal/remote-serve-flag.ts +41 -0
  335. package/src/internal/remote-server-plugin.ts +16 -0
  336. package/src/internal/rhi-capture.ts +211 -0
  337. package/src/internal/rhi-debug-flag.ts +40 -0
  338. package/src/internal/worker-engine-profile.ts +47 -0
  339. package/src/load-game-errors.ts +211 -0
  340. package/src/load-game.ts +115 -0
  341. package/src/plugin-factories.ts +2 -0
  342. package/src/renderer-plugin.ts +96 -0
  343. package/src/tool-preview/bootstrap.ts +835 -0
  344. package/src/tool-preview/evidence.ts +59 -0
  345. package/src/tool-preview/framing.ts +52 -0
  346. package/src/tool-preview/recipe.ts +137 -0
  347. package/src/types.ts +450 -0
  348. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,190 @@
1
+ import type { AudioBackend } from '@forgeax/engine-audio';
2
+ import { createWorldContext, World } from '@forgeax/engine-ecs';
3
+ import { type InputBackend, ownedInputBackendPlugin } from '@forgeax/engine-input';
4
+ import { Context, type Plugin } from '@forgeax/engine-plugin';
5
+ import type { Renderer } from '@forgeax/engine-render';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { assembledEngineProfile } from '../internal/assembled-engine-profile';
8
+ import { mainEngineProfile } from '../internal/main-engine-profile';
9
+ import { remoteServerPlugin } from '../internal/remote-server-plugin';
10
+ import { workerEngineProfile } from '../internal/worker-engine-profile';
11
+ import { ownedRendererPlugin } from '../renderer-plugin';
12
+
13
+ interface PluginMetadata {
14
+ readonly name?: string;
15
+ readonly inject?: readonly string[] | Readonly<Record<string, unknown>>;
16
+ readonly provide?: string | readonly string[];
17
+ }
18
+
19
+ function metadata(plugin: Plugin): PluginMetadata {
20
+ return plugin as PluginMetadata;
21
+ }
22
+
23
+ function names(value: string | readonly string[] | undefined): readonly string[] {
24
+ if (value === undefined) return [];
25
+ return typeof value === 'string' ? [value] : value;
26
+ }
27
+
28
+ function injects(value: PluginMetadata['inject']): readonly string[] {
29
+ if (value === undefined) return [];
30
+ return Array.isArray(value) ? value : Object.keys(value);
31
+ }
32
+
33
+ function expectDag(profile: readonly Plugin[], roots: readonly string[]): void {
34
+ const providers = new Map<string, number>();
35
+ for (const root of roots) providers.set(root, -1);
36
+ for (const [index, plugin] of profile.entries()) {
37
+ for (const service of names(metadata(plugin).provide)) {
38
+ expect(providers.has(service), `duplicate provider for ${service}`).toBe(false);
39
+ providers.set(service, index);
40
+ }
41
+ }
42
+
43
+ const outgoing = profile.map(() => new Set<number>());
44
+ const indegree = profile.map(() => 0);
45
+ for (const [consumer, plugin] of profile.entries()) {
46
+ for (const service of injects(metadata(plugin).inject)) {
47
+ const provider = providers.get(service);
48
+ expect(
49
+ provider,
50
+ `${metadata(plugin).name ?? consumer} requires ${service}`,
51
+ ).not.toBeUndefined();
52
+ expect(
53
+ provider === consumer,
54
+ `${metadata(plugin).name ?? consumer} provides and requires ${service}`,
55
+ ).toBe(false);
56
+ if (provider === undefined || provider < 0) continue;
57
+ if (outgoing[provider]?.has(consumer)) continue;
58
+ outgoing[provider]?.add(consumer);
59
+ indegree[consumer] = (indegree[consumer] ?? 0) + 1;
60
+ }
61
+ }
62
+
63
+ const ready = indegree.flatMap((degree, index) => (degree === 0 ? [index] : []));
64
+ let visited = 0;
65
+ while (ready.length > 0) {
66
+ const current = ready.shift();
67
+ if (current === undefined) break;
68
+ visited += 1;
69
+ for (const consumer of outgoing[current] ?? []) {
70
+ indegree[consumer] = (indegree[consumer] ?? 0) - 1;
71
+ if (indegree[consumer] === 0) ready.push(consumer);
72
+ }
73
+ }
74
+ expect(visited, 'built-in profile dependency cycle').toBe(profile.length);
75
+ }
76
+
77
+ const renderer = { assets: {} } as unknown as Renderer;
78
+ const input = { sample: () => ({}) } as InputBackend;
79
+ const audio = {} as AudioBackend;
80
+ describe('Engine built-in plugin profiles', () => {
81
+ it('keeps browser-main capabilities in one acyclic declared graph', () => {
82
+ const profile = mainEngineProfile({
83
+ renderer,
84
+ input,
85
+ inputMap: [],
86
+ animationPayloads: () => undefined,
87
+ });
88
+ expect(profile.map((plugin) => metadata(plugin).name)).toEqual([
89
+ 'renderer',
90
+ 'render-components',
91
+ 'renderer-assets',
92
+ 'assets-world',
93
+ 'input-backend',
94
+ 'scene',
95
+ 'animation-payloads',
96
+ 'animation',
97
+ 'state',
98
+ 'input',
99
+ 'input-map',
100
+ ]);
101
+ expectDag(profile, ['world']);
102
+ });
103
+
104
+ it('keeps Worker and assembled selections acyclic without copying App wiring', () => {
105
+ expectDag(
106
+ workerEngineProfile({
107
+ renderer,
108
+ input,
109
+ audio,
110
+ animationPayloads: () => undefined,
111
+ }),
112
+ ['world'],
113
+ );
114
+ expectDag(assembledEngineProfile({ renderer }), ['world']);
115
+ });
116
+
117
+ it('closes an App-owned remote server with its Fiber', async () => {
118
+ let closeCount = 0;
119
+ const context = new Context();
120
+ await context.plugin(
121
+ remoteServerPlugin({
122
+ port: 5732,
123
+ async close() {
124
+ closeCount += 1;
125
+ },
126
+ }),
127
+ );
128
+ await context.fiber.dispose();
129
+ expect(closeCount).toBe(1);
130
+ });
131
+
132
+ it('releases an owned Renderer through its dependent Fiber', async () => {
133
+ const disposalOrder: string[] = [];
134
+ const ownedRenderer = {
135
+ dispose() {
136
+ disposalOrder.push('renderer');
137
+ },
138
+ } as Renderer;
139
+ const context = new Context();
140
+ await context.plugin(ownedRendererPlugin(ownedRenderer));
141
+ await context.plugin({
142
+ name: 'renderer-consumer',
143
+ inject: ['renderer'],
144
+ apply(ctx) {
145
+ ctx.effect(
146
+ () => () => {
147
+ disposalOrder.push('consumer');
148
+ },
149
+ 'test/renderer-consumer',
150
+ );
151
+ },
152
+ });
153
+ await context.fiber.dispose();
154
+ expect(disposalOrder).toEqual(['consumer', 'renderer']);
155
+ });
156
+
157
+ it('releases an App-acquired input backend through its provider Fiber', async () => {
158
+ let disposeCount = 0;
159
+ const context = new Context();
160
+ await context.plugin(
161
+ ownedInputBackendPlugin(input, () => {
162
+ disposeCount += 1;
163
+ }),
164
+ );
165
+ await context.fiber.dispose();
166
+ expect(disposeCount).toBe(1);
167
+ });
168
+
169
+ it('releases an owned Renderer once when later activation rolls back', async () => {
170
+ let disposeCount = 0;
171
+ const ownedRenderer = {
172
+ dispose() {
173
+ disposeCount += 1;
174
+ },
175
+ } as Renderer;
176
+
177
+ await expect(
178
+ createWorldContext(new World(), [
179
+ ownedRendererPlugin(ownedRenderer),
180
+ {
181
+ name: 'activation-failure',
182
+ apply() {
183
+ throw new Error('activation failed');
184
+ },
185
+ },
186
+ ]),
187
+ ).rejects.toThrow('activation failed');
188
+ expect(disposeCount).toBe(1);
189
+ });
190
+ });
@@ -0,0 +1,53 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { createProfiler } from '@forgeax/engine-profiler';
3
+ import type { Renderer } from '@forgeax/engine-render';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { createFrameLoop } from '../internal/frame-loop';
7
+
8
+ function makeRenderer(): Renderer {
9
+ return {
10
+ backend: 'webgpu',
11
+ ready: Promise.resolve({ ok: true, value: undefined }),
12
+ attach: () => ({ ok: true, value: undefined }),
13
+ detachWorld: () => {},
14
+ draw: () => ({ ok: true, value: undefined }),
15
+ onError: () => () => {},
16
+ onLost: () => () => {},
17
+ dispose: () => {},
18
+ } as unknown as Renderer;
19
+ }
20
+
21
+ describe('App profiler default-off allocation boundary', () => {
22
+ it('reports zero profiler-owned event objects and no artifact on the full loop', () => {
23
+ const allocationReport = { profilerEventObjectAllocations: 0 };
24
+ const profiler = createProfiler({ enabled: false, allocationReport });
25
+ const scheduler = {
26
+ callback: undefined as ((timestamp: number) => void) | undefined,
27
+ raf(callback: (timestamp: number) => void): number {
28
+ this.callback = callback;
29
+ return 1;
30
+ },
31
+ caf(): void {
32
+ this.callback = undefined;
33
+ },
34
+ };
35
+ const loop = createFrameLoop({
36
+ world: new World(),
37
+ renderer: makeRenderer(),
38
+ now: () => 1000,
39
+ raf: scheduler.raf.bind(scheduler),
40
+ caf: scheduler.caf.bind(scheduler),
41
+ profiler,
42
+ });
43
+
44
+ loop.start().unwrap();
45
+ scheduler.callback?.(1000);
46
+ loop.stop().unwrap();
47
+
48
+ expect({
49
+ profilerEventObjectAllocations: allocationReport.profilerEventObjectAllocations,
50
+ artifact: profiler.latestCapture(),
51
+ }).toEqual({ profilerEventObjectAllocations: 0, artifact: undefined });
52
+ });
53
+ });
@@ -0,0 +1,235 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { createRenderReadLease } from '@forgeax/engine-ecs/projection';
3
+ import { createProfiler, type ProfileCapture } from '@forgeax/engine-profiler';
4
+ import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { createFrameLoop } from '../internal/frame-loop';
8
+ import { APP_PHASE_CATALOG } from '../types';
9
+
10
+ const FRAME_LOOP_PHASES = [
11
+ 'frame-total',
12
+ 'draw-source',
13
+ 'world-update-primary',
14
+ 'world-update-injected',
15
+ 'renderer-draw',
16
+ ] as const;
17
+
18
+ function makeScheduler() {
19
+ let callback: ((timestamp: number) => void) | undefined;
20
+ return {
21
+ raf(next: (timestamp: number) => void): number {
22
+ callback = next;
23
+ return 1;
24
+ },
25
+ caf(): void {
26
+ callback = undefined;
27
+ },
28
+ tick(timestamp: number): void {
29
+ const next = callback;
30
+ callback = undefined;
31
+ next?.(timestamp);
32
+ },
33
+ };
34
+ }
35
+
36
+ function makeRenderer(onDraw: (profileFrame: unknown) => void = () => {}): Renderer {
37
+ return {
38
+ attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
39
+ draw: (request: RenderFrameInput) => {
40
+ onDraw(request.profileFrame);
41
+ return { ok: true, value: undefined };
42
+ },
43
+ subscribe: () => () => {},
44
+ dispose: () => {},
45
+ } as unknown as Renderer;
46
+ }
47
+
48
+ function startSession(
49
+ profiler: ReturnType<typeof createProfiler>,
50
+ frameLimit = 2,
51
+ eventLimit = 64,
52
+ ) {
53
+ const result = profiler.startCapture({ frameLimit, eventLimit });
54
+ if (!result.ok) throw new Error(result.error.code);
55
+ return result.value;
56
+ }
57
+
58
+ function finish(profiler: ReturnType<typeof createProfiler>): ProfileCapture {
59
+ const session = profiler.activeSession();
60
+ if (session === undefined) {
61
+ const latest = profiler.latestCapture();
62
+ expect(latest).toBeDefined();
63
+ if (latest === undefined) throw new Error('capture did not publish');
64
+ return latest;
65
+ }
66
+ const result = session?.finish();
67
+ expect(result?.ok).toBe(true);
68
+ if (result === undefined || !result.ok) throw new Error('capture did not finish');
69
+ return result.value;
70
+ }
71
+
72
+ describe('App profiler frame token', () => {
73
+ it('records the local frame phases with one token per logical frame', () => {
74
+ const profiler = createProfiler();
75
+ expect(startSession(profiler).captureId).toBe('capture-0001');
76
+ const scheduler = makeScheduler();
77
+ const drawOptions: unknown[] = [];
78
+ const loop = createFrameLoop({
79
+ world: new World(),
80
+ renderer: makeRenderer((options) => drawOptions.push(options)),
81
+ now: (() => {
82
+ const values = [1000, 1016, 1032];
83
+ return () => values.shift() ?? 1032;
84
+ })(),
85
+ raf: scheduler.raf,
86
+ caf: scheduler.caf,
87
+ profiler,
88
+ });
89
+
90
+ loop.start().unwrap();
91
+ scheduler.tick(1016);
92
+ scheduler.tick(1032);
93
+ loop.stop().unwrap();
94
+
95
+ const capture = finish(profiler);
96
+ expect(capture.phaseCatalog.app).toEqual(APP_PHASE_CATALOG);
97
+ expect(capture.captureId).toBe('capture-0001');
98
+ expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(10);
99
+ expect(
100
+ capture.records
101
+ .filter((record) => record.source === 'app' && record.kind === 'phase')
102
+ .map((record) => record.phase),
103
+ ).toEqual(expect.arrayContaining([...FRAME_LOOP_PHASES]));
104
+ expect(
105
+ new Set(
106
+ capture.records
107
+ .filter((record) => record.source === 'app' && record.kind === 'phase')
108
+ .map((record) => record.phase),
109
+ ),
110
+ ).toEqual(new Set(FRAME_LOOP_PHASES));
111
+ expect(
112
+ new Set(
113
+ capture.records.filter((record) => record.source === 'app').map((record) => record.frameId),
114
+ ),
115
+ ).toEqual(new Set([1, 2]));
116
+ expect(drawOptions).toHaveLength(2);
117
+ expect(drawOptions[0]).toMatchObject({
118
+ captureId: 'capture-0001',
119
+ frameId: 1,
120
+ });
121
+ expect(drawOptions[1]).toMatchObject({
122
+ captureId: 'capture-0001',
123
+ frameId: 2,
124
+ });
125
+ });
126
+
127
+ it('does not reuse capture identity and isolates observer and sink failures', () => {
128
+ const profiler = createProfiler({
129
+ sink: {
130
+ write: () => {
131
+ throw new Error('sink failure');
132
+ },
133
+ },
134
+ });
135
+ const session = startSession(profiler, 1);
136
+ const scheduler = makeScheduler();
137
+ let draws = 0;
138
+ const loop = createFrameLoop({
139
+ world: new World(),
140
+ renderer: makeRenderer(() => {
141
+ draws += 1;
142
+ }),
143
+ now: () => 1000,
144
+ raf: scheduler.raf,
145
+ caf: scheduler.caf,
146
+ profiler,
147
+ });
148
+
149
+ loop.start().unwrap();
150
+ scheduler.tick(1000);
151
+ loop.stop().unwrap();
152
+ expect(draws).toBe(1);
153
+ expect(session.finish().ok).toBe(false);
154
+
155
+ const secondSession = startSession(profiler, 1);
156
+ expect(secondSession.captureId).not.toBe(session.captureId);
157
+ });
158
+
159
+ it('recaptures complete data after an overflow without stale capture state', () => {
160
+ const profiler = createProfiler();
161
+ const scheduler = makeScheduler();
162
+ const loop = createFrameLoop({
163
+ world: new World(),
164
+ renderer: makeRenderer(),
165
+ now: () => 1000,
166
+ raf: scheduler.raf,
167
+ caf: scheduler.caf,
168
+ profiler,
169
+ });
170
+
171
+ const overflowSession = startSession(profiler, 1, 1);
172
+ loop.start().unwrap();
173
+ scheduler.tick(1000);
174
+
175
+ const overflow = profiler.latestCapture();
176
+ expect(overflow?.captureId).toBe(overflowSession.captureId);
177
+ expect(overflow?.completeness.status).toBe('overflow');
178
+ expect(overflow?.completeness.droppedEventCount).toBeGreaterThan(0);
179
+
180
+ const completeSession = startSession(profiler, 1, 64);
181
+ scheduler.tick(1016);
182
+ loop.stop().unwrap();
183
+
184
+ const complete = profiler.latestCapture();
185
+ expect(complete?.captureId).toBe(completeSession.captureId);
186
+ expect(complete?.captureId).not.toBe(overflow?.captureId);
187
+ expect(complete?.completeness).toMatchObject({ status: 'complete', droppedEventCount: 0 });
188
+ expect(overflow?.completeness.status).toBe('overflow');
189
+ expect(profiler.activeCaptureId()).toBeUndefined();
190
+ });
191
+
192
+ it('publishes a partial artifact when the host stops before its frame boundary', () => {
193
+ const profiler = createProfiler();
194
+ startSession(profiler, 2);
195
+ const scheduler = makeScheduler();
196
+ const loop = createFrameLoop({
197
+ world: new World(),
198
+ renderer: makeRenderer(),
199
+ now: () => 1000,
200
+ raf: scheduler.raf,
201
+ caf: scheduler.caf,
202
+ profiler,
203
+ });
204
+
205
+ loop.start().unwrap();
206
+ scheduler.tick(1000);
207
+ loop.stop().unwrap();
208
+
209
+ expect(profiler.latestCapture()?.completeness).toMatchObject({
210
+ status: 'partial',
211
+ incompleteReason: 'stopped-before-frame',
212
+ });
213
+ expect(profiler.activeCaptureId()).toBeUndefined();
214
+ });
215
+
216
+ it('keeps the default-off path free of profiler records', () => {
217
+ const scheduler = makeScheduler();
218
+ let draws = 0;
219
+ const loop = createFrameLoop({
220
+ world: new World(),
221
+ renderer: makeRenderer(() => {
222
+ draws += 1;
223
+ }),
224
+ now: () => 1000,
225
+ raf: scheduler.raf,
226
+ caf: scheduler.caf,
227
+ });
228
+
229
+ loop.start().unwrap();
230
+ scheduler.tick(1000);
231
+ loop.stop().unwrap();
232
+
233
+ expect(draws).toBe(1);
234
+ });
235
+ });
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createProfiler } from '@forgeax/engine-profiler';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { APP_PHASE_CATALOG } from '../types';
6
+
7
+ describe('App profiler phase catalog ownership', () => {
8
+ it('matches the profiler receiver set by definition and has five unique phases', () => {
9
+ const profiler = createProfiler();
10
+ expect(profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG).ok).toBe(true);
11
+ expect(APP_PHASE_CATALOG).toHaveLength(9);
12
+ expect(new Set(APP_PHASE_CATALOG).size).toBe(APP_PHASE_CATALOG.length);
13
+ expect(new Set(APP_PHASE_CATALOG)).toEqual(new Set(profiler.phaseCatalog.app));
14
+ });
15
+
16
+ it('rejects a second App catalog definition', () => {
17
+ const profiler = createProfiler();
18
+ expect(profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG).ok).toBe(true);
19
+ const duplicate = profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG);
20
+ expect(duplicate.ok).toBe(false);
21
+ if (!duplicate.ok) expect(duplicate.error.code).toBe('phase-catalog-conflict');
22
+ });
23
+
24
+ it('keeps the phase declaration in the App owner module', () => {
25
+ const source = readFileSync(new URL('../types.ts', import.meta.url), 'utf8');
26
+ expect(source.match(/'frame-total'/g)).toHaveLength(1);
27
+ expect(source).not.toContain('FramePhaseObserver');
28
+ expect(source).not.toContain('frameSeq');
29
+ });
30
+ });
@@ -0,0 +1,117 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { createRenderReadLease } from '@forgeax/engine-ecs/projection';
3
+ import { createProfiler } from '@forgeax/engine-profiler';
4
+ import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { createFrameLoop } from '../internal/frame-loop';
8
+ import { APP_PHASE_CATALOG } from '../types';
9
+
10
+ function makeScheduler() {
11
+ let callback: ((timestamp: number) => void) | undefined;
12
+ return {
13
+ raf(next: (timestamp: number) => void): number {
14
+ callback = next;
15
+ return 1;
16
+ },
17
+ caf(): void {
18
+ callback = undefined;
19
+ },
20
+ tick(timestamp: number): void {
21
+ const next = callback;
22
+ callback = undefined;
23
+ next?.(timestamp);
24
+ },
25
+ };
26
+ }
27
+
28
+ function makeRenderer(draw: (profileFrame: unknown) => void): Renderer {
29
+ return {
30
+ attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
31
+ draw: (request: RenderFrameInput) => {
32
+ draw(request.profileFrame);
33
+ return { ok: true, value: undefined };
34
+ },
35
+ subscribe: () => () => {},
36
+ dispose: () => {},
37
+ } as unknown as Renderer;
38
+ }
39
+
40
+ function start(profiler: ReturnType<typeof createProfiler>) {
41
+ const result = profiler.startCapture({ frameLimit: 2, eventLimit: 64 });
42
+ if (!result.ok) throw new Error(result.error.code);
43
+ return result.value;
44
+ }
45
+
46
+ function finish(profiler: ReturnType<typeof createProfiler>) {
47
+ const session = profiler.activeSession();
48
+ if (session === undefined) {
49
+ const latest = profiler.latestCapture();
50
+ if (latest === undefined) throw new Error('capture did not publish');
51
+ return latest;
52
+ }
53
+ const result = session.finish();
54
+ if (!result.ok) throw new Error(result.error.code);
55
+ return result.value;
56
+ }
57
+
58
+ describe('App and Render profiler integration', () => {
59
+ it('keeps normal and App-only frames on one capture identity', () => {
60
+ const profiler = createProfiler();
61
+ start(profiler);
62
+ const scheduler = makeScheduler();
63
+ const drawOptions: unknown[] = [];
64
+ const loop = createFrameLoop({
65
+ world: new World(),
66
+ renderer: makeRenderer((options) => drawOptions.push(options)),
67
+ now: () => 1000,
68
+ raf: scheduler.raf,
69
+ caf: scheduler.caf,
70
+ profiler,
71
+ });
72
+
73
+ loop.start().unwrap();
74
+ scheduler.tick(1000);
75
+ loop.stop().unwrap();
76
+
77
+ const capture = finish(profiler);
78
+ expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(5);
79
+ expect(capture.records.every((record) => record.frameId === 1)).toBe(true);
80
+ expect(drawOptions[0]).toMatchObject({
81
+ captureId: capture.captureId,
82
+ frameId: 1,
83
+ });
84
+ expect(capture.phaseCatalog.app).toEqual(APP_PHASE_CATALOG);
85
+ expect(profiler.phaseCatalog.app).toEqual([]);
86
+ });
87
+
88
+ it('isolates a renderer failure while the host frame loop continues', () => {
89
+ const profiler = createProfiler();
90
+ start(profiler);
91
+ const scheduler = makeScheduler();
92
+ let draws = 0;
93
+ const errors: unknown[] = [];
94
+ const loop = createFrameLoop({
95
+ world: new World(),
96
+ renderer: makeRenderer(() => {
97
+ draws += 1;
98
+ throw new Error('render failure');
99
+ }),
100
+ now: () => 1000,
101
+ raf: scheduler.raf,
102
+ caf: scheduler.caf,
103
+ onError: (error) => errors.push(error),
104
+ profiler,
105
+ });
106
+
107
+ loop.start().unwrap();
108
+ scheduler.tick(1000);
109
+ scheduler.tick(1000);
110
+ loop.stop().unwrap();
111
+
112
+ const capture = finish(profiler);
113
+ expect(draws).toBe(2);
114
+ expect(errors).toHaveLength(2);
115
+ expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(10);
116
+ });
117
+ });
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const BRIDGE_SOURCE = fileURLToPath(
6
+ new URL('../internal/browser-remote-bridge.ts', import.meta.url),
7
+ );
8
+ const CREATE_APP_SOURCE = fileURLToPath(new URL('../create-app.ts', import.meta.url));
9
+
10
+ describe('browser remote profiler bridge', () => {
11
+ const bridge = readFileSync(BRIDGE_SOURCE, 'utf8');
12
+ const createApp = readFileSync(CREATE_APP_SOURCE, 'utf8');
13
+
14
+ it('propagates the explicit profiler root through the existing bridge context', () => {
15
+ expect(bridge).toContain('readonly profiler?: unknown');
16
+ expect(bridge).toContain('profiler,');
17
+ expect(bridge).toContain('profiler: profiler');
18
+ });
19
+
20
+ it('keeps the bridge opt-in and free of a static remote dependency', () => {
21
+ expect(createApp).toContain('VITE_FORGEAX_ENGINE_BRIDGE');
22
+ expect(createApp).toContain("import('./internal/browser-remote-bridge')");
23
+ expect(bridge).not.toMatch(/^import .*@forgeax\/engine-remote/m);
24
+ });
25
+
26
+ it('keeps profiler failures in the JSON-safe eval envelope', () => {
27
+ expect(bridge).toContain("code: typeof e.code === 'string' ? e.code : 'script-runtime-error'");
28
+ expect(bridge).toContain('detail');
29
+ expect(bridge).toContain('JSON.stringify(envelope)');
30
+ });
31
+ });