@forgeax/engine-assets-runtime 0.1.4 → 0.1.7

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 +453 -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 +554 -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 +143 -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 +7233 -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 +508 -0
  228. package/src/__tests__/material-stale-generation.unit.test.ts +147 -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 +27 -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 +2045 -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 +186 -0
  258. package/src/image-error.ts +45 -0
  259. package/src/index.ts +183 -16
  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 +385 -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
@@ -1,113 +0,0 @@
1
- import type { AssetLoadError } from '@forgeax/engine-types';
2
- import { err, ok } from '@forgeax/engine-types';
3
- import { describe, expect, it, vi } from 'vitest';
4
- import { AssetGraph } from '../internal/asset-graph.js';
5
-
6
- type Value = { readonly refs: readonly string[]; readonly value: string };
7
-
8
- const failure = (guid: string): AssetLoadError => ({
9
- code: 'asset-dependency-failed',
10
- expected: 'a readable dependency',
11
- hint: 'repair the dependency publication and retry',
12
- detail: { guid, dependencyGuid: guid },
13
- });
14
-
15
- describe('AssetGraph', () => {
16
- it('deduplicates shared reads, bounds concurrency, and promotes a closure atomically', async () => {
17
- let active = 0;
18
- let peak = 0;
19
- const reads = vi.fn(async (guid: string) => {
20
- active += 1;
21
- peak = Math.max(peak, active);
22
- await new Promise((resolve) => setTimeout(resolve, 1));
23
- active -= 1;
24
- if (guid === 'root') return ok({ value: guid, refs: ['shared', 'left'] });
25
- if (guid === 'left') return ok({ value: guid, refs: ['shared'] });
26
- return ok({ value: guid, refs: [] });
27
- });
28
- const graph = new AssetGraph<Value>({ read: reads, maxConcurrentReads: 2 });
29
-
30
- const [first, second] = await Promise.all([graph.load('root'), graph.load('root')]);
31
- expect(first).toEqual(second);
32
- expect(reads.mock.calls.filter(([guid]) => guid === 'shared')).toHaveLength(1);
33
- expect(peak).toBeLessThanOrEqual(2);
34
- expect(graph.snapshot().ready).toEqual(['left', 'root', 'shared']);
35
- expect(graph.snapshot().pending).toBe(0);
36
- });
37
-
38
- it('terminates cycles as one SCC and does not deadlock', async () => {
39
- const reads = vi.fn(async (guid: string) =>
40
- ok<Value>({ value: guid, refs: [guid === 'a' ? 'b' : 'a'] }),
41
- );
42
- const graph = new AssetGraph<Value>({ read: reads, maxConcurrentReads: 1 });
43
-
44
- expect((await graph.load('a')).ok).toBe(true);
45
- expect(reads).toHaveBeenCalledTimes(2);
46
- expect(graph.snapshot().sccs).toContainEqual(['a', 'b']);
47
- });
48
-
49
- it('clears failed pending work so a repaired publication can retry', async () => {
50
- let repaired = false;
51
- const reads = vi.fn(async (guid: string) => {
52
- if (guid === 'child' && !repaired) return err(failure(guid));
53
- return ok<Value>({ value: guid, refs: [] });
54
- });
55
- const graph = new AssetGraph<Value>({ read: reads });
56
-
57
- expect((await graph.load('child')).ok).toBe(false);
58
- expect(graph.snapshot().pending).toBe(0);
59
- repaired = true;
60
- expect((await graph.load('child')).ok).toBe(true);
61
- expect(reads).toHaveBeenCalledTimes(2);
62
- });
63
-
64
- it('fences an in-flight read after publication invalidation', async () => {
65
- let release: (() => void) | undefined;
66
- let reads = 0;
67
- const graph = new AssetGraph<Value>({
68
- read: async (guid) => {
69
- reads += 1;
70
- await new Promise<void>((resolve) => {
71
- release = resolve;
72
- });
73
- return ok<Value>({ value: guid, refs: [] });
74
- },
75
- });
76
-
77
- const first = graph.load('asset');
78
- graph.invalidate('asset');
79
- release?.();
80
- expect(await first).toEqual({
81
- ok: false,
82
- error: expect.objectContaining({ code: 'asset-superseded' }),
83
- });
84
- expect(reads).toBe(1);
85
- });
86
-
87
- it('resolves a queued permit with the terminal dispose result', async () => {
88
- let release: (() => void) | undefined;
89
- const graph = new AssetGraph<Value>({
90
- maxConcurrentReads: 1,
91
- read: async (guid) => {
92
- await new Promise<void>((resolve) => {
93
- release = resolve;
94
- });
95
- return ok<Value>({ value: guid, refs: [] });
96
- },
97
- });
98
-
99
- const first = graph.load('first');
100
- await Promise.resolve();
101
- const second = graph.load('second');
102
- graph.dispose();
103
- expect(await second).toEqual({
104
- ok: false,
105
- error: expect.objectContaining({ code: 'asset-runtime-disposed' }),
106
- });
107
- release?.();
108
- expect(await first).toEqual({
109
- ok: false,
110
- error: expect.objectContaining({ code: 'asset-runtime-disposed' }),
111
- });
112
- });
113
- });
@@ -1,161 +0,0 @@
1
- import type { AssetDecoder, CatalogDelta, CatalogEntry } from '@forgeax/engine-types';
2
- import { ok } from '@forgeax/engine-types';
3
- import { describe, expect, it } from 'vitest';
4
- import { defineAssetKind } from '../asset-kind.js';
5
- import { createCatalogSource } from '../catalog-source.js';
6
- import { createAssetRegistry } from '../internal/load-asset.js';
7
-
8
- const ROOT = '00000000-0000-7000-8000-000000000021';
9
- const CHILD = '00000000-0000-7000-8000-000000000022';
10
- const PACKAGE_URL = 'https://assets.test/runtime.pack.json';
11
- const kind = defineAssetKind<unknown, 'host-blob'>('host-blob');
12
-
13
- function row(guid: string, refs: readonly string[] = [], revision = 1): CatalogEntry {
14
- return {
15
- guid,
16
- packageUrl: PACKAGE_URL,
17
- kind: kind.kind,
18
- sourcePath: `fixtures/${guid}`,
19
- revision: { rootId: 'root', digest: `sha256:catalog-${revision}`, observedAt: revision },
20
- publication: {
21
- schemaVersion: 'asset-publication/1',
22
- sourcePath: `fixtures/${guid}`,
23
- sourceRevision: 'source-1',
24
- generation: 1,
25
- digest: 'sha256:pack',
26
- outputSetDigest: 'sha256:outputs',
27
- outputs: [{ guid, sourceKey: guid, kind: kind.kind, digest: `sha256:${guid}`, refs }],
28
- receipt: {
29
- schemaVersion: 'asset-publication-receipt/1',
30
- sourcePath: `fixtures/${guid}`,
31
- sourceRevision: 'source-1',
32
- inputFingerprint: 'input-1',
33
- outputDigest: 'sha256:outputs',
34
- outputSetDigest: 'sha256:outputs',
35
- externalEvidence: [],
36
- },
37
- externalEvidence: [],
38
- },
39
- };
40
- }
41
-
42
- describe('AssetRegistry integration', () => {
43
- it('loads a real Pack closure through the owner decoder and reuses one Pack read', async () => {
44
- let packReads = 0;
45
- let artifactReads = 0;
46
- let publishCatalog: ((delta: CatalogDelta) => void) | undefined;
47
- const fetcher: typeof globalThis.fetch = async (input) => {
48
- const url = String(input);
49
- if (url === PACKAGE_URL) {
50
- packReads += 1;
51
- return new Response(
52
- JSON.stringify({
53
- schemaVersion: '2.0.0',
54
- kind: 'internal-text-package',
55
- scopeId: 'scope',
56
- generation: 1,
57
- digest: 'sha256:pack',
58
- outputSetDigest: 'sha256:outputs',
59
- assets: [
60
- {
61
- guid: ROOT,
62
- kind: kind.kind,
63
- payload: { label: 'root' },
64
- refs: [CHILD],
65
- artifacts: {
66
- preview: {
67
- path: 'preview.bin',
68
- mediaType: 'application/octet-stream',
69
- contentEncoding: 'identity',
70
- byteLength: 3,
71
- integrity: {
72
- algorithm: 'sha256',
73
- digest:
74
- 'sha256:039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81',
75
- },
76
- },
77
- },
78
- },
79
- {
80
- guid: CHILD,
81
- kind: kind.kind,
82
- payload: { label: 'child' },
83
- refs: [],
84
- artifacts: {},
85
- },
86
- ],
87
- }),
88
- );
89
- }
90
- if (url === 'https://assets.test/preview.bin') {
91
- artifactReads += 1;
92
- return new Response(new Uint8Array([1, 2, 3]));
93
- }
94
- return new Response('not found', { status: 404 });
95
- };
96
- const decoder: AssetDecoder<unknown> = {
97
- decode: async ({ envelope, artifacts }) => {
98
- if (envelope.guid === ROOT) {
99
- const preview = await artifacts.read(
100
- envelope.artifacts.preview as NonNullable<typeof envelope.artifacts.preview>,
101
- );
102
- if (!preview.ok) return preview;
103
- return ok({
104
- child: envelope.refs[0],
105
- inline: (envelope.payload as { label: string }).label,
106
- preview: [...preview.value],
107
- });
108
- }
109
- return ok({ inline: (envelope.payload as { label: string }).label });
110
- },
111
- };
112
- const core = createAssetRegistry({
113
- catalog: createCatalogSource({
114
- entries: [row(ROOT, [CHILD]), row(CHILD)],
115
- expectedScope: { scopeId: 'scope', generation: 1 },
116
- subscribe(listener) {
117
- publishCatalog = listener;
118
- return () => {};
119
- },
120
- }),
121
- fetcher,
122
- maxConcurrentReads: 2,
123
- });
124
- core.installDecoder(kind, decoder);
125
-
126
- const result = await core.load(ROOT, kind);
127
-
128
- expect(result).toEqual({
129
- ok: true,
130
- value: { child: CHILD, inline: 'root', preview: [1, 2, 3] },
131
- });
132
- expect(result.ok && Object.isFrozen(result.value)).toBe(true);
133
- expect(packReads).toBe(1);
134
- expect(artifactReads).toBe(1);
135
- expect(core.snapshot().ready).toHaveLength(2);
136
- expect(core.snapshot().resources).toBe(2);
137
- publishCatalog?.({
138
- added: [],
139
- changed: [row(ROOT, [CHILD], 2)],
140
- removed: [],
141
- });
142
- expect(core.snapshot().resources).toBe(1);
143
- expect(core.snapshot().ready).toEqual([CHILD]);
144
-
145
- publishCatalog?.({
146
- added: [],
147
- changed: [row(ROOT, [CHILD], 3)],
148
- removed: [],
149
- revisions: {
150
- baseline: [{ rootId: 'root', revision: 1 }],
151
- current: [{ rootId: 'root', revision: 3 }],
152
- },
153
- });
154
- expect(core.snapshot().resources).toBe(0);
155
- const recovered = await core.load(ROOT, kind);
156
- expect(recovered.ok).toBe(true);
157
- expect(core.snapshot().resources).toBe(2);
158
- expect(packReads).toBe(1);
159
- core.dispose();
160
- });
161
- });
@@ -1,31 +0,0 @@
1
- import { describe, expectTypeOf, it } from 'vitest';
2
- import type { CatalogSource } from '../catalog-source.js';
3
- import {
4
- type AssetRegistry,
5
- type AssetRegistryOptions,
6
- type AssetRegistrySnapshot,
7
- createAssetRegistry,
8
- } from '../index.js';
9
-
10
- describe('AssetRegistry public API', () => {
11
- it('binds the factory and options to the root registry interface', () => {
12
- const options = { catalog: null as unknown as CatalogSource } satisfies AssetRegistryOptions;
13
- expectTypeOf(createAssetRegistry(options)).toEqualTypeOf<AssetRegistry>();
14
- });
15
-
16
- it('exposes the graph observation through registry snapshot types', () => {
17
- const registry = null as unknown as AssetRegistry;
18
- expectTypeOf(registry.snapshot()).toEqualTypeOf<AssetRegistrySnapshot>();
19
- expectTypeOf(registry.snapshot().ready).toEqualTypeOf<readonly string[]>();
20
- expectTypeOf(registry.snapshot().counters.loads).toEqualTypeOf<number>();
21
- });
22
-
23
- it('keeps renderer payload projection out of the five-action root surface', () => {
24
- const registry = null as unknown as AssetRegistry;
25
- expectTypeOf(registry.installDecoder).toBeFunction();
26
- expectTypeOf(registry.load).toBeFunction();
27
- expectTypeOf(registry.snapshot).toBeFunction();
28
- expectTypeOf(registry.subscribe).toBeFunction();
29
- expectTypeOf(registry.dispose).toBeFunction();
30
- });
31
- });
@@ -1,79 +0,0 @@
1
- import type { CatalogEntry } from '@forgeax/engine-types';
2
- import { ok } from '@forgeax/engine-types';
3
- import { describe, expect, it } from 'vitest';
4
- import { defineAssetKind } from '../asset-kind.js';
5
- import { createCatalogSource } from '../catalog-source.js';
6
- import { createAssetRegistry } from '../internal/load-asset.js';
7
-
8
- const GUID = '00000000-0000-7000-8000-000000000011';
9
- const kind = defineAssetKind<string, 'host-blob'>('host-blob');
10
-
11
- function row(): CatalogEntry {
12
- return {
13
- guid: GUID,
14
- packageUrl: '/asset.pack.json',
15
- kind: kind.kind,
16
- sourcePath: 'test',
17
- publication: {
18
- schemaVersion: 'asset-publication/1',
19
- sourcePath: 'test',
20
- sourceRevision: 'source-1',
21
- generation: 1,
22
- digest: 'sha256:pack',
23
- outputSetDigest: 'sha256:outputs',
24
- outputs: [
25
- { guid: GUID, sourceKey: 'test', kind: kind.kind, digest: 'sha256:asset', refs: [] },
26
- ],
27
- receipt: {
28
- schemaVersion: 'asset-publication-receipt/1',
29
- sourcePath: 'test',
30
- sourceRevision: 'source-1',
31
- inputFingerprint: 'input-1',
32
- outputDigest: 'sha256:outputs',
33
- outputSetDigest: 'sha256:outputs',
34
- externalEvidence: [],
35
- },
36
- externalEvidence: [],
37
- },
38
- };
39
- }
40
-
41
- describe('AssetRegistry lifecycle', () => {
42
- it('does not promote a late Pack result after dispose', async () => {
43
- let resolveFetch: ((response: Response) => void) | undefined;
44
- const fetcher = () =>
45
- new Promise<Response>((resolve) => {
46
- resolveFetch = resolve;
47
- });
48
- const core = createAssetRegistry({
49
- catalog: createCatalogSource({ entries: [row()] }),
50
- fetcher,
51
- scopeId: 'scope',
52
- generation: 1,
53
- });
54
- core.installDecoder(kind, {
55
- decode: async ({ envelope }) => ok(envelope.payload as string),
56
- });
57
- const request = core.load(GUID, kind);
58
- core.dispose();
59
- resolveFetch?.(
60
- new Response(
61
- JSON.stringify({
62
- schemaVersion: '2.0.0',
63
- kind: 'internal-text-package',
64
- scopeId: 'scope',
65
- generation: 1,
66
- digest: 'sha256:pack',
67
- outputSetDigest: 'sha256:outputs',
68
- assets: [{ guid: GUID, kind: kind.kind, payload: 'value', refs: [], artifacts: {} }],
69
- }),
70
- ),
71
- );
72
- expect(await request).toEqual({
73
- ok: false,
74
- error: expect.objectContaining({ code: 'asset-runtime-disposed' }),
75
- });
76
- expect(core.snapshot().pending).toBe(0);
77
- expect(core.snapshot().resources).toBe(0);
78
- });
79
- });
@@ -1,23 +0,0 @@
1
- import { ok } from '@forgeax/engine-types';
2
- import { describe, expect, it } from 'vitest';
3
- import { AssetGraph } from '../internal/asset-graph.js';
4
-
5
- describe('asset runtime snapshot counters', () => {
6
- it('reports no-change loads without new reads and keeps bounded counters', async () => {
7
- let reads = 0;
8
- const graph = new AssetGraph<{ readonly refs: readonly string[]; readonly value: string }>({
9
- read: async (guid) => {
10
- reads += 1;
11
- return ok({ value: guid, refs: [] });
12
- },
13
- });
14
- await graph.load('same');
15
- const before = graph.snapshot();
16
- await graph.load('same');
17
- const after = graph.snapshot();
18
- expect(reads).toBe(1);
19
- expect(after.counters.noChange).toBe(before.counters.noChange + 1);
20
- expect(after.resources).toBe(1);
21
- expect(after.pending).toBe(0);
22
- });
23
- });
@@ -1,276 +0,0 @@
1
- import type { CatalogDelta, CatalogEntry } from '@forgeax/engine-types';
2
- import { ok } from '@forgeax/engine-types';
3
- import { describe, expect, it } from 'vitest';
4
- import { createCatalogSource } from '../catalog-source.js';
5
- import { ArtifactCache } from '../internal/artifact-cache.js';
6
- import { CatalogSession } from '../internal/catalog-session.js';
7
- import { PackReader } from '../internal/pack-reader.js';
8
-
9
- const GUID = '00000000-0000-7000-8000-000000000001';
10
-
11
- function entry(guid = GUID, revision = 1): CatalogEntry {
12
- return {
13
- guid,
14
- packageUrl: '/runtime.pack.json',
15
- kind: 'host-blob',
16
- sourcePath: 'test',
17
- revision: { rootId: 'root', digest: `sha256:revision-${revision}`, observedAt: revision },
18
- publication: {
19
- schemaVersion: 'asset-publication/1',
20
- sourcePath: 'test',
21
- sourceRevision: `source-${revision}`,
22
- generation: 1,
23
- digest: 'sha256:pack',
24
- outputSetDigest: 'sha256:outputs',
25
- outputs: [{ guid, sourceKey: guid, kind: 'host-blob', digest: 'sha256:asset', refs: [] }],
26
- receipt: {
27
- schemaVersion: 'asset-publication-receipt/1',
28
- sourcePath: 'test',
29
- sourceRevision: `source-${revision}`,
30
- inputFingerprint: 'input',
31
- outputDigest: 'sha256:outputs',
32
- outputSetDigest: 'sha256:outputs',
33
- externalEvidence: [],
34
- },
35
- externalEvidence: [],
36
- },
37
- };
38
- }
39
-
40
- describe('CatalogSession', () => {
41
- it('rejects a Catalog row without a complete publication at the runtime boundary', async () => {
42
- const invalidRow = { ...entry() };
43
- delete (invalidRow as { publication?: CatalogEntry['publication'] }).publication;
44
- const session = new CatalogSession(createCatalogSource({ entries: [invalidRow] }));
45
-
46
- expect(await session.start()).toEqual({
47
- ok: false,
48
- error: expect.objectContaining({ code: 'asset-package-invalid' }),
49
- });
50
- });
51
-
52
- it('turns a thrown Catalog enumeration into a structured runtime failure', async () => {
53
- const session = new CatalogSession({
54
- enumerate: async () => {
55
- throw new Error('catalog transport unavailable');
56
- },
57
- subscribe: () => () => {},
58
- });
59
-
60
- await expect(session.start()).resolves.toMatchObject({
61
- ok: false,
62
- error: { code: 'asset-package-invalid' },
63
- });
64
- expect(session.snapshot().stale).toBe(true);
65
- });
66
-
67
- it('binds one scope and rejects a delta from another scope', async () => {
68
- let publish: ((delta: CatalogDelta) => void) | undefined;
69
- const source = createCatalogSource({
70
- entries: [entry()],
71
- expectedScope: { scopeId: 'scope-a', generation: 4 },
72
- subscribe(listener) {
73
- publish = listener;
74
- return () => {};
75
- },
76
- });
77
- const session = new CatalogSession(source);
78
-
79
- expect((await session.start()).ok).toBe(true);
80
- expect(session.snapshot().scopeId).toBe('scope-a');
81
- expect(session.snapshot().generation).toBe(4);
82
- publish?.({
83
- scopeId: 'scope-b',
84
- generation: 4,
85
- added: [entry('00000000-0000-7000-8000-000000000002', 2)],
86
- changed: [],
87
- removed: [],
88
- });
89
-
90
- expect(session.snapshot().entries).toHaveLength(1);
91
- expect(session.snapshot().diagnostics.map((item) => item.code)).toContain(
92
- 'catalog-scope-mismatch',
93
- );
94
- });
95
-
96
- it('serializes reconcile and preserves continuity across ordered deltas', async () => {
97
- const deltas: CatalogDelta[] = [];
98
- const source = createCatalogSource({
99
- entries: [entry()],
100
- subscribe(listener) {
101
- deltas.push({
102
- added: [],
103
- changed: [entry(GUID, 2)],
104
- removed: [],
105
- revisions: {
106
- baseline: [{ rootId: 'root', revision: 1 }],
107
- current: [{ rootId: 'root', revision: 2 }],
108
- },
109
- });
110
- listener(deltas[0] as CatalogDelta);
111
- return () => {};
112
- },
113
- });
114
- const session = new CatalogSession(source, { scopeId: 'scope', generation: 1 });
115
-
116
- await session.start();
117
- expect(session.current(GUID)?.revision?.observedAt).toBe(2);
118
- expect((await session.reconcile()).ok).toBe(true);
119
- expect(session.snapshot().epoch).toBeGreaterThan(0);
120
- });
121
-
122
- it('marks a discontinuous delta and clears the terminal observation on reconcile', async () => {
123
- let publish: ((delta: CatalogDelta) => void) | undefined;
124
- const source = createCatalogSource({
125
- entries: [entry()],
126
- subscribe(listener) {
127
- publish = listener;
128
- return () => {};
129
- },
130
- });
131
- const session = new CatalogSession(source, { scopeId: 'scope', generation: 1 });
132
-
133
- await session.start();
134
- publish?.({
135
- added: [],
136
- changed: [entry(GUID, 3)],
137
- removed: [],
138
- revisions: {
139
- baseline: [{ rootId: 'root', revision: 1 }],
140
- current: [{ rootId: 'root', revision: 3 }],
141
- },
142
- });
143
-
144
- expect(session.snapshot().stale).toBe(true);
145
- expect(session.snapshot().epoch).toBeGreaterThan(0);
146
- expect(session.discontinuity()).toMatchObject({ code: 'catalog-discontinuous' });
147
-
148
- expect((await session.reconcile()).ok).toBe(true);
149
- expect(session.snapshot().stale).toBe(false);
150
- expect(session.snapshot().diagnostics).toEqual([]);
151
- expect(session.discontinuity()).toBeUndefined();
152
- });
153
-
154
- it('does not expose mutable session state', async () => {
155
- const session = new CatalogSession(createCatalogSource({ entries: [entry()] }), {
156
- scopeId: 'scope',
157
- generation: 1,
158
- });
159
- await session.start();
160
- const snapshot = session.snapshot();
161
- expect(Object.isFrozen(snapshot)).toBe(true);
162
- expect(Object.isFrozen(snapshot.entries)).toBe(true);
163
- expect(ok(snapshot.entries[0])).toEqual(ok(snapshot.entries[0]));
164
- });
165
-
166
- it('records observer failures without starving later observers', async () => {
167
- const session = new CatalogSession(createCatalogSource({ entries: [entry()] }));
168
- const seen: number[] = [];
169
- session.subscribe(() => {
170
- throw new Error('observer failure');
171
- });
172
- session.subscribe((snapshot) => seen.push(snapshot.entries.length));
173
-
174
- await session.start();
175
-
176
- expect(seen).toContain(1);
177
- expect(session.snapshot().listenerFailures).toBe(1);
178
- });
179
-
180
- it('accepts only the current Pack v2 tuple and complete artifact descriptors', async () => {
181
- const tuple = {
182
- scopeId: 'scope',
183
- generation: 1,
184
- digest: 'sha256:pack',
185
- outputSetDigest: 'sha256:outputs',
186
- };
187
- const reader = new PackReader();
188
- const result = reader.verify(
189
- {
190
- schemaVersion: '2.0.0',
191
- kind: 'internal-text-package',
192
- ...tuple,
193
- assets: [
194
- {
195
- guid: GUID,
196
- kind: 'host-blob',
197
- payload: { value: 'ok' },
198
- refs: [],
199
- artifacts: {
200
- body: {
201
- path: 'body.bin',
202
- mediaType: 'application/octet-stream',
203
- contentEncoding: 'identity',
204
- byteLength: 2,
205
- integrity: {
206
- algorithm: 'sha256',
207
- digest: 'sha256:230d8358dc8e8890b4c58deeb62912ee2f20357ae92a5cc861b98e68fe31acb5',
208
- },
209
- },
210
- },
211
- },
212
- ],
213
- },
214
- tuple,
215
- );
216
- expect(result.ok).toBe(true);
217
- if (result.ok) expect(Object.isFrozen(result.value.assets[0])).toBe(true);
218
- expect(reader.verify({ schemaVersion: '1.0.0' }, tuple).ok).toBe(false);
219
- });
220
-
221
- it('resolves a late-installed host fetch when no transport was injected', async () => {
222
- const tuple = {
223
- scopeId: 'scope',
224
- generation: 1,
225
- digest: 'sha256:pack',
226
- outputSetDigest: 'sha256:outputs',
227
- };
228
- const pack = {
229
- schemaVersion: '2.0.0',
230
- kind: 'internal-text-package',
231
- ...tuple,
232
- assets: [
233
- {
234
- guid: GUID,
235
- kind: 'host-blob',
236
- payload: { value: 'late-fetch' },
237
- refs: [],
238
- artifacts: {},
239
- },
240
- ],
241
- };
242
- const originalFetch = globalThis.fetch;
243
- try {
244
- const reader = new PackReader();
245
- globalThis.fetch = (async () => new Response(JSON.stringify(pack))) as typeof fetch;
246
- const result = await reader.read('/late.pack.json', tuple, new AbortController().signal);
247
- expect(result.ok).toBe(true);
248
- } finally {
249
- globalThis.fetch = originalFetch;
250
- }
251
- });
252
-
253
- it('deduplicates content-addressed artifact reads and removes rejected entries', async () => {
254
- const cache = new ArtifactCache();
255
- let reads = 0;
256
- const read = () => {
257
- reads += 1;
258
- return Promise.resolve(ok(new Uint8Array([1, 2, 3])));
259
- };
260
- await Promise.all([cache.read('sha256:body', read), cache.read('sha256:body', read)]);
261
- expect(reads).toBe(1);
262
- expect(cache.snapshot().hits).toBe(1);
263
- await cache.read('sha256:failed', () =>
264
- Promise.resolve({
265
- ok: false as const,
266
- error: {
267
- code: 'asset-fetch-failed' as const,
268
- expected: 'bytes',
269
- hint: 'retry',
270
- detail: { guid: GUID, packageUrl: '/body.bin' },
271
- },
272
- } as never),
273
- );
274
- expect(cache.snapshot().pending).toBe(0);
275
- });
276
- });