@forgeax/engine-assets-runtime 0.1.4 → 0.1.6

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 (311) hide show
  1. package/README.md +442 -175
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  31. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  33. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  39. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  41. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  43. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  45. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  46. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  47. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  48. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  49. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  50. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  51. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  52. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  53. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  54. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  55. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  56. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  57. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  58. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  59. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  60. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  61. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  62. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  64. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  66. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  67. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  68. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  69. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  70. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  71. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  72. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  73. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  74. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  76. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  77. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  78. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  79. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  80. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  81. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  82. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  83. package/dist/__tests__/plugin.test.d.ts +2 -0
  84. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  85. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  86. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  87. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  88. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  89. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  90. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  91. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  92. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  93. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  94. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  95. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  96. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  97. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  98. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  99. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  100. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  101. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  102. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  103. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  104. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  105. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  106. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  107. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  108. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  109. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  110. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  111. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  112. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  113. package/dist/aabb.d.ts +3 -0
  114. package/dist/aabb.d.ts.map +1 -0
  115. package/dist/asset-registry.d.ts +552 -0
  116. package/dist/asset-registry.d.ts.map +1 -0
  117. package/dist/builtin-asset-registry.d.ts +22 -0
  118. package/dist/builtin-asset-registry.d.ts.map +1 -0
  119. package/dist/catalog-source.d.ts +2 -1
  120. package/dist/catalog-source.d.ts.map +1 -1
  121. package/dist/decode-image-bytes.d.ts +57 -0
  122. package/dist/decode-image-bytes.d.ts.map +1 -0
  123. package/dist/dynamic-texture-store.d.ts +89 -0
  124. package/dist/dynamic-texture-store.d.ts.map +1 -0
  125. package/dist/errors/asset.d.ts +201 -0
  126. package/dist/errors/asset.d.ts.map +1 -0
  127. package/dist/handles.d.ts +141 -0
  128. package/dist/handles.d.ts.map +1 -0
  129. package/dist/image-error.d.ts +6 -0
  130. package/dist/image-error.d.ts.map +1 -0
  131. package/dist/index.d.ts +32 -3
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.mjs +7218 -114
  134. package/dist/index.mjs.map +1 -1
  135. package/dist/internal.d.ts +1 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/loader-registry.d.ts +63 -0
  138. package/dist/loader-registry.d.ts.map +1 -0
  139. package/dist/loaders/inline-pack.d.ts +72 -0
  140. package/dist/loaders/inline-pack.d.ts.map +1 -0
  141. package/dist/loaders/mesh-bin.d.ts +21 -0
  142. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  143. package/dist/loaders/pack-artifact.d.ts +20 -0
  144. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  145. package/dist/material/dependency-graph.d.ts +9 -0
  146. package/dist/material/dependency-graph.d.ts.map +1 -0
  147. package/dist/material/generation-cache.d.ts +24 -0
  148. package/dist/material/generation-cache.d.ts.map +1 -0
  149. package/dist/material/inspection.d.ts +24 -0
  150. package/dist/material/inspection.d.ts.map +1 -0
  151. package/dist/material/loader.d.ts +59 -0
  152. package/dist/material/loader.d.ts.map +1 -0
  153. package/dist/material/runtime-shader.d.ts +14 -0
  154. package/dist/material/runtime-shader.d.ts.map +1 -0
  155. package/dist/mesh-bin.d.ts +3 -0
  156. package/dist/mesh-bin.d.ts.map +1 -0
  157. package/dist/mipmap-generator.d.ts +132 -0
  158. package/dist/mipmap-generator.d.ts.map +1 -0
  159. package/dist/payload-validate.d.ts +60 -0
  160. package/dist/payload-validate.d.ts.map +1 -0
  161. package/dist/plugin.d.ts +16 -0
  162. package/dist/plugin.d.ts.map +1 -0
  163. package/dist/registry/artifact-io.d.ts +16 -0
  164. package/dist/registry/artifact-io.d.ts.map +1 -0
  165. package/dist/registry/asset-evidence.d.ts +13 -0
  166. package/dist/registry/asset-evidence.d.ts.map +1 -0
  167. package/dist/registry/catalog-state.d.ts +47 -0
  168. package/dist/registry/catalog-state.d.ts.map +1 -0
  169. package/dist/registry/catalog.d.ts +48 -0
  170. package/dist/registry/catalog.d.ts.map +1 -0
  171. package/dist/registry/instantiate.d.ts +153 -0
  172. package/dist/registry/instantiate.d.ts.map +1 -0
  173. package/dist/registry/load-by-guid.d.ts +247 -0
  174. package/dist/registry/load-by-guid.d.ts.map +1 -0
  175. package/dist/registry/load-state.d.ts +26 -0
  176. package/dist/registry/load-state.d.ts.map +1 -0
  177. package/dist/registry/runtime-projection.d.ts +16 -0
  178. package/dist/registry/runtime-projection.d.ts.map +1 -0
  179. package/dist/registry/scene-publication-fence.d.ts +38 -0
  180. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  181. package/dist/registry/validate-material.d.ts +77 -0
  182. package/dist/registry/validate-material.d.ts.map +1 -0
  183. package/dist/resolve-asset-handle.d.ts +43 -0
  184. package/dist/resolve-asset-handle.d.ts.map +1 -0
  185. package/dist/resolve-tileset-runtime.d.ts +23 -0
  186. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  187. package/dist/scene-handle-fields.d.ts +88 -0
  188. package/dist/scene-handle-fields.d.ts.map +1 -0
  189. package/dist/scene-payload.d.ts +15 -0
  190. package/dist/scene-payload.d.ts.map +1 -0
  191. package/dist/wire-default-loaders.d.ts +29 -0
  192. package/dist/wire-default-loaders.d.ts.map +1 -0
  193. package/package.json +22 -9
  194. package/src/__tests__/aabb.unit.test.ts +97 -0
  195. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  196. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  197. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  198. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  199. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  200. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  201. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  202. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  203. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  204. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  205. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  206. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  207. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  208. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  209. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  210. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  211. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  212. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  213. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  214. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  215. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  216. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  217. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  218. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  219. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  220. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  221. package/src/__tests__/instantiate.unit.test.ts +684 -0
  222. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  223. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  224. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  225. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  226. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  227. package/src/__tests__/material-ready.integration.test.ts +492 -0
  228. package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
  229. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  230. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  231. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  232. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  233. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  234. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  235. package/src/__tests__/plugin.test.ts +55 -0
  236. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  237. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  238. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  239. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  240. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  241. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  242. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  243. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  244. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  245. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  246. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
  247. package/src/__tests__/static-rig.integration.test.ts +8 -0
  248. package/src/__tests__/validate-material.unit.test.ts +359 -0
  249. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  250. package/src/aabb.ts +107 -0
  251. package/src/asset-registry.ts +2040 -0
  252. package/src/builtin-asset-registry.ts +164 -0
  253. package/src/catalog-source.ts +14 -70
  254. package/src/decode-image-bytes.ts +126 -0
  255. package/src/dynamic-texture-store.ts +234 -0
  256. package/src/errors/asset.ts +347 -0
  257. package/src/handles.ts +180 -0
  258. package/src/image-error.ts +45 -0
  259. package/src/index.ts +177 -19
  260. package/src/internal.ts +1 -0
  261. package/src/loader-registry.ts +157 -0
  262. package/src/loaders/inline-pack.ts +837 -0
  263. package/src/loaders/mesh-bin.ts +304 -0
  264. package/src/loaders/pack-artifact.ts +665 -0
  265. package/src/material/dependency-graph.ts +46 -0
  266. package/src/material/generation-cache.ts +177 -0
  267. package/src/material/inspection.ts +46 -0
  268. package/src/material/loader.ts +326 -0
  269. package/src/material/runtime-shader.ts +133 -0
  270. package/src/mesh-bin.ts +5 -0
  271. package/src/mipmap-generator.ts +450 -0
  272. package/src/payload-validate.ts +573 -0
  273. package/src/plugin.ts +48 -0
  274. package/src/registry/artifact-io.ts +259 -0
  275. package/src/registry/asset-evidence.ts +51 -0
  276. package/src/registry/catalog-state.ts +409 -0
  277. package/src/registry/catalog.ts +369 -0
  278. package/src/registry/instantiate.ts +1056 -0
  279. package/src/registry/load-by-guid.ts +1657 -0
  280. package/src/registry/load-state.ts +115 -0
  281. package/src/registry/runtime-projection.ts +63 -0
  282. package/src/registry/scene-publication-fence.ts +256 -0
  283. package/src/registry/validate-material.ts +386 -0
  284. package/src/resolve-asset-handle.ts +147 -0
  285. package/src/resolve-tileset-runtime.ts +72 -0
  286. package/src/scene-handle-fields.ts +221 -0
  287. package/src/scene-payload.ts +207 -0
  288. package/src/wire-default-loaders.ts +79 -0
  289. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  290. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  291. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  292. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  294. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  297. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  298. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  301. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  302. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  304. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  305. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  306. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  307. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  308. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  309. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  310. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  311. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,285 @@
