@pixel-point/toolcraft 0.0.16 → 0.0.17

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 (170) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +68 -18
  6. package/src/generate-test-doc-assertions.test.mjs +22 -0
  7. package/src/generate-test-file-assertions.mjs +43 -76
  8. package/src/generate-test-output-assertions.mjs +95 -0
  9. package/src/generate-test-package-assertions.mjs +6 -0
  10. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  11. package/src/generate-test-product-fixture.mjs +1 -0
  12. package/src/generate-test-product-fixture.test.mjs +18 -0
  13. package/src/generate-test-product-iteration.mjs +1 -0
  14. package/src/generate-test-product-iteration.test.mjs +1 -0
  15. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  16. package/src/generate.test.mjs +14 -0
  17. package/templates/runtime/contracts/component-contracts.choices.test.ts +12 -9
  18. package/templates/runtime/contracts/component-contracts.choices.ts +10 -9
  19. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +17 -2
  20. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +1 -1
  21. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +1 -1
  22. package/templates/runtime/contracts/component-contracts.runtime.ts +3 -2
  23. package/templates/runtime/contracts/decision-contracts.test.ts +25 -4
  24. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  25. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  26. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  27. package/templates/runtime/export/artifact-download.test.ts +71 -0
  28. package/templates/runtime/export/artifact-download.ts +80 -0
  29. package/templates/runtime/export/artifact-export-request.ts +23 -0
  30. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  31. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  32. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  33. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  34. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  35. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  36. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  37. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  38. package/templates/runtime/export/export-background.ts +65 -0
  39. package/templates/runtime/export/export-error.ts +43 -0
  40. package/templates/runtime/export/export.test.ts +6 -186
  41. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  42. package/templates/runtime/export/image-artifact-export.ts +145 -0
  43. package/templates/runtime/export/index.ts +14 -1
  44. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  45. package/templates/runtime/export/product-export-renderer.ts +63 -0
  46. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  47. package/templates/runtime/export/video-artifact-export.ts +164 -0
  48. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  49. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  50. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  51. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  52. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  53. package/templates/runtime/index.ts +1 -0
  54. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  55. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  56. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +4 -303
  57. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +12 -27
  58. package/templates/runtime/react/app-shell/toolcraft-app.tsx +22 -4
  59. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  60. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  61. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  62. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  63. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  64. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  65. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  66. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  67. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  68. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +17 -83
  69. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +42 -84
  70. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +62 -0
  71. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  72. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  73. package/templates/runtime/scene/scene-bounds.ts +1 -2
  74. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  75. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  76. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +3 -3
  77. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  78. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  79. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  80. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  81. package/templates/runtime/schema/types.ts +4 -4
  82. package/templates/runtime/state/canvas-background-state.ts +6 -0
  83. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  84. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  85. package/templates/starter/AGENTS.md +8 -7
  86. package/templates/starter/NOTICE.md +6 -0
  87. package/templates/starter/docs/toolcraft/README.md +2 -1
  88. package/templates/starter/docs/toolcraft/acceptance-testing.md +8 -6
  89. package/templates/starter/docs/toolcraft/agent-worklog.md +3 -3
  90. package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -3
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -2
  92. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  93. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +3 -1
  94. package/templates/starter/docs/toolcraft/core/setup-export.md +16 -12
  95. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  96. package/templates/starter/docs/toolcraft/performance.md +20 -6
  97. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  98. package/templates/starter/docs/toolcraft/schema-reference.md +2 -6
  99. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  100. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  101. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +43 -1
  102. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  103. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  104. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  105. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  106. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +4 -1
  107. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +0 -1
  108. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  109. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  110. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  111. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +9 -330
  112. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  113. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  114. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  115. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  116. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +4 -1
  117. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +43 -9
  118. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  119. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  120. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  121. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  122. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  123. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  124. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  125. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  126. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  127. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  128. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  129. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  130. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  131. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  132. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  133. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  134. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  135. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  136. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  137. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  138. package/templates/starter/gitignore +1 -0
  139. package/templates/starter/package.json +1 -0
  140. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  141. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  142. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  143. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  144. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +24 -2
  145. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  146. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +46 -0
  147. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  148. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  149. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +168 -0
  150. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  151. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  152. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +155 -206
  153. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  154. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  155. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  156. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  157. package/templates/starter/src/app/acceptance/output-background-rules.ts +1 -1
  158. package/templates/starter/src/app/acceptance/output-export.ts +13 -2
  159. package/templates/starter/src/app/acceptance/types.ts +7 -0
  160. package/templates/starter/src/app/acceptance/validate-coverage.ts +2 -1
  161. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  162. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  163. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +42 -1
  164. package/templates/starter/src/app/starter-acceptance.ts +2 -0
  165. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  166. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  167. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  168. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -0
  169. package/templates/starter/src/routes/index.tsx +1 -0
  170. package/templates/runtime/export/export.ts +0 -166
@@ -1,28 +1,14 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  "use client";
4
2
 
5
3
  import * as React from "react";
6
4
 
7
- import {
8
- createToolcraftSourceAssetCoordinator,
9
- type ToolcraftSourceAssetCoordinator,
10
- } from "../../source-assets/source-asset-coordinator";
11
- import { createIndexedDbToolcraftBinaryAssetRepository } from "../../source-assets/repository/indexeddb-binary-asset-repository";
12
- import { createMemoryToolcraftBinaryAssetRepository } from "../../source-assets/repository/memory-binary-asset-repository";
13
- import { createUnavailableToolcraftBinaryAssetRepository } from "../../source-assets/repository/unavailable-binary-asset-repository";
14
- import type { ToolcraftBinaryAssetRepository } from "../../source-assets/repository/binary-asset-repository";
15
- import {
16
- createToolcraftSourceAssetPresentationResources,
17
- type ToolcraftSourceAssetPresentationResources,
18
- } from "../../source-assets/source-asset-presentation-resources";
19
5
  import {
20
6
  createToolcraftDefaultBinaryMediaHydrationJobs,
21
7
  getToolcraftBinaryMediaHydrationKey,
22
8
  type ToolcraftBinaryMediaHydrationJob,
23
9
  } from "../../source-assets/binary-media-hydration";
10
+ import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator";
24
11
  import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store";
25
- import type { ToolcraftImageAsset } from "../../state/types";
26
12
  import { ToolcraftModelRenderProvider } from "../model-rendering/model-render-provider";
27
13
  import { useToolcraftModelPresentationMode } from "../model-rendering/model-presentation-mode";