1
+ // @forgeax/engine-assets-runtime -- register-time payload validation coverage
2
+ // (fix issue #709). Covers validateMeshPayload branch tree, validateTilesetPayload
3
+ // R-6 first-error ordering, and inferAtlasExtent.
4
+
5
+ import type { Asset, MeshAsset, TilesetAsset } from '@forgeax/engine-types';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { inferAtlasExtent, validateMeshPayload, validateTilesetPayload } from '../payload-validate';
8
+
9
+ function mesh(over: Partial<MeshAsset>): Asset {
10
+ return {
11
+ kind: 'mesh',
12
+ vertices: new Float32Array(0),
13
+ attributes: {},
14
+ submeshes: [
15
+ { indexOffset: 0, indexCount: 0, vertexCount: 0, topology: 'triangle-list', materialSlot: 0 },
16
+ ],
17
+ materialSlots: [{ slotName: 'Default' }],
18
+ ...over,
19
+ } as MeshAsset;
20
+ }
21
+
22
+ describe('validateMeshPayload', () => {
23
+ it('returns null for a non-mesh asset', () => {
24
+ expect(validateMeshPayload({ kind: 'material' } as unknown as Asset)).toBeNull();
25
+ });
26
+
27
+ it('returns null for a legal empty mesh (triangle-list, no vertices/indices)', () => {
28
+ expect(validateMeshPayload(mesh({}))).toBeNull();
29
+ });
30
+
31
+ it('errors when submeshes is empty', () => {
32
+ const e = validateMeshPayload(mesh({ submeshes: [] }));
33
+ expect(e?.code).toBe('mesh-asset-submeshes-empty');
34
+ });
35
+
36
+ it('errors on strip topology without an index buffer', () => {
37
+ const e = validateMeshPayload(
38
+ mesh({
39
+ vertices: new Float32Array(12),
40
+ submeshes: [
41
+ {
42
+ indexOffset: 0,
43
+ indexCount: 0,
44
+ vertexCount: 1,
45
+ topology: 'triangle-strip',
46
+ materialSlot: 0,
47
+ },
48
+ ],
49
+ }),
50
+ );
51
+ expect(e?.code).toBe('asset-invalid-value');
52
+ expect((e?.detail as { reason: string }).reason).toBe('strip-topology-without-indices');
53
+ });
54
+
55
+ it('errors when an empty-geometry submesh uses a non-triangle-list topology', () => {
56
+ const e = validateMeshPayload(
57
+ mesh({
58
+ vertices: new Float32Array(0),
59
+ submeshes: [
60
+ { indexOffset: 0, indexCount: 0, vertexCount: 0, topology: 'line-list', materialSlot: 0 },
61
+ ],
62
+ }),
63
+ );
64
+ expect(e?.code).toBe('asset-invalid-value');
65
+ expect((e?.detail as { reason: string }).reason).toBe('empty-geometry-non-default-topology');
66
+ });
67
+
68
+ it('errors when a submesh index range exceeds the index buffer length', () => {
69
+ const e = validateMeshPayload(
70
+ mesh({
71
+ vertices: new Float32Array(36), // 3 verts * 12
72
+ indices: Uint16Array.of(0, 1, 2),
73
+ submeshes: [
74
+ {
75
+ indexOffset: 0,
76
+ indexCount: 6,
77
+ vertexCount: 3,
78
+ topology: 'triangle-list',
79
+ materialSlot: 0,
80
+ },
81
+ ],
82
+ }),
83
+ );
84
+ expect(e?.code).toBe('mesh-submesh-index-range-out-of-bounds');
85
+ });
86
+
87
+ it('errors when vertices.length is not divisible by 12 (stride mismatch)', () => {
88
+ const e = validateMeshPayload(
89
+ mesh({
90
+ vertices: new Float32Array(13),
91
+ submeshes: [
92
+ {
93
+ indexOffset: 0,
94
+ indexCount: 0,
95
+ vertexCount: 1,
96
+ topology: 'triangle-list',
97
+ materialSlot: 0,
98
+ },
99
+ ],
100
+ }),
101
+ );
102
+ expect(e?.code).toBe('mesh-vertex-stride-mismatch');
103
+ });
104
+
105
+ it('errors when maxIndex + 1 !== vertexCount', () => {
106
+ const e = validateMeshPayload(
107
+ mesh({
108
+ vertices: new Float32Array(24), // 2 vertices
109
+ indices: Uint16Array.of(0, 1, 5), // maxIndex 5 -> expects 6 verts
110
+ submeshes: [
111
+ {
112
+ indexOffset: 0,
113
+ indexCount: 3,
114
+ vertexCount: 2,
115
+ topology: 'triangle-list',
116
+ materialSlot: 0,
117
+ },
118
+ ],
119
+ }),
120
+ );
121
+ expect(e?.code).toBe('mesh-vertex-stride-mismatch');
122
+ expect((e?.detail as { vertexCount: number }).vertexCount).toBe(6);
123
+ });
124
+
125
+ it('accepts a well-formed indexed mesh', () => {
126
+ expect(
127
+ validateMeshPayload(
128
+ mesh({
129
+ vertices: new Float32Array(36), // 3 vertices
130
+ indices: Uint16Array.of(0, 1, 2),
131
+ submeshes: [
132
+ {
133
+ indexOffset: 0,
134
+ indexCount: 3,
135
+ vertexCount: 3,
136
+ topology: 'triangle-list',
137
+ materialSlot: 0,
138
+ },
139
+ ],
140
+ }),
141
+ ),
142
+ ).toBeNull();
143
+ });
144
+
145
+ it('accepts a vertex-only mesh (no indices, skips the maxIndex invariant)', () => {
146
+ expect(
147
+ validateMeshPayload(
148
+ mesh({
149
+ vertices: new Float32Array(24),
150
+ submeshes: [
151
+ {
152
+ indexOffset: 0,
153
+ indexCount: 0,
154
+ vertexCount: 2,
155
+ topology: 'triangle-list',
156
+ materialSlot: 0,
157
+ },
158
+ ],
159
+ }),
160
+ ),
161
+ ).toBeNull();
162
+ });
163
+ });
164
+
165
+ function tileset(over: Partial<TilesetAsset>): TilesetAsset {
166
+ return {
167
+ kind: 'tileset',
168
+ atlases: ['atlas-guid'],
169
+ tileWidth: 16,
170
+ tileHeight: 16,
171
+ columns: 4,
172
+ rows: 4,
173
+ regions: [{ x: 0, y: 0, width: 16, height: 16 }],
174
+ tiles: [{ regionIndex: 0 }],
175
+ ...over,
176
+ } as TilesetAsset;
177
+ }
178
+
179
+ describe('validateTilesetPayload', () => {
180
+ it('accepts a well-formed tileset', () => {
181
+ expect(validateTilesetPayload(tileset({}))).toBeNull();
182
+ });
183
+
184
+ it('errors when atlases is empty', () => {
185
+ const e = validateTilesetPayload(tileset({ atlases: [] }));
186
+ expect(e?.code).toBe('tileset-tile-entry-malformed');
187
+ expect((e?.detail as { field: string }).field).toBe('atlases');
188
+ });
189
+
190
+ it('errors on a negative/zero region rectangle', () => {
191
+ const e = validateTilesetPayload(
192
+ tileset({ regions: [{ x: -1, y: 0, width: 16, height: 16 }] }),
193
+ );
194
+ expect(e?.code).toBe('tileset-region-index-out-of-range');
195
+ });
196
+
197
+ it('errors when a region escapes the supplied atlas extent', () => {
198
+ const e = validateTilesetPayload(
199
+ tileset({ regions: [{ x: 0, y: 0, width: 100, height: 16 }] }),
200
+ { atlasWidth: 64, atlasHeight: 64 },
201
+ );
202
+ expect(e?.code).toBe('tileset-region-index-out-of-range');
203
+ });
204
+
205
+ it('errors on an out-of-range region.atlasIndex', () => {
206
+ const e = validateTilesetPayload(
207
+ tileset({ regions: [{ x: 0, y: 0, width: 16, height: 16, atlasIndex: 5 }] }),
208
+ );
209
+ expect((e?.detail as { field: string }).field).toBe('atlasIndex');
210
+ });
211
+
212
+ it('errors on an out-of-range tile.regionIndex', () => {
213
+ const e = validateTilesetPayload(tileset({ tiles: [{ regionIndex: 9 }] }));
214
+ expect(e?.code).toBe('tileset-region-index-out-of-range');
215
+ });
216
+
217
+ it('errors on widthCells / heightCells / pivotX / pivotY out of bounds', () => {
218
+ expect(
219
+ validateTilesetPayload(tileset({ tiles: [{ regionIndex: 0, widthCells: 0 }] }))?.detail,
220
+ ).toMatchObject({ field: 'widthCells' });
221
+ expect(
222
+ validateTilesetPayload(tileset({ tiles: [{ regionIndex: 0, heightCells: 65 }] }))?.detail,
223
+ ).toMatchObject({ field: 'heightCells' });
224
+ expect(
225
+ validateTilesetPayload(tileset({ tiles: [{ regionIndex: 0, pivotX: 2 }] }))?.detail,
226
+ ).toMatchObject({ field: 'pivotX' });
227
+ expect(
228
+ validateTilesetPayload(tileset({ tiles: [{ regionIndex: 0, pivotY: -1 }] }))?.detail,
229
+ ).toMatchObject({ field: 'pivotY' });
230
+ });
231
+
232
+ it('accepts a valid rect collider and rejects a malformed one', () => {
233
+ expect(
234
+ validateTilesetPayload(
235
+ tileset({
236
+ tiles: [{ regionIndex: 0, collider: { type: 'rect', rect: [0, 0, 0.5, 0.5] } }],
237
+ }),
238
+ ),
239
+ ).toBeNull();
240
+ const e = validateTilesetPayload(
241
+ tileset({ tiles: [{ regionIndex: 0, collider: { type: 'rect', rect: [0, 0, 2, 2] } }] }),
242
+ );
243
+ expect((e?.detail as { field: string }).field).toBe('collider');
244
+ });
245
+
246
+ it('accepts a valid polygon collider and rejects fewer-than-3 points', () => {
247
+ expect(
248
+ validateTilesetPayload(
249
+ tileset({
250
+ tiles: [
251
+ {
252
+ regionIndex: 0,
253
+ collider: {
254
+ type: 'polygon',
255
+ points: [
256
+ [0, 0],
257
+ [1, 0],
258
+ [0, 1],
259
+ ],
260
+ },
261
+ },
262
+ ],
263
+ }),
264
+ ),
265
+ ).toBeNull();
266
+ const e = validateTilesetPayload(
267
+ tileset({ tiles: [{ regionIndex: 0, collider: { type: 'polygon', points: [[0, 0]] } }] }),
268
+ );
269
+ expect((e?.detail as { field: string }).field).toBe('collider');
270
+ });
271
+
272
+ it("accepts a 'none' collider", () => {
273
+ expect(
274
+ validateTilesetPayload(tileset({ tiles: [{ regionIndex: 0, collider: { type: 'none' } }] })),
275
+ ).toBeNull();
276
+ });
277
+ });
278
+
279
+ describe('inferAtlasExtent', () => {
280
+ it('multiplies columns*tileWidth and rows*tileHeight', () => {
281
+ expect(
282
+ inferAtlasExtent(tileset({ columns: 8, tileWidth: 16, rows: 4, tileHeight: 32 })),
283
+ ).toEqual({ atlasWidth: 128, atlasHeight: 128 });
284
+ });
285
+ });
@@ -0,0 +1,55 @@
1
+ import { Context } from '@forgeax/engine-plugin';
2
+ import type { Loader } from '@forgeax/engine-types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import type { AssetRegistry } from '../asset-registry';
5
+ import { LoaderRegistry } from '../loader-registry';
6
+ import { assetLoaderPlugin, assetsPlugin, packLoaderPlugin } from '../plugin';
7
+
8
+ describe('asset capability plugins', () => {
9
+ it('removes loader contributions with their Fibers', async () => {
10
+ const loaders = new LoaderRegistry();
11
+ const assets = { loaders } as AssetRegistry;
12
+ const loader = {
13
+ kind: 'test-dialogue',
14
+ load: () => ({ lines: [] }),
15
+ } satisfies Loader<unknown>;
16
+ const packLoader = {
17
+ kind: 'test-cutscene',
18
+ load: () => ({ shots: [] }),
19
+ };
20
+ const context = new Context();
21
+ await context.plugin(assetsPlugin(assets));
22
+ const loaderFiber = await context.plugin(assetLoaderPlugin(loader));
23
+ const packFiber = await context.plugin(packLoaderPlugin(packLoader));
24
+
25
+ expect(loaders.get(loader.kind)).toBe(loader);
26
+ expect(
27
+ await loaders.loadPack(
28
+ {
29
+ guid: '00000000-0000-7000-8000-000000000001',
30
+ kind: packLoader.kind,
31
+ payload: {},
32
+ refs: [],
33
+ artifacts: {},
34
+ },
35
+ {} as never,
36
+ ),
37
+ ).toMatchObject({ ok: true });
38
+
39
+ await packFiber.dispose();
40
+ await loaderFiber.dispose();
41
+ expect(loaders.get(loader.kind)).toBeUndefined();
42
+ expect(
43
+ await loaders.loadPack(
44
+ {
45
+ guid: '00000000-0000-7000-8000-000000000001',
46
+ kind: packLoader.kind,
47
+ payload: {},
48
+ refs: [],
49
+ artifacts: {},
50
+ },
51
+ {} as never,
52
+ ),
53
+ ).toMatchObject({ ok: false });
54
+ });
55
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AssetRegistry } from '../asset-registry';
3
+
4
+ const GUID = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
5
+
6
+ describe('registry concurrency counters', () => {
7
+ it('deduplicates concurrent same-GUID fetch and parse work', async () => {
8
+ const registry = new AssetRegistry({} as never);
9
+ registry.loaders.register({
10
+ kind: 'counter-node',
11
+ load: vi.fn((payload) => ({ kind: 'counter-node', ...payload })),
12
+ });
13
+ registry.configurePackIndex('/pack-index.json');
14
+ const fetcher = vi.fn(async (url: string) => {
15
+ if (url.endsWith('pack-index.json')) {
16
+ return new Response(
17
+ JSON.stringify([
18
+ { guid: GUID, packageUrl: '/counter.pack.json', kind: 'counter-node', sourcePath: 'a' },
19
+ ]),
20
+ );
21
+ }
22
+ return new Response(
23
+ JSON.stringify({
24
+ schemaVersion: '2.0.0',
25
+ kind: 'internal-text-package',
26
+ assets: [
27
+ { guid: GUID, kind: 'counter-node', payload: { value: 1 }, refs: [], artifacts: {} },
28
+ ],
29
+ }),
30
+ );
31
+ });
32
+ vi.stubGlobal('fetch', fetcher);
33
+
34
+ const results = await Promise.all(
35
+ Array.from({ length: 8 }, () => registry.loadByGuid(registry.parseGuid(GUID))),
36
+ );
37
+ expect(results.every((result) => result.ok)).toBe(true);
38
+ expect(fetcher.mock.calls.filter(([url]) => url.endsWith('counter.pack.json'))).toHaveLength(1);
39
+ });
40
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AssetRegistry } from '../asset-registry';
3
+
4
+ const ROOT = '11111111-1111-4111-8111-111111111111';
5
+ const REF = '22222222-2222-4222-8222-222222222222';
6
+
7
+ describe('registry failure cleanup and retry', () => {
8
+ it('purges a failed ref closure before retrying the repaired package', async () => {
9
+ const registry = new AssetRegistry({} as never);
10
+ registry.loaders.register({
11
+ kind: 'retry-node',
12
+ load: (payload) => ({ kind: 'retry-node', ...payload }),
13
+ });
14
+ registry.configurePackIndex('/pack-index.json');
15
+ let repaired = false;
16
+ vi.stubGlobal(
17
+ 'fetch',
18
+ vi.fn(async (url: string) => {
19
+ if (url.endsWith('pack-index.json')) {
20
+ return new Response(
21
+ JSON.stringify([
22
+ {
23
+ guid: ROOT,
24
+ packageUrl: '/retry.pack.json',
25
+ kind: 'retry-node',
26
+ sourcePath: 'root',
27
+ },
28
+ { guid: REF, packageUrl: '/retry.pack.json', kind: 'retry-node', sourcePath: 'ref' },
29
+ ]),
30
+ );
31
+ }
32
+ return new Response(
33
+ JSON.stringify({
34
+ schemaVersion: '2.0.0',
35
+ kind: 'internal-text-package',
36
+ assets: [
37
+ {
38
+ guid: ROOT,
39
+ kind: 'retry-node',
40
+ payload: { name: 'root' },
41
+ refs: [REF],
42
+ artifacts: {},
43
+ },
44
+ ...(repaired
45
+ ? [
46
+ {
47
+ guid: REF,
48
+ kind: 'retry-node',
49
+ payload: { name: 'ref' },
50
+ refs: [],
51
+ artifacts: {},
52
+ },
53
+ ]
54
+ : []),
55
+ ],
56
+ }),
57
+ );
58
+ }),
59
+ );
60
+
61
+ const failed = await registry.loadByGuid(registry.parseGuid(ROOT));
62
+ expect(failed.ok).toBe(false);
63
+ expect(registry.lookup(ROOT)).toBeUndefined();
64
+ repaired = true;
65
+ const retried = await registry.loadByGuid(registry.parseGuid(ROOT));
66
+ expect(retried.ok).toBe(true);
67
+ expect(registry.lookup(REF)).toBeDefined();
68
+ });
69
+ });
@@ -0,0 +1,246 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AssetRegistry } from '../asset-registry';
3
+
4
+ const A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
5
+ const B = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
6
+ const C = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
7
+ const D = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd';
8
+
9
+ describe('registry load graph', () => {
10
+ it('restores descriptor payloads through the production Pack path', async () => {
11
+ const registry = new AssetRegistry({} as never);
12
+ registry.configurePackIndex('/pack-index.json');
13
+ const pack = {
14
+ schemaVersion: '2.0.0',
15
+ kind: 'internal-text-package',
16
+ assets: [
17
+ {
18
+ guid: C,
19
+ kind: 'render-pipeline',
20
+ payload: { kind: 'render-pipeline', pipelineId: 'forgeax::urp' },
21
+ refs: [],
22
+ artifacts: {},
23
+ },
24
+ ],
25
+ };
26
+ vi.stubGlobal(
27
+ 'fetch',
28
+ vi.fn(async (url: string) => {
29
+ if (url.endsWith('pack-index.json')) {
30
+ return new Response(
31
+ JSON.stringify([
32
+ {
33
+ guid: C,
34
+ packageUrl: '/descriptor.pack.json',
35
+ kind: 'render-pipeline',
36
+ sourcePath: 'pipeline',
37
+ },
38
+ ]),
39
+ );
40
+ }
41
+ return new Response(JSON.stringify(pack));
42
+ }),
43
+ );
44
+
45
+ const result = await registry.loadByGuid(registry.parseGuid(C));
46
+ expect(result).toMatchObject({ ok: true, value: { kind: 'render-pipeline' } });
47
+ });
48
+
49
+ it('keeps a parent asset out of lookup when a recursive dependency is not ready', async () => {
50
+ const registry = new AssetRegistry({} as never);
51
+ registry.loaders.register({
52
+ kind: 'parent-node',
53
+ load: (payload) => ({ kind: 'parent-node', ...payload }),
54
+ });
55
+ registry.configurePackIndex('/pack-index.json');
56
+ vi.stubGlobal(
57
+ 'fetch',
58
+ vi.fn(async (url: string) => {
59
+ if (url.endsWith('pack-index.json')) {
60
+ return new Response(
61
+ JSON.stringify([
62
+ { guid: A, packageUrl: '/parent.pack.json', kind: 'parent-node', sourcePath: 'a' },
63
+ ]),
64
+ );
65
+ }
66
+ return new Response(
67
+ JSON.stringify({
68
+ schemaVersion: '2.0.0',
69
+ kind: 'internal-text-package',
70
+ assets: [
71
+ {
72
+ guid: A,
73
+ kind: 'parent-node',
74
+ payload: { name: 'parent' },
75
+ refs: [B],
76
+ artifacts: {},
77
+ },
78
+ ],
79
+ }),
80
+ );
81
+ }),
82
+ );
83
+
84
+ const result = await registry.loadByGuid(registry.parseGuid(A));
85
+ expect(result).toMatchObject({
86
+ ok: false,
87
+ error: { code: expect.any(String), hint: expect.any(String) },
88
+ });
89
+ expect(registry.lookup(A)).toBeUndefined();
90
+ });
91
+
92
+ it('terminates a cycle without exposing either provisional asset early', async () => {
93
+ const registry = new AssetRegistry({} as never);
94
+ registry.loaders.register({
95
+ kind: 'cycle-node',
96
+ load: (payload) => ({ kind: 'cycle-node', ...payload }),
97
+ });
98
+ registry.configurePackIndex('/pack-index.json');
99
+ const pack = {
100
+ schemaVersion: '2.0.0',
101
+ kind: 'internal-text-package',
102
+ assets: [
103
+ { guid: A, kind: 'cycle-node', payload: { name: 'a' }, refs: [B], artifacts: {} },
104
+ { guid: B, kind: 'cycle-node', payload: { name: 'b' }, refs: [A], artifacts: {} },
105
+ ],
106
+ };
107
+ vi.stubGlobal(
108
+ 'fetch',
109
+ vi.fn(async (url: string) => {
110
+ if (url.endsWith('pack-index.json')) {
111
+ return new Response(
112
+ JSON.stringify([
113
+ { guid: A, packageUrl: '/cycle.pack.json', kind: 'cycle-node', sourcePath: 'a' },
114
+ { guid: B, packageUrl: '/cycle.pack.json', kind: 'cycle-node', sourcePath: 'b' },
115
+ ]),
116
+ );
117
+ }
118
+ return new Response(JSON.stringify(pack));
119
+ }),
120
+ );
121
+
122
+ const result = await registry.loadByGuid(registry.parseGuid(A));
123
+ expect(result.ok).toBe(true);
124
+ expect(registry.lookup(A)).toBeDefined();
125
+ expect(registry.lookup(B)).toBeDefined();
126
+ });
127
+
128
+ it('keeps a public concurrent load pending until a cyclic ref is ready', async () => {
129
+ const registry = new AssetRegistry({} as never);
130
+ registry.loaders.register({
131
+ kind: 'race-node',
132
+ load: (payload) => ({ kind: 'race-node', ...payload }),
133
+ });
134
+ registry.configurePackIndex('/pack-index.json');
135
+ let releaseRef!: () => void;
136
+ const refStarted = new Promise<void>((resolve) => {
137
+ releaseRef = resolve;
138
+ });
139
+ let refRequested!: () => void;
140
+ const refRequest = new Promise<void>((resolve) => {
141
+ refRequested = resolve;
142
+ });
143
+ vi.stubGlobal(
144
+ 'fetch',
145
+ vi.fn(async (url: string) => {
146
+ if (url.endsWith('pack-index.json')) {
147
+ return new Response(
148
+ JSON.stringify([
149
+ { guid: A, packageUrl: '/race.pack.json', kind: 'race-node', sourcePath: 'a' },
150
+ { guid: B, packageUrl: '/race.pack.json', kind: 'race-node', sourcePath: 'b' },
151
+ ]),
152
+ );
153
+ }
154
+ if (url.endsWith('a.json')) {
155
+ return new Response(JSON.stringify({ format: 'race', emitters: [{ id: 'a' }] }));
156
+ }
157
+ if (url.endsWith('b.json')) {
158
+ refRequested();
159
+ await refStarted;
160
+ return new Response(JSON.stringify({ format: 'race', emitters: [{ id: 'b' }] }));
161
+ }
162
+ return new Response(
163
+ JSON.stringify({
164
+ schemaVersion: '2.0.0',
165
+ kind: 'internal-text-package',
166
+ assets: [
167
+ {
168
+ guid: A,
169
+ kind: 'race-node',
170
+ payload: { name: 'a' },
171
+ refs: [B],
172
+ artifacts: { program: { path: 'a.json', mediaType: 'application/json' } },
173
+ },
174
+ {
175
+ guid: B,
176
+ kind: 'race-node',
177
+ payload: { name: 'b' },
178
+ refs: [],
179
+ artifacts: { program: { path: 'b.json', mediaType: 'application/json' } },
180
+ },
181
+ ],
182
+ }),
183
+ );
184
+ }),
185
+ );
186
+
187
+ const first = registry.loadByGuid(registry.parseGuid(A));
188
+ await refRequest;
189
+ let publicSettled = false;
190
+ const concurrent = registry.loadByGuid(registry.parseGuid(A)).then((result) => {
191
+ publicSettled = true;
192
+ return result;
193
+ });
194
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
195
+ expect(publicSettled).toBe(false);
196
+
197
+ releaseRef();
198
+ const [firstResult, concurrentResult] = await Promise.all([first, concurrent]);
199
+ expect(firstResult.ok).toBe(true);
200
+ expect(concurrentResult).toEqual(firstResult);
201
+ });
202
+
203
+ it('waits for a shared sibling dependency while preserving SCC back-edges', async () => {
204
+ const registry = new AssetRegistry({} as never);
205
+ registry.loaders.register({
206
+ kind: 'shared-node',
207
+ load: (payload) => ({ kind: 'shared-node', ...payload }),
208
+ });
209
+ registry.configurePackIndex('/pack-index.json');
210
+ vi.stubGlobal(
211
+ 'fetch',
212
+ vi.fn(async (url: string) => {
213
+ if (url.endsWith('pack-index.json')) {
214
+ return new Response(
215
+ JSON.stringify([
216
+ { guid: A, packageUrl: '/shared.pack.json', kind: 'shared-node', sourcePath: 'a' },
217
+ { guid: B, packageUrl: '/shared.pack.json', kind: 'shared-node', sourcePath: 'b' },
218
+ { guid: C, packageUrl: '/shared.pack.json', kind: 'shared-node', sourcePath: 'c' },
219
+ { guid: D, packageUrl: '/shared.pack.json', kind: 'shared-node', sourcePath: 'd' },
220
+ ]),
221
+ );
222
+ }
223
+ return new Response(
224
+ JSON.stringify({
225
+ schemaVersion: '2.0.0',
226
+ kind: 'internal-text-package',
227
+ assets: [
228
+ { guid: A, kind: 'shared-node', payload: { name: 'a' }, refs: [B, C], artifacts: {} },
229
+ { guid: B, kind: 'shared-node', payload: { name: 'b' }, refs: [D], artifacts: {} },
230
+ { guid: C, kind: 'shared-node', payload: { name: 'c' }, refs: [D], artifacts: {} },
231
+ { guid: D, kind: 'shared-node', payload: { name: 'd' }, refs: [], artifacts: {} },
232
+ ],
233
+ }),
234
+ );
235
+ }),
236
+ );
237
+
238
+ const result = await registry.loadByGuid(registry.parseGuid(A));
239
+
240
+ expect(result.ok).toBe(true);
241
+ expect(registry.lookup(A)).toBeDefined();
242
+ expect(registry.lookup(B)).toBeDefined();
243
+ expect(registry.lookup(C)).toBeDefined();
244
+ expect(registry.lookup(D)).toBeDefined();
245
+ });
246
+ });