28
14
  import {
@@ -30,6 +16,8 @@ import {
30
16
  resolveToolcraftOrientationControl,
31
17
  } from "../orientation-gizmo/orientation-gizmo-selection";
32
18
  import { ToolcraftMediaPresentationProvider } from "./toolcraft-media-presentation";
19
+ import { acquireToolcraftSourceAssetOwner } from "./toolcraft-source-asset-owner";
20
+ import { decorateToolcraftUnavailableResourceProofCoordinator } from "./toolcraft-unavailable-resource-proof";
33
21
 
34
22
  export { useToolcraftMediaPresentationUrls } from "./toolcraft-media-presentation";
35
23
 
@@ -40,33 +28,13 @@ type ToolcraftSourceAssetCoordinatorFactory = (
40
28
  store: ToolcraftExternalStore,
41
29
  ) => ToolcraftSourceAssetCoordinator;
42
30
 
43
- type ToolcraftSourceAssetOwner = {
44
- activeUnavailableResourceProof: {
45
- assetId: string;
46
- requestId: string;
47
- resourceRef: string;
48
- } | null;
49
- coordinator: ToolcraftSourceAssetCoordinator;
50
- lifecycleGeneration: number;
51
- onDisposeError: (error: unknown) => void;
52
- presentationResources: ToolcraftSourceAssetPresentationResources | null;
53
- unavailableResourceProofRefs: Set<string>;
54
- };
31
+ type ToolcraftSourceAssetOwnerLease = ReturnType<
32
+ typeof acquireToolcraftSourceAssetOwner
33
+ >;
55
34
 
56
- const TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST =
57
- "toolcraft.browser-proof.unavailable-resource-request";
58
- const TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_ATTRIBUTE =
59
- "data-toolcraft-unavailable-resource-evidence";
60
- const toolcraftUnavailableResourceProofEnabled =
61
- import.meta.env?.MODE === "test" ||
62
- import.meta.env?.VITE_TOOLCRAFT_BROWSER_PROOF_FIXTURES ===
63
- "unavailable-resource";
64
-
65
- type ToolcraftUnavailableResourceProofRequest = Readonly<{
66
- action: "activate" | "cleanup";
67
- fileName: string;
68
- requestId: string;
69
- sourceTarget?: string;
35
+ type ToolcraftSourceAssetAcquisition = Readonly<{
36
+ lease: ToolcraftSourceAssetOwnerLease;
37
+ store: ToolcraftExternalStore;
70
38
  }>;
71
39
 
72
40
  function selectModelHydrationFingerprint(
@@ -94,166 +62,71 @@ function selectBinaryHydrationFingerprint(
94
62
  .join("\u0001");
95
63
  }
96
64
 
97
- const sourceAssetOwners = new WeakMap<
98
- ToolcraftExternalStore,
99
- ToolcraftSourceAssetOwner
100
- >();
101
-
102
- function createRepository(): ToolcraftBinaryAssetRepository {
103
- if (typeof globalThis.indexedDB === "undefined") {
104
- return typeof navigator !== "undefined" &&
105
- navigator.userAgent.toLowerCase().includes("jsdom")
106
- ? createMemoryToolcraftBinaryAssetRepository()
107
- : createUnavailableToolcraftBinaryAssetRepository();
108
- }
109
-
110
- try {
111
- return createIndexedDbToolcraftBinaryAssetRepository();
112
- } catch {
113
- return createUnavailableToolcraftBinaryAssetRepository();
114
- }
115
- }
116
-
117
- function reportSourceAssetDisposeError(error: unknown): void {
118
- const globalReportError = globalThis.reportError;
119
- if (typeof globalReportError === "function") {
120
- try {
121
- globalReportError(error);
122
- return;
123
- } catch (reportingFailure) {
124
- console.error(
125
- "Toolcraft source asset disposal reporting failed.",
126
- new AggregateError(
127
- [error, reportingFailure],
128
- "Toolcraft source asset disposal and reporting both failed.",
129
- ),
130
- );
131
- return;
132
- }
133
- }
134
- console.error("Toolcraft source asset disposal failed.", error);
135
- }
136
-
137
- const createDefaultCoordinator: ToolcraftSourceAssetCoordinatorFactory = (
138
- store,
139
- ) =>
140
- createToolcraftSourceAssetCoordinator({
141
- dispatch: store.dispatch,
142
- getState: store.getCommittedState,
143
- repository: createRepository(),
144
- });
145
-
146
- function getOrCreateSourceAssetOwner(
147
- store: ToolcraftExternalStore,
148
- createCoordinator: ToolcraftSourceAssetCoordinatorFactory,
149
- onDisposeError: (error: unknown) => void,
150
- ): ToolcraftSourceAssetOwner {
151
- const current = sourceAssetOwners.get(store);
152
- if (current) return current;
153
-
154
- const baseCoordinator = createCoordinator(store);
155
- const unavailableResourceProofRefs = new Set<string>();
156
- const proofScopedResolveResource = baseCoordinator.resolveResource
157
- ? async (
158
- ref: string,
159
- options: Readonly<{ signal: AbortSignal }>,
160
- ): Promise<Uint8Array | null> =>
161
- unavailableResourceProofRefs.has(ref)
162
- ? null
163
- : baseCoordinator.resolveResource!(ref, options)
164
- : undefined;
165
- const coordinator = proofScopedResolveResource
166
- ? {
167
- ...baseCoordinator,
168
- resolveResource: proofScopedResolveResource,
169
- }
170
- : baseCoordinator;
171
- const presentationResources =
172
- coordinator.resolveResource && coordinator.retainResourceRef
173
- ? createToolcraftSourceAssetPresentationResources({
174
- resolveResource: coordinator.resolveResource,
175
- retainResourceRef: coordinator.retainResourceRef,
176
- })
177
- : null;
178
- const owner = {
179
- activeUnavailableResourceProof: null,
180
- coordinator,
181
- lifecycleGeneration: 0,
182
- onDisposeError,
183
- presentationResources,
184
- unavailableResourceProofRefs,
185
- };
186
- sourceAssetOwners.set(store, owner);
187
- return owner;
188
- }
189
-
190
- function readUnavailableResourceProofRequest(
191
- event: Event,
192
- ): ToolcraftUnavailableResourceProofRequest | null {
193
- if (!(event instanceof CustomEvent)) return null;
194
- const detail: unknown = event.detail;
195
- if (typeof detail !== "object" || detail === null || Array.isArray(detail)) {
196
- return null;
197
- }
198
-
199
- const request = detail as Partial<ToolcraftUnavailableResourceProofRequest>;
200
- if (
201
- (request.action !== "activate" && request.action !== "cleanup") ||
202
- typeof request.fileName !== "string" ||
203
- request.fileName.trim() !== request.fileName ||
204
- request.fileName.length === 0 ||
205
- typeof request.requestId !== "string" ||
206
- request.requestId.trim() !== request.requestId ||
207
- request.requestId.length === 0 ||
208
- (
209
- request.sourceTarget !== undefined &&
210
- (
211
- typeof request.sourceTarget !== "string" ||
212
- request.sourceTarget.trim() !== request.sourceTarget ||
213
- request.sourceTarget.length === 0
214
- )
215
- )
216
- ) {
217
- return null;
218
- }
219
-
220
- return request as ToolcraftUnavailableResourceProofRequest;
221
- }
222
-
223
- function reportOwnerDisposeError(
224
- owner: ToolcraftSourceAssetOwner,
225
- error: unknown,
226
- ): void {
227
- try {
228
- owner.onDisposeError(error);
229
- } catch (reportingFailure) {
230
- reportSourceAssetDisposeError(
231
- new AggregateError(
232
- [error, reportingFailure],
233
- "Toolcraft source asset disposal and its injected reporter both failed.",
234
- ),
235
- );
236
- }
237
- }
238
-
239
65
  export function ToolcraftSourceAssetProvider({
240
66
  binaryMediaHydrationJobs = [],
241
67
  children,
242
- createCoordinator = createDefaultCoordinator,
243
- onDisposeError = reportSourceAssetDisposeError,
68
+ createCoordinator,
69
+ onDisposeError,
70
+ onError,
244
71
  store,
245
72
  }: {
246
73
  binaryMediaHydrationJobs?: readonly ToolcraftBinaryMediaHydrationJob[];
247
74
  children: React.ReactNode;
248
75
  createCoordinator?: ToolcraftSourceAssetCoordinatorFactory;
76
+ /** @deprecated Use onError. */
249
77
  onDisposeError?: (error: unknown) => void;
78
+ onError?: (error: unknown) => void;
250
79
  store: ToolcraftExternalStore;
251
80
  }): React.JSX.Element {
252
- const owner = getOrCreateSourceAssetOwner(
253
- store,
254
- createCoordinator,
255
- onDisposeError,
256
- );
81
+ const [acquisition, setAcquisition] =
82
+ React.useState<ToolcraftSourceAssetAcquisition | null>(null);
83
+ const activeLease = acquisition?.store === store
84
+ ? acquisition.lease
85
+ : null;
86
+ const constructionOnError = onError ?? onDisposeError;
87
+
88
+ // Construction inputs are captured when the store owner is acquired.
89
+ // Same-store callback identity changes intentionally keep that owner.
90
+ React.useLayoutEffect(() => {
91
+ const lease = acquireToolcraftSourceAssetOwner({
92
+ createCoordinator,
93
+ decorateCoordinator:
94
+ decorateToolcraftUnavailableResourceProofCoordinator,
95
+ onError: constructionOnError,
96
+ store,
97
+ });
98
+ setAcquisition({
99
+ lease,
100
+ store,
101
+ });
102
+ return lease.release;
103
+ }, [store]);
104
+
105
+ return activeLease
106
+ ? (
107
+ <ToolcraftSourceAssetProviderContent
108
+ binaryMediaHydrationJobs={binaryMediaHydrationJobs}
109
+ lease={activeLease}
110
+ store={store}
111
+ >
112
+ {children}
113
+ </ToolcraftSourceAssetProviderContent>
114
+ )
115
+ : <></>;
116
+ }
117
+
118
+ function ToolcraftSourceAssetProviderContent({
119
+ binaryMediaHydrationJobs,
120
+ children,
121
+ lease,
122
+ store,
123
+ }: {
124
+ binaryMediaHydrationJobs: readonly ToolcraftBinaryMediaHydrationJob[];
125
+ children: React.ReactNode;
126
+ lease: ToolcraftSourceAssetOwnerLease;
127
+ store: ToolcraftExternalStore;
128
+ }): React.JSX.Element {
129
+ const coordinator = lease.coordinator;
257
130
  const modelPresentation = useToolcraftModelPresentationMode();
258
131
  const customSourceTargets = React.useMemo(
259
132
  () => modelPresentation.mode === "custom"
@@ -333,8 +206,6 @@ export function ToolcraftSourceAssetProvider({
333
206
  );
334
207
 
335
208
  React.useEffect(() => {
336
- const generation = ++owner.lifecycleGeneration;
337
-
338
209
  const hydrateModels = (): void => {
339
210
  const currentTargets = new Set<string>();
340
211
  for (const asset of store.getCommittedState().mediaAssets) {
@@ -346,22 +217,18 @@ export function ToolcraftSourceAssetProvider({
346
217
  ...hydratedModelTargetsRef.current,
347
218
  ...currentTargets,
348
219
  ])) {
349
- owner.coordinator.clearPresentationFeedback(target);
220
+ coordinator.clearPresentationFeedback(target);
350
221
  }
351
222
  hydratedModelTargetsRef.current = currentTargets;
352
- void owner.coordinator.hydrateModels().catch((error: unknown) => {
353
- reportOwnerDisposeError(owner, error);
354
- });
223
+ void coordinator.hydrateModels().catch(lease.reportError);
355
224
  };
356
225
  const unsubscribeHydration = store.subscribeSelector(
357
226
  selectModelHydrationFingerprint,
358
227
  hydrateModels,
359
228
  );
360
229
  const hydrateBinaryMedia = (): void => {
361
- void owner.coordinator.hydrateBinaryMedia(binaryHydrationJobs).catch(
362
- (error: unknown) => {
363
- reportOwnerDisposeError(owner, error);
364
- },
230
+ void coordinator.hydrateBinaryMedia(binaryHydrationJobs).catch(
231
+ lease.reportError,
365
232
  );
366
233
  };
367
234
  const unsubscribeBinaryHydration = store.subscribeSelector(
@@ -374,258 +241,32 @@ export function ToolcraftSourceAssetProvider({
374
241
  return () => {
375
242
  unsubscribeHydration();
376
243
  unsubscribeBinaryHydration();
377
- queueMicrotask(() => {
378
- if (
379
- owner.lifecycleGeneration !== generation ||
380
- sourceAssetOwners.get(store) !== owner
381
- ) {
382
- return;
383
- }
384
-
385
- sourceAssetOwners.delete(store);
386
- try {
387
- owner.presentationResources?.dispose();
388
- void owner.coordinator.dispose().catch((error: unknown) => {
389
- reportOwnerDisposeError(owner, error);
390
- });
391
- } catch (error) {
392
- reportOwnerDisposeError(owner, error);
393
- }
394
- });
395
- };
396
- }, [binaryHydrationJobs, owner, store]);
397
-
398
- React.useEffect(() => {
399
- if (!toolcraftUnavailableResourceProofEnabled) return undefined;
400
-
401
- const rootSelector = '[data-slot="toolcraft-runtime-app"]';
402
- let pendingRequestId: string | null = null;
403
-
404
- const publish = (
405
- requestId: string,
406
- observation: Readonly<Record<string, boolean | string>>,
407
- ): void => {
408
- const root = document.querySelector<HTMLElement>(rootSelector);
409
- if (!root) return;
410
- root.setAttribute(
411
- TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_ATTRIBUTE,
412
- JSON.stringify({ requestId, ...observation }),
413
- );
414
- };
415
-
416
- const handleRequest = (event: Event): void => {
417
- const request = readUnavailableResourceProofRequest(event);
418
- if (!request) return;
419
-
420
- if (pendingRequestId) {
421
- publish(request.requestId, {
422
- message: `Unavailable-resource proof request "${pendingRequestId}" is still pending.`,
423
- status: "error",
424
- });
425
- return;
426
- }
427
- pendingRequestId = request.requestId;
428
-
429
- void (async () => {
430
- if (request.action === "cleanup") {
431
- const active = owner.activeUnavailableResourceProof;
432
- if (
433
- !active ||
434
- active.requestId !== request.requestId
435
- ) {
436
- throw new Error(
437
- "Unavailable-resource proof cleanup must match the active fixture request.",
438
- );
439
- }
440
-
441
- owner.unavailableResourceProofRefs.delete(active.resourceRef);
442
- const restoredBytes = await owner.coordinator.resolveResource?.(
443
- active.resourceRef,
444
- { signal: new AbortController().signal },
445
- );
446
- if (!restoredBytes?.byteLength) {
447
- throw new Error(
448
- "Unavailable-resource proof cleanup could not restore the real resolver.",
449
- );
450
- }
451
- store.dispatch({
452
- assetId: active.assetId,
453
- expectedResourceRef: active.resourceRef,
454
- lifecycle: "ready",
455
- type: "media.setBinaryResourceState",
456
- });
457
- const restored = store
458
- .getCommittedState()
459
- .mediaAssets.find((asset) => asset.id === active.assetId);
460
- if (
461
- !restored ||
462
- restored.assetKind !== "image" ||
463
- restored.lifecycle !== "ready"
464
- ) {
465
- throw new Error(
466
- "Unavailable-resource proof cleanup did not restore the typed resource state.",
467
- );
468
- }
469
-
470
- owner.activeUnavailableResourceProof = null;
471
- publish(request.requestId, {
472
- assetId: active.assetId,
473
- fileName: request.fileName,
474
- lifecycle: "ready",
475
- resolverAvailable: true,
476
- status: "cleaned",
477
- });
478
- return;
479
- }
480
-
481
- if (owner.activeUnavailableResourceProof) {
482
- throw new Error(
483
- "Unavailable-resource proof supports exactly one active resource fixture.",
484
- );
485
- }
486
- const candidates = store
487
- .getCommittedState()
488
- .mediaAssets.filter(
489
- (
490
- asset,
491
- ): asset is ToolcraftImageAsset & { lifecycle: "ready" } =>
492
- asset.assetKind === "image" &&
493
- asset.lifecycle === "ready" &&
494
- asset.fileName === request.fileName &&
495
- (
496
- request.sourceTarget === undefined ||
497
- asset.sourceTarget === request.sourceTarget
498
- ),
499
- );
500
- if (candidates.length !== 1) {
501
- throw new Error(
502
- `Unavailable-resource proof requires exactly one ready image named "${request.fileName}", received ${candidates.length}.`,
503
- );
504
- }
505
-
506
- const asset = candidates[0]!;
507
- const initialBytes = await owner.coordinator.resolveResource?.(
508
- asset.resourceRef,
509
- { signal: new AbortController().signal },
510
- );
511
- if (!initialBytes?.byteLength) {
512
- throw new Error(
513
- "Unavailable-resource proof requires a resource available through the real resolver before fault activation.",
514
- );
515
- }
516
-
517
- owner.unavailableResourceProofRefs.add(asset.resourceRef);
518
- try {
519
- const faultedBytes = await owner.coordinator.resolveResource?.(
520
- asset.resourceRef,
521
- { signal: new AbortController().signal },
522
- );
523
- if (faultedBytes !== null) {
524
- throw new Error(
525
- "Unavailable-resource proof did not isolate the requested resolver resource.",
526
- );
527
- }
528
- store.dispatch({
529
- assetId: asset.id,
530
- error: {
531
- code: "browser-proof-resource-unavailable",
532
- message:
533
- "The resource is intentionally unavailable for protected browser proof.",
534
- },
535
- expectedResourceRef: asset.resourceRef,
536
- lifecycle: "unavailable",
537
- type: "media.setBinaryResourceState",
538
- });
539
- const unavailable = store
540
- .getCommittedState()
541
- .mediaAssets.find((candidate) => candidate.id === asset.id);
542
- if (
543
- !unavailable ||
544
- unavailable.assetKind !== "image" ||
545
- unavailable.lifecycle !== "unavailable"
546
- ) {
547
- throw new Error(
548
- "Unavailable-resource proof did not reach the typed unavailable state.",
549
- );
550
- }
551
-
552
- owner.activeUnavailableResourceProof = {
553
- assetId: asset.id,
554
- requestId: request.requestId,
555
- resourceRef: asset.resourceRef,
556
- };
557
- publish(request.requestId, {
558
- assetId: asset.id,
559
- fileName: asset.fileName,
560
- lifecycle: "unavailable",
561
- resolverAvailable: false,
562
- status: "active",
563
- });
564
- } catch (error) {
565
- owner.unavailableResourceProofRefs.delete(asset.resourceRef);
566
- throw error;
567
- }
568
- })()
569
- .catch((error: unknown) => {
570
- publish(request.requestId, {
571
- message:
572
- error instanceof Error
573
- ? error.message
574
- : "Unavailable-resource proof fixture failed.",
575
- status: "error",
576
- });
577
- })
578
- .finally(() => {
579
- pendingRequestId = null;
580
- });
581
- };
582
-
583
- document.addEventListener(
584
- TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST,
585
- handleRequest,
586
- );
587
-
588
- return () => {
589
- document.removeEventListener(
590
- TOOLCRAFT_UNAVAILABLE_RESOURCE_PROOF_REQUEST,
591
- handleRequest,
592
- );
593
- const active = owner.activeUnavailableResourceProof;
594
- if (!active) return;
595
- owner.unavailableResourceProofRefs.delete(active.resourceRef);
596
- store.dispatch({
597
- assetId: active.assetId,
598
- expectedResourceRef: active.resourceRef,
599
- lifecycle: "ready",
600
- type: "media.setBinaryResourceState",
601
- });
602
- owner.activeUnavailableResourceProof = null;
603
244
  };
604
- }, [owner, store]);
245
+ }, [binaryHydrationJobs, coordinator, lease.reportError, store]);
605
246
 
606
247
  const context = (
607
248
  <ToolcraftMediaPresentationProvider
608
249
  bootstrapUrls={binaryBootstrapUrls}
609
- resources={owner.presentationResources}
250
+ resources={lease.presentationResources}
610
251
  >
611
- <ToolcraftSourceAssetCoordinatorContext.Provider value={owner.coordinator}>
252
+ <ToolcraftSourceAssetCoordinatorContext.Provider value={coordinator}>
612
253
  {children}
613
254
  </ToolcraftSourceAssetCoordinatorContext.Provider>
614
255
  </ToolcraftMediaPresentationProvider>
615
256
  );
616
257
 
617
- return owner.coordinator.resolveResource ? (
258
+ return coordinator.resolveResource ? (
618
259
  <ToolcraftModelRenderProvider
619
260
  activeCustomTargets={activeCustomTargets}
620
- clearPresentationFeedback={owner.coordinator.clearPresentationFeedback}
261
+ clearPresentationFeedback={coordinator.clearPresentationFeedback}
621
262
  customConsumers={
622
263
  modelPresentation.mode === "custom"
623
264
  ? modelPresentation.consumers
624
265
  : []
625
266
  }
626
- reportPresentationFeedback={owner.coordinator.reportPresentationFeedback}
627
- resolveResource={owner.coordinator.resolveResource}
628
- retainResourceRef={owner.coordinator.retainResourceRef}
267
+ reportPresentationFeedback={coordinator.reportPresentationFeedback}
268
+ resolveResource={coordinator.resolveResource}
269
+ retainResourceRef={coordinator.retainResourceRef}
629
270
  visibleOrientationTarget={visibleOrientationTarget || undefined}
630
271
  >
631
272
  {context}