@carbonenginejs/runtime-resource 0.1.0 → 0.7.0

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 (186) hide show
  1. package/FORMAT-PROVENANCE.md +14 -5
  2. package/README.md +537 -241
  3. package/dist/CjsMotherLode.js +1244 -29
  4. package/dist/CjsMotherLode.js.map +1 -1
  5. package/dist/CjsResMan.js +2537 -272
  6. package/dist/CjsResMan.js.map +1 -1
  7. package/dist/CjsResource.js +218 -18
  8. package/dist/CjsResource.js.map +1 -1
  9. package/dist/format/CjsBlueReader.js +269 -0
  10. package/dist/format/CjsBlueReader.js.map +1 -0
  11. package/dist/format/CjsFormat.js +44 -30
  12. package/dist/format/CjsFormat.js.map +1 -1
  13. package/dist/format/CjsReader.js +18 -0
  14. package/dist/format/CjsReader.js.map +1 -0
  15. package/dist/format/CjsResourceProbe.js +12 -4
  16. package/dist/format/CjsResourceProbe.js.map +1 -1
  17. package/dist/formats/black/CjsBlackFormat.js +12 -6
  18. package/dist/formats/black/CjsBlackFormat.js.map +1 -1
  19. package/dist/formats/black/core/CjsBlackReader.js +27 -148
  20. package/dist/formats/black/core/CjsBlackReader.js.map +1 -1
  21. package/dist/formats/bnk/CjsBnkFormat.js +140 -147
  22. package/dist/formats/bnk/CjsBnkFormat.js.map +1 -1
  23. package/dist/formats/bnk/core/graph.js +140 -0
  24. package/dist/formats/bnk/core/graph.js.map +1 -0
  25. package/dist/formats/bnk/core/helpers.js +58 -3
  26. package/dist/formats/bnk/core/helpers.js.map +1 -1
  27. package/dist/formats/bnk/core/musicNodes.js +521 -0
  28. package/dist/formats/bnk/core/musicNodes.js.map +1 -0
  29. package/dist/formats/bnk/index.js +0 -1
  30. package/dist/formats/bnk/index.js.map +1 -1
  31. package/dist/formats/cmf/CjsCmfFormat.js +14 -8
  32. package/dist/formats/cmf/CjsCmfFormat.js.map +1 -1
  33. package/dist/formats/dds/CjsDdsFormat.js +12 -6
  34. package/dist/formats/dds/CjsDdsFormat.js.map +1 -1
  35. package/dist/formats/fbx/CjsFbxFormat.js +12 -6
  36. package/dist/formats/fbx/CjsFbxFormat.js.map +1 -1
  37. package/dist/formats/flac/CjsFlacFormat.js +10 -4
  38. package/dist/formats/flac/CjsFlacFormat.js.map +1 -1
  39. package/dist/formats/gif/CjsGifFormat.js +11 -5
  40. package/dist/formats/gif/CjsGifFormat.js.map +1 -1
  41. package/dist/formats/gltf/CjsGltfFormat.js +12 -6
  42. package/dist/formats/gltf/CjsGltfFormat.js.map +1 -1
  43. package/dist/formats/jpeg/CjsJpegFormat.js +11 -5
  44. package/dist/formats/jpeg/CjsJpegFormat.js.map +1 -1
  45. package/dist/formats/mp3/CjsMp3Format.js +11 -5
  46. package/dist/formats/mp3/CjsMp3Format.js.map +1 -1
  47. package/dist/formats/mp4/CjsMp4Format.js +10 -4
  48. package/dist/formats/mp4/CjsMp4Format.js.map +1 -1
  49. package/dist/formats/obj/CjsObjFormat.js +12 -6
  50. package/dist/formats/obj/CjsObjFormat.js.map +1 -1
  51. package/dist/formats/ogg/CjsOggFormat.js +9 -3
  52. package/dist/formats/ogg/CjsOggFormat.js.map +1 -1
  53. package/dist/formats/png/CjsPngFormat.js +11 -5
  54. package/dist/formats/png/CjsPngFormat.js.map +1 -1
  55. package/dist/formats/red/CjsRedFormat.js +19 -5
  56. package/dist/formats/red/CjsRedFormat.js.map +1 -1
  57. package/dist/formats/red/core/CjsRedReader.js +63 -70
  58. package/dist/formats/red/core/CjsRedReader.js.map +1 -1
  59. package/dist/formats/red/core/helpers.js +3 -0
  60. package/dist/formats/red/core/helpers.js.map +1 -1
  61. package/dist/formats/stl/CjsStlFormat.js +77 -16
  62. package/dist/formats/stl/CjsStlFormat.js.map +1 -1
  63. package/dist/formats/stl/core/helpers.js +4 -0
  64. package/dist/formats/stl/core/helpers.js.map +1 -1
  65. package/dist/formats/stl/core/stl.js +100 -15
  66. package/dist/formats/stl/core/stl.js.map +1 -1
  67. package/dist/formats/tga/CjsTgaFormat.js +11 -5
  68. package/dist/formats/tga/CjsTgaFormat.js.map +1 -1
  69. package/dist/formats/wav/CjsWavFormat.js +11 -5
  70. package/dist/formats/wav/CjsWavFormat.js.map +1 -1
  71. package/dist/formats/webm/CjsWebmFormat.js +10 -4
  72. package/dist/formats/webm/CjsWebmFormat.js.map +1 -1
  73. package/dist/formats/webp/CjsWebpFormat.js +9 -3
  74. package/dist/formats/webp/CjsWebpFormat.js.map +1 -1
  75. package/dist/formats/wem/CjsWemFormat.js +50 -6
  76. package/dist/formats/wem/CjsWemFormat.js.map +1 -1
  77. package/dist/formats/wem/core/helpers.js +84 -3
  78. package/dist/formats/wem/core/helpers.js.map +1 -1
  79. package/dist/formats/wem/core/ptadpcm.js +77 -0
  80. package/dist/formats/wem/core/ptadpcm.js.map +1 -0
  81. package/dist/formats/wem/core/resolve.js +121 -0
  82. package/dist/formats/wem/core/resolve.js.map +1 -0
  83. package/dist/formats/yaml/CjsYamlFormat.js +11 -5
  84. package/dist/formats/yaml/CjsYamlFormat.js.map +1 -1
  85. package/dist/formats/yaml/core/CjsYamlReader.js +3 -2
  86. package/dist/formats/yaml/core/CjsYamlReader.js.map +1 -1
  87. package/dist/index.js +28 -32
  88. package/dist/index.js.map +1 -1
  89. package/dist/{generated/resources → resources}/AudioGeometryResData.js +2 -2
  90. package/dist/resources/AudioGeometryResData.js.map +1 -0
  91. package/dist/{generated/resources → resources}/GStateBindingCallbackData.js +2 -2
  92. package/dist/resources/GStateBindingCallbackData.js.map +1 -0
  93. package/dist/{generated/resources → resources}/MeshDecalData.js +2 -2
  94. package/dist/resources/MeshDecalData.js.map +1 -0
  95. package/dist/{generated/resources → resources}/MeshDecalLodData.js +2 -2
  96. package/dist/resources/MeshDecalLodData.js.map +1 -0
  97. package/dist/resources/Tr2GrannyIntersectionResult.js +60 -0
  98. package/dist/resources/Tr2GrannyIntersectionResult.js.map +1 -0
  99. package/dist/{generated/resources → resources}/Tr2MaterialArea.js +2 -2
  100. package/dist/resources/Tr2MaterialArea.js.map +1 -0
  101. package/dist/{generated/resources → resources}/Tr2MaterialMesh.js +2 -2
  102. package/dist/resources/Tr2MaterialMesh.js.map +1 -0
  103. package/dist/{generated/resources → resources}/Tr2MaterialRes.js +2 -2
  104. package/dist/resources/Tr2MaterialRes.js.map +1 -0
  105. package/dist/{generated/resources → resources}/Tr2ShaderPermutation.js +2 -2
  106. package/dist/resources/Tr2ShaderPermutation.js.map +1 -0
  107. package/dist/{generated/resources → resources}/Tr2TextureLodManager.js +50 -10
  108. package/dist/resources/Tr2TextureLodManager.js.map +1 -0
  109. package/dist/{generated/resources → resources}/Tr2TextureLodUpdateRequest.js +2 -2
  110. package/dist/resources/Tr2TextureLodUpdateRequest.js.map +1 -0
  111. package/dist/{generated/resources → resources}/Tr2TexturePackChannel.js +2 -2
  112. package/dist/resources/Tr2TexturePackChannel.js.map +1 -0
  113. package/dist/resources/Tr2TexturePipeline.js +68 -0
  114. package/dist/resources/Tr2TexturePipeline.js.map +1 -0
  115. package/dist/{generated/resources → resources}/Tr2TexturePipelineParams.js +2 -2
  116. package/dist/resources/Tr2TexturePipelineParams.js.map +1 -0
  117. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepCompress.js +3 -3
  118. package/dist/resources/Tr2TexturePipelineStepCompress.js.map +1 -0
  119. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepLimitSize.js +2 -2
  120. package/dist/resources/Tr2TexturePipelineStepLimitSize.js.map +1 -0
  121. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepLoad.js +2 -2
  122. package/dist/resources/Tr2TexturePipelineStepLoad.js.map +1 -0
  123. package/dist/{generated/resources → resources}/Tr2TexturePipelineStepPack.js +3 -3
  124. package/dist/resources/Tr2TexturePipelineStepPack.js.map +1 -0
  125. package/dist/{generated/resources → resources}/TriGeometryResAreaData.js +2 -2
  126. package/dist/resources/TriGeometryResAreaData.js.map +1 -0
  127. package/dist/{generated/resources → resources}/TriGeometryResJointData.js +2 -2
  128. package/dist/resources/TriGeometryResJointData.js.map +1 -0
  129. package/dist/{generated/resources → resources}/TriGeometryResLodData.js +2 -2
  130. package/dist/resources/TriGeometryResLodData.js.map +1 -0
  131. package/dist/{generated/resources → resources}/TriGeometryResMeshData.js +2 -2
  132. package/dist/resources/TriGeometryResMeshData.js.map +1 -0
  133. package/dist/{generated/resources → resources}/TriGeometryResSkeletonData.js +2 -2
  134. package/dist/resources/TriGeometryResSkeletonData.js.map +1 -0
  135. package/dist/{generated/resources → resources}/TriJointBinding.js +2 -2
  136. package/dist/resources/TriJointBinding.js.map +1 -0
  137. package/dist/{generated/resources → resources}/TriMorphTargetGeometryConstants.js +2 -2
  138. package/dist/resources/TriMorphTargetGeometryConstants.js.map +1 -0
  139. package/dist/{generated/resources → resources}/TriRtGeometryConstants.js +2 -2
  140. package/dist/resources/TriRtGeometryConstants.js.map +1 -0
  141. package/dist/resources/enums.js.map +1 -0
  142. package/dist/resources/texturePipelineBehavior.js +320 -0
  143. package/dist/resources/texturePipelineBehavior.js.map +1 -0
  144. package/format-notices/bnk/NOTICE +7 -4
  145. package/package.json +51 -51
  146. package/resource-lifecycle.md +262 -70
  147. package/dist/generated/resources/AudioGeometryResData.js.map +0 -1
  148. package/dist/generated/resources/CmfVertexReader.js +0 -46
  149. package/dist/generated/resources/CmfVertexReader.js.map +0 -1
  150. package/dist/generated/resources/GStateBindingCallbackData.js.map +0 -1
  151. package/dist/generated/resources/MeshDecalData.js.map +0 -1
  152. package/dist/generated/resources/MeshDecalLodData.js.map +0 -1
  153. package/dist/generated/resources/Tr2AsyncSave.js +0 -46
  154. package/dist/generated/resources/Tr2AsyncSave.js.map +0 -1
  155. package/dist/generated/resources/Tr2CmfContents.js +0 -37
  156. package/dist/generated/resources/Tr2CmfContents.js.map +0 -1
  157. package/dist/generated/resources/Tr2GrannyIntersectionResult.js +0 -58
  158. package/dist/generated/resources/Tr2GrannyIntersectionResult.js.map +0 -1
  159. package/dist/generated/resources/Tr2LoadPrepareFence.js +0 -37
  160. package/dist/generated/resources/Tr2LoadPrepareFence.js.map +0 -1
  161. package/dist/generated/resources/Tr2MaterialArea.js.map +0 -1
  162. package/dist/generated/resources/Tr2MaterialMesh.js.map +0 -1
  163. package/dist/generated/resources/Tr2MaterialRes.js.map +0 -1
  164. package/dist/generated/resources/Tr2ShaderPermutation.js.map +0 -1
  165. package/dist/generated/resources/Tr2TextureLodManager.js.map +0 -1
  166. package/dist/generated/resources/Tr2TextureLodUpdateRequest.js.map +0 -1
  167. package/dist/generated/resources/Tr2TexturePackChannel.js.map +0 -1
  168. package/dist/generated/resources/Tr2TexturePipeline.js +0 -44
  169. package/dist/generated/resources/Tr2TexturePipeline.js.map +0 -1
  170. package/dist/generated/resources/Tr2TexturePipelineParams.js.map +0 -1
  171. package/dist/generated/resources/Tr2TexturePipelineStepCompress.js.map +0 -1
  172. package/dist/generated/resources/Tr2TexturePipelineStepLimitSize.js.map +0 -1
  173. package/dist/generated/resources/Tr2TexturePipelineStepLoad.js.map +0 -1
  174. package/dist/generated/resources/Tr2TexturePipelineStepPack.js.map +0 -1
  175. package/dist/generated/resources/TriGeometryResAreaData.js.map +0 -1
  176. package/dist/generated/resources/TriGeometryResJointData.js.map +0 -1
  177. package/dist/generated/resources/TriGeometryResLodData.js.map +0 -1
  178. package/dist/generated/resources/TriGeometryResMeshData.js.map +0 -1
  179. package/dist/generated/resources/TriGeometryResSkeletonData.js.map +0 -1
  180. package/dist/generated/resources/TriJointBinding.js.map +0 -1
  181. package/dist/generated/resources/TriMorphTargetGeometryConstants.js.map +0 -1
  182. package/dist/generated/resources/TriRtGeometryConstants.js.map +0 -1
  183. package/dist/generated/resources/enums.js.map +0 -1
  184. package/dist/generated/resources/index.js +0 -32
  185. package/dist/generated/resources/index.js.map +0 -1
  186. /package/dist/{generated/resources → resources}/enums.js +0 -0
package/dist/CjsResMan.js CHANGED
@@ -1,10 +1,173 @@
1
- import { CjsMotherLode } from './CjsMotherLode.js';
1
+ import { CjsMotherLode, getMotherLodeKey } from './CjsMotherLode.js';
2
2
  import { CjsEventEmitter } from '@carbonenginejs/core-types/model';
3
3
  import { CjsResource as _CjsResource } from './CjsResource.js';
4
4
  import { CjsResManWorkQueue, CjsResManQueue, NormalizeCjsResManQueue } from './CjsResManQueue.js';
5
5
  import { normalizeResourceExtension, normalizeResourcePath, getResourceExtension } from './resourcePath.js';
6
6
 
7
+ /** @type {WeakMap<object, CjsResourceReadContext>} */
8
+ const READ_CONTEXTS = new WeakMap();
9
+
10
+ /** Small request fields retained only so a released payload can be rebuilt. */
11
+ const RESOURCE_REQUEST_OPTION_KEYS = Object.freeze(["variant", "requirement", "payload", "emit", "mediaType", "format", "classes", "formatOptions"]);
12
+
13
+ /** Requested-output fields pinned by a bound resource handle. */
14
+ const RESOURCE_OUTPUT_OPTION_KEYS = Object.freeze(["variant", "emit", "requirement", "payload"]);
15
+
16
+ /** Source provenance retained by a bound resource handle when present. */
17
+ const RESOURCE_PROVENANCE_OPTION_KEYS = Object.freeze(["source", "sourceRevision", "ext"]);
18
+
19
+ /** Process-local tokens used only by parsed-format operation caching. */
20
+ const LOCAL_VALUE_IDENTITIES = new WeakMap();
21
+ let nextLocalValueIdentity = 1;
22
+
23
+ /**
24
+ * Immutable source provenance used by one read/format operation chain.
25
+ *
26
+ * `sourceRevision` is caller/source-provided opaque content identity. It scopes
27
+ * read caches only and never becomes part of MotherLode resource identity.
28
+ *
29
+ * @typedef {object} CjsResourceReadContext
30
+ * @property {CjsResMan} resMan Owning manager.
31
+ * @property {object|Function} source Selected source implementation.
32
+ * @property {string} path Normalized Carbon-style source path.
33
+ * @property {string|number|undefined} sourceRevision Caller content token.
34
+ * @property {string} revisionKey Type-stable internal form of the content token.
35
+ */
36
+
37
+ /**
38
+ * Mutable record retained in a source or format operation ledger.
39
+ *
40
+ * @typedef {object} CjsResourceReadOperationRecord
41
+ * @property {Promise<*>} promise Shared operation promise.
42
+ * @property {string} path Normalized source path used for explicit invalidation.
43
+ * @property {string} revisionKey Type-stable source revision key.
44
+ * @property {boolean} retain Whether any joined caller requested settled retention.
45
+ */
46
+
47
+ /**
48
+ * Source provenance and tri-state read-cache controls.
49
+ *
50
+ * Omitted cache flags share in-flight or explicitly retained work but do not
51
+ * retain a newly completed operation. `true` retains success, while `false`
52
+ * bypasses sharing and retention. Failures are never retained.
53
+ *
54
+ * @typedef {object} CjsResManReadCacheOptions
55
+ * @property {object|Function} [source] Source override providing `Read(path, options)`.
56
+ * @property {string|number} [sourceRevision] Opaque caller/source content token; finite numbers only.
57
+ * @property {boolean} [reload=false] Invalidate this source/path and begin fresh work once.
58
+ * @property {boolean} [cacheSource] Source operation sharing/retention policy.
59
+ * @property {boolean} [cacheFormat] Parsed-format operation sharing/retention policy.
60
+ */
61
+
62
+ /**
63
+ * Opt-in time-based inactivity policy run by {@link CjsResMan#Update}.
64
+ *
65
+ * Automatic policy deliberately uses elapsed milliseconds rather than
66
+ * MotherLode activity frames: the latter count explicit activity observations
67
+ * and are not guaranteed to match renderer frames. At least one identity or
68
+ * payload limit is required. The policy is disabled by default.
69
+ *
70
+ * @typedef {object} CjsResManAutoPurgePolicy
71
+ * @property {number} [intervalMilliseconds=1000] Minimum elapsed milliseconds between automatic sweeps; `0` permits every update.
72
+ * @property {number} [maxIdleMilliseconds] Elapsed milliseconds after which an unlocked canonical identity is removed and cleaned.
73
+ * @property {number} [payloadMaxIdleMilliseconds] Elapsed milliseconds after which an unlocked CPU payload is released.
74
+ * @property {boolean} [destroyAdapters=true] Whether identity cleanup destroys adapter allocations.
75
+ * @property {boolean} [releasePayload=true] Whether sweeps may release CPU payloads.
76
+ * @property {false|Function} [cleanup] `false` retains owned state; a function replaces default identity cleanup.
77
+ * @property {() => number} [now=Date.now] Cadence clock. Use the MotherLode activity clock when supplying a custom clock.
78
+ */
79
+
80
+ /**
81
+ * Per-call controls for {@link CjsResMan#PumpAutoPurge}.
82
+ *
83
+ * @typedef {object} CjsResManAutoPurgePumpOptions
84
+ * @property {number} [time] Explicit non-negative timestamp in milliseconds for deterministic pumping and sweeping.
85
+ */
86
+
87
+ /**
88
+ * Queue and automatic-purge controls accepted by {@link CjsResMan#Update}.
89
+ * Top-level queue options remain a compatibility form for the prepare pump.
90
+ *
91
+ * @typedef {object} CjsResManUpdateOptions
92
+ * @property {object} [background] Background queue pump options.
93
+ * @property {object} [prepare] Main-thread prepare queue pump options.
94
+ * @property {false|object} [cache] `false` skips recorded-byte housekeeping; otherwise supplies cache-cleanup policy.
95
+ * @property {false|CjsResManAutoPurgePumpOptions} [purge] `false` skips this update's automatic sweep; otherwise supplies its timestamp.
96
+ */
97
+
98
+ /**
99
+ * Snapshot-fence controls accepted by {@link CjsResMan#Wait}.
100
+ *
101
+ * The default pump uses the manager's ordinary queue budgets and honors queue
102
+ * pause state. `pump: false` leaves all progress to an external driver. The
103
+ * yield callback is used only while this call is pumping and may return either
104
+ * a promise or an immediate value.
105
+ *
106
+ * @typedef {object} CjsResManWaitOptions
107
+ * @property {boolean} [pump=true] Whether this call pumps the two queues while awaiting its snapshot.
108
+ * @property {object} [background] Options forwarded to `PumpBackgroundQueue`.
109
+ * @property {object} [prepare] Options forwarded to `PumpMainThreadQueue`.
110
+ * @property {() => (*|Promise<*>)} [yield] Cooperative wait between pump attempts.
111
+ */
112
+
113
+ /**
114
+ * Immutable authority captured by asynchronous work that may mutate a
115
+ * canonical resource. A generation changes whenever ResMan removes and later
116
+ * rebinds the same JavaScript handle, even when owner and key are reused.
117
+ *
118
+ * @typedef {object} CjsResourceOwnership
119
+ * @property {number} generation Manager-local ownership generation.
120
+ * @property {CjsMotherLode} owner Exact registry that owns the resource.
121
+ * @property {string} key Exact canonical registry identity.
122
+ * @property {object|Function} resource Canonical resource handle.
123
+ */
124
+
125
+ /**
126
+ * Immutable authority for one off-registry reload candidate.
127
+ *
128
+ * The newest generation for a canonical key is the only candidate permitted
129
+ * to commit. `expectedOwnership` keeps the former canonical handle protected
130
+ * by the same exact-owner generation checks used by ordinary asynchronous
131
+ * resource work.
132
+ *
133
+ * @typedef {object} CjsResourceReloadCandidate
134
+ * @property {true} reloadCandidate Distinguishes staged authority from canonical ownership.
135
+ * @property {number} generation Manager-local reload request generation.
136
+ * @property {CjsMotherLode} owner Exact registry expected to receive the candidate.
137
+ * @property {string} key Exact canonical registry identity.
138
+ * @property {object|Function} expected Former canonical resource preserved until commit.
139
+ * @property {CjsResourceOwnership} expectedOwnership Captured authority of the former owner.
140
+ * @property {object|Function} resource Off-registry candidate resource.
141
+ * @property {Readonly<object>} loaderOptions Durable identity/source options restored after commit.
142
+ */
143
+
144
+ /**
145
+ * Hidden authority accepted by guarded CPU read/publication helpers.
146
+ *
147
+ * @typedef {CjsResourceOwnership|CjsResourceReloadCandidate} CjsResourceMutationAuthority
148
+ */
149
+
7
150
  class CjsResMan extends CjsEventEmitter {
151
+ #autoPurgePolicy = null;
152
+ #activeResourceOperations = 0;
153
+ #invalidResourceOwnership = new WeakSet();
154
+ #lastAutoPurgeTime = null;
155
+ #nextResourceReloadGeneration = 1;
156
+ #nextResourceOwnershipGeneration = 1;
157
+ #nextResourceOperationId = 1;
158
+ #queueOperations = new Map();
159
+ #resourceOwnership = new WeakMap();
160
+ #resourceOperations = new Map();
161
+ #reloadCandidates = new WeakMap();
162
+ #reloadGenerations = new Map();
163
+ #reloadOperations = new WeakMap();
164
+
165
+ /**
166
+ * Create a GPU-free resource manager and apply its initial registration.
167
+ *
168
+ * @param {object} [options={}] Configuration forwarded to {@link CjsResMan#Register}.
169
+ * @throws {TypeError} If registration, queue, source, or format options are invalid.
170
+ */
8
171
  constructor(options = {}) {
9
172
  super();
10
173
  this.motherLode = new CjsMotherLode();
@@ -16,8 +179,6 @@ class CjsResMan extends CjsEventEmitter {
16
179
  this.sourceOperations = new WeakMap();
17
180
  this.queuedSourceOperations = new WeakMap();
18
181
  this.formatOperations = new WeakMap();
19
- this.preparePipelines = new Map();
20
- this.defaultPreparePipeline = "";
21
182
  this.maxConcurrentLoads = 8;
22
183
  this.maxPrepareTime = 0.005;
23
184
  this.maxPrepareItemsPerTick = 0;
@@ -43,13 +204,44 @@ class CjsResMan extends CjsEventEmitter {
43
204
  * The same options object can be forwarded unchanged by CjsLibrary. Format
44
205
  * classes continue to own their input extensions; resource types are keyed
45
206
  * by semantic requirements such as "texture", "image", or "geometry".
207
+ * Replacing MotherLode shuts down and cleans the former owner before the new
208
+ * registry is activated. Replacement is rejected while queued or direct
209
+ * resource mutations are active so a synchronous configuration call cannot
210
+ * detach ownership underneath asynchronous publication. Normal `Wait()`
211
+ * drains queued roots only; direct callers must await their own promises.
212
+ *
213
+ * @param {object} [options={}] Additive source, registry, queue, format, and resource-type settings.
214
+ * @param {number} [options.cacheSize] Recorded-byte budget immediately installed on the active MotherLode.
215
+ * @param {object} [options.cacheCleanup] Cleanup policy used if a smaller configured budget evicts cached identities.
216
+ * @returns {CjsResMan} This resource manager.
217
+ * @throws {TypeError} If options or any configured component are invalid.
218
+ * @throws {Error|AggregateError} If resource mutations are active or replacing MotherLode cannot clean its resources.
46
219
  */
47
220
  Register(options = {}) {
48
221
  if (!options || typeof options !== "object" || Array.isArray(options)) {
49
222
  throw new TypeError("CjsResMan.Register options must be an object.");
50
223
  }
51
224
  if (Object.prototype.hasOwnProperty.call(options, "motherLode")) {
52
- this.motherLode = options.motherLode || new CjsMotherLode();
225
+ const nextMotherLode = options.motherLode || new CjsMotherLode();
226
+ if (nextMotherLode !== this.motherLode) {
227
+ if (this.#activeResourceOperations > 0) {
228
+ throw ActiveResourceOperationsError(this.#activeResourceOperations);
229
+ }
230
+ const previousMotherLode = this.motherLode;
231
+ this.#InvalidateMotherLodeOwnership(previousMotherLode);
232
+ previousMotherLode?.Shutdown?.();
233
+ this.motherLode = nextMotherLode;
234
+ this.motherLode.Startup?.();
235
+ this.#BindMotherLodeResources();
236
+ this.#reloadGenerations.clear();
237
+ this.#lastAutoPurgeTime = null;
238
+ }
239
+ }
240
+ if (Object.prototype.hasOwnProperty.call(options, "cacheSize")) {
241
+ this.motherLode.SetCacheSize(options.cacheSize, options.cacheCleanup || {});
242
+ }
243
+ if (Object.prototype.hasOwnProperty.call(options, "autoPurgePolicy")) {
244
+ this.SetAutoPurgePolicy(options.autoPurgePolicy);
53
245
  }
54
246
  if (Object.prototype.hasOwnProperty.call(options, "source")) {
55
247
  this.SetSource(options.source);
@@ -79,15 +271,6 @@ class CjsResMan extends CjsEventEmitter {
79
271
  if (Object.prototype.hasOwnProperty.call(options, "urgentResourceLoads")) {
80
272
  this.SetUrgentResourceLoads(options.urgentResourceLoads);
81
273
  }
82
- for (const [name, entry] of NormalizePreparePipelineEntries(options.preparePipelines)) {
83
- const stages = Array.isArray(entry) ? entry : entry.stages;
84
- this.RegisterPreparePipeline(name, stages, {
85
- default: !Array.isArray(entry) && entry.default === true
86
- });
87
- }
88
- if (Object.prototype.hasOwnProperty.call(options, "defaultPreparePipeline")) {
89
- this.SetDefaultPreparePipeline(options.defaultPreparePipeline);
90
- }
91
274
  for (const entry of NormalizeRegistrationEntries(options.formats)) {
92
275
  if (typeof entry === "function") this.RegisterFormat(entry);else this.RegisterFormat(entry.Format || entry.format, entry.defaults || {});
93
276
  }
@@ -99,40 +282,19 @@ class CjsResMan extends CjsEventEmitter {
99
282
  }
100
283
  return this;
101
284
  }
285
+
286
+ /**
287
+ * Replace the default source selected by later resource requests.
288
+ * Existing handles retain the effective source captured when they were
289
+ * created, so payload reconstruction does not silently move to this source.
290
+ *
291
+ * @param {object|Function|null} source Source exposing `Read(path, options)`, or a falsey value to clear the default.
292
+ * @returns {CjsResMan} This resource manager.
293
+ */
102
294
  SetSource(source) {
103
295
  this.source = source || null;
104
296
  return this;
105
297
  }
106
- RegisterPreparePipeline(name, stages, options = {}) {
107
- const key = NormalizePipelineName(name);
108
- if (!key) throw new TypeError("CjsResMan.RegisterPreparePipeline requires a name.");
109
- this.preparePipelines.set(key, Object.freeze(NormalizePrepareStages(stages)));
110
- if (options.default === true) this.defaultPreparePipeline = key;
111
- return this;
112
- }
113
- SetDefaultPreparePipeline(name = "") {
114
- const key = NormalizePipelineName(name);
115
- if (key && !this.preparePipelines.has(key)) {
116
- throw new Error(`Unknown CjsResMan prepare pipeline: ${key}`);
117
- }
118
- this.defaultPreparePipeline = key;
119
- return this;
120
- }
121
- GetPreparePipeline(name) {
122
- return [...(this.preparePipelines.get(NormalizePipelineName(name)) || [])];
123
- }
124
- ResolvePrepareStages(options = {}) {
125
- const requested = options.preparePipeline ?? options.pipeline ?? this.defaultPreparePipeline;
126
- const key = NormalizePipelineName(requested);
127
- if (key && !this.preparePipelines.has(key)) {
128
- const error = new Error(`Unknown CjsResMan prepare pipeline: ${key}`);
129
- error.code = "CJS_RESOURCE_PREPARE_PIPELINE_MISSING";
130
- error.pipeline = key;
131
- throw error;
132
- }
133
- const stages = key ? this.GetPreparePipeline(key) : [];
134
- return [...stages, ...NormalizePrepareStages(options.prepareStages || [])];
135
- }
136
298
  AddToQueue(queue, callback, context = null, flags = 0) {
137
299
  const task = this.QueueTask(queue, callback, context, {
138
300
  flags
@@ -199,27 +361,121 @@ class CjsResMan extends CjsEventEmitter {
199
361
  IsLoading() {
200
362
  return this.GetPendingLoads() + this.GetPendingPrepares() > 0;
201
363
  }
364
+
365
+ /**
366
+ * Pump background and main-thread work, enforce the recorded-byte cache
367
+ * budget, then run a due automatic inactivity sweep when one has been
368
+ * explicitly configured. Queue work is processed before housekeeping, and
369
+ * manager-owned active loads/prepares hold eviction locks. No housekeeping
370
+ * operation fetches, prepares, or reloads source data.
371
+ *
372
+ * @param {CjsResManUpdateOptions} [options={}] Queue budgets and optional automatic-purge controls.
373
+ * @returns {boolean} Whether queue work ran or housekeeping released owned state.
374
+ * @throws {TypeError} If queue, cache, purge timing, or MotherLode policy options are invalid.
375
+ * @throws {AggregateError} If cache or inactivity housekeeping cannot clean one or more resources.
376
+ */
202
377
  Update(options = {}) {
203
378
  const loaded = this.PumpBackgroundQueue(options.background || {});
204
379
  const prepared = this.PumpMainThreadQueue(options.prepare || options);
205
- return loaded || prepared;
380
+ const cacheOptions = options.cache === true || options.cache === undefined ? {} : options.cache;
381
+ const cacheResult = options.cache === false ? null : this.motherLode.TrimCache?.(cacheOptions);
382
+ const trimmed = Boolean(cacheResult && cacheResult.evicted > 0);
383
+ const purgeOptions = options.purge === true || options.purge === undefined ? {} : options.purge;
384
+ const purgeResult = options.purge === false ? null : this.PumpAutoPurge(purgeOptions);
385
+ const purged = Boolean(purgeResult && (purgeResult.purged > 0 || purgeResult.payloadsReleased > 0));
386
+ return loaded || prepared || trimmed || purged;
206
387
  }
388
+
389
+ /**
390
+ * Compatibility alias for {@link CjsResMan#Update}. It uses the same queue
391
+ * budgets, byte-cache housekeeping, opt-in purge cadence, return value, and
392
+ * error behavior.
393
+ *
394
+ * @param {CjsResManUpdateOptions} [options={}] Queue budgets and optional automatic-purge controls.
395
+ * @returns {boolean} Whether queue work ran or housekeeping released owned state.
396
+ * @throws {TypeError|AggregateError} If update options or a due purge fail.
397
+ */
207
398
  Tick(options = {}) {
208
399
  return this.Update(options);
209
400
  }
401
+
402
+ /**
403
+ * Wait for the manager work that exists when this method is called.
404
+ * The snapshot contains active queued resource load roots and direct queue
405
+ * tasks. A captured resource root includes every CPU-work descendant it
406
+ * enqueues later, while unrelated roots/tasks submitted after the call do
407
+ * not postpone this fence. Shared-source joins do not merge distinct roots.
408
+ *
409
+ * Failure and queued cancellation count as terminal settlement: callers
410
+ * observe those errors through the original operation promises, while this
411
+ * method resolves after every captured promise settles. By default it pumps
412
+ * background and main queues directly, so waiting never triggers automatic
413
+ * retention sweeps. Paused queues remain paused. With `pump: false`, an
414
+ * external driver must resume/pump queues or the returned promise may remain
415
+ * pending.
416
+ *
417
+ * A standalone canonical `PrepareResourceObjectQueued()` call opens a
418
+ * resource root. Direct `LoadResourceObject()`, direct
419
+ * `PrepareResourceObject()`, standalone `ReadResource()`, and standalone
420
+ * `ReadFormatOnce()` calls bypass the two queues and are not resource roots
421
+ * unless they also own a captured queue task. `WaitUrgent()` remains absent
422
+ * until per-item priority and urgent membership exist.
423
+ *
424
+ * @param {CjsResManWaitOptions} [options={}] Snapshot pumping and cooperative-yield controls.
425
+ * @returns {Promise<CjsResMan>} This manager after all captured work settles.
426
+ * @throws {TypeError} If options, `pump`, or `yield` are invalid.
427
+ * @throws {Error} If a queue pump or custom yield callback itself fails.
428
+ */
210
429
  async Wait(options = {}) {
211
- const yieldQueue = typeof options.yield === "function" ? options.yield : DefaultQueueYield;
212
- while (this.IsLoading()) {
213
- if (options.pump !== false) this.Update(options);
214
- await yieldQueue();
430
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
431
+ throw new TypeError("CjsResMan.Wait options must be an object.");
432
+ }
433
+ if (options.pump !== undefined && typeof options.pump !== "boolean") {
434
+ throw new TypeError("CjsResMan.Wait pump must be a boolean when supplied.");
435
+ }
436
+ if (options.yield !== undefined && typeof options.yield !== "function") {
437
+ throw new TypeError("CjsResMan.Wait yield must be a function when supplied.");
215
438
  }
439
+ const snapshot = new Set([...this.#resourceOperations.values(), ...this.#queueOperations.values()]);
440
+ if (snapshot.size === 0) return this;
441
+ const fence = Promise.allSettled([...snapshot]);
442
+ if (options.pump === false) {
443
+ await fence;
444
+ return this;
445
+ }
446
+ const yieldQueue = options.yield || DefaultQueueYield;
447
+ let settled = false;
448
+ fence.then(() => {
449
+ settled = true;
450
+ });
451
+ while (!settled) {
452
+ this.PumpBackgroundQueue(options.background || {});
453
+ this.PumpMainThreadQueue(options.prepare || {});
454
+ if (!settled) await Promise.race([fence, Promise.resolve().then(yieldQueue)]);
455
+ }
456
+ await fence;
216
457
  return this;
217
458
  }
218
459
  GetWorkQueue(queue) {
219
460
  return NormalizeCjsResManQueue(queue) === CjsResManQueue.MAIN ? this._prepareQueue : this._loadQueue;
220
461
  }
462
+
463
+ /**
464
+ * Add one low-level task to a manager queue and retain its promise only while
465
+ * pending so a contemporaneous {@link CjsResMan#Wait} snapshot can include
466
+ * it. This does not assign resource lineage to tasks the callback may submit
467
+ * later; callers should return/await such work or use a manager resource
468
+ * operation.
469
+ *
470
+ * @param {string} queue Queue name or compatibility alias.
471
+ * @param {Function} callback Queue callback receiving immutable task metadata.
472
+ * @param {*} [context=null] `this` value used to invoke the callback.
473
+ * @param {object|null} [metadata=null] Opaque diagnostics retained on the task.
474
+ * @returns {object} Queue task record containing id, queue, state, metadata, and promise.
475
+ * @throws {TypeError} If the queue or callback is invalid.
476
+ */
221
477
  QueueTask(queue, callback, context = null, metadata = null) {
222
- return this.GetWorkQueue(queue).Add(callback, context, metadata);
478
+ return this.#TrackQueueTask(this.GetWorkQueue(queue).Add(callback, context, metadata));
223
479
  }
224
480
  ScheduleBackgroundQueue() {
225
481
  if (this._backgroundPumpScheduled || this._loadQueue.IsPaused()) return this;
@@ -244,6 +500,20 @@ class CjsResMan extends CjsEventEmitter {
244
500
  }
245
501
  return this;
246
502
  }
503
+
504
+ /**
505
+ * Register a resource constructor or factory for one semantic outcome.
506
+ * The constructor is selected by `requirement`/`payload`, never by file
507
+ * extension. It does not enter resource identity: re-registration affects a
508
+ * path/output only after its existing handle is explicitly deleted/cleared.
509
+ *
510
+ * @param {string|Function} requirement Semantic outcome key, or a constructor declaring its own `payload`.
511
+ * @param {Function|object|null} [Constructor=null] Resource constructor/factory, or options when the first argument is the constructor.
512
+ * @param {object} [options={}] Registration aliases and optional requirement/payload override.
513
+ * @param {readonly string[]} [options.aliases=[]] Additional semantic keys mapped to the same constructor.
514
+ * @returns {CjsResMan} This resource manager.
515
+ * @throws {TypeError} If the semantic key or constructor/factory is invalid.
516
+ */
247
517
  RegisterResourceType(requirement, Constructor = null, options = {}) {
248
518
  if (typeof requirement === "function") {
249
519
  options = Constructor && typeof Constructor === "object" ? Constructor : {};
@@ -262,6 +532,19 @@ class CjsResMan extends CjsEventEmitter {
262
532
  }
263
533
  return this;
264
534
  }
535
+
536
+ /**
537
+ * Register the direct byte-to-object reader for one input extension.
538
+ * Direct loaders take precedence over registered format facades and are
539
+ * resolved from current setup-time configuration whenever source is read.
540
+ * A direct loader represents only the extension's unforced default result;
541
+ * multiple named outputs belong on a registered format facade.
542
+ *
543
+ * @param {string} ext Input extension with or without a leading dot.
544
+ * @param {Function} loader Reader receiving source bytes and an immutable preparation context.
545
+ * @returns {CjsResMan} This resource manager.
546
+ * @throws {TypeError} If the extension or loader is invalid.
547
+ */
265
548
  RegisterObjectLoader(ext, loader) {
266
549
  const key = normalizeResourceExtension(ext);
267
550
  if (!key) throw new TypeError("CjsResMan.RegisterObjectLoader requires an extension.");
@@ -273,11 +556,15 @@ class CjsResMan extends CjsEventEmitter {
273
556
  /**
274
557
  * Register a reusable format facade for each accepted input extension.
275
558
  * Multiple candidates may share an extension and are resolved by requested
276
- * output/media type or by their support probes.
559
+ * output/media type or by their support probes. Defaults are copied into a
560
+ * deeply frozen plain-object/array snapshot so later caller mutation cannot
561
+ * rewrite the registered configuration. Format methods are read from the
562
+ * currently registered facade when an operation runs.
277
563
  *
278
- * @param {Function} Format
279
- * @param {object} defaults
280
- * @returns {CjsResMan}
564
+ * @param {Function} Format Format facade declaring at least one `inputTypes` extension.
565
+ * @param {object} [defaults={}] Plain reader-option defaults to snapshot for this registration.
566
+ * @returns {CjsResMan} This resource manager.
567
+ * @throws {TypeError} If the format declaration or immutable defaults snapshot is invalid.
281
568
  */
282
569
  RegisterFormat(Format, defaults = {}) {
283
570
  if (typeof Format !== "function") {
@@ -288,9 +575,7 @@ class CjsResMan extends CjsEventEmitter {
288
575
  }
289
576
  const descriptor = Object.freeze({
290
577
  Format,
291
- defaults: Object.freeze({
292
- ...defaults
293
- })
578
+ defaults: SnapshotFormatDefaults(defaults)
294
579
  });
295
580
  for (const inputType of Format.inputTypes) {
296
581
  const key = normalizeResourceExtension(inputType);
@@ -302,32 +587,164 @@ class CjsResMan extends CjsEventEmitter {
302
587
  }
303
588
  return this;
304
589
  }
590
+
591
+ /**
592
+ * Return registered format facades for one normalized input extension.
593
+ *
594
+ * @param {string} inputType Input extension with or without a leading dot.
595
+ * @returns {Function[]} Detached format-class list in registration order.
596
+ */
305
597
  GetFormats(inputType) {
306
598
  return this.GetFormatDescriptors(inputType).map(descriptor => descriptor.Format);
307
599
  }
600
+
601
+ /**
602
+ * Select one registered format facade for an input and requested outcome.
603
+ *
604
+ * @param {string} inputType Input extension with or without a leading dot.
605
+ * @param {object} [options={}] Optional format, output, media type, and source-byte selectors.
606
+ * @returns {Function} Unambiguous selected format facade.
607
+ * @throws {Error} If no candidate matches or multiple candidates remain.
608
+ */
308
609
  ResolveFormat(inputType, options = {}) {
309
610
  return this.ResolveFormatDescriptor(inputType, options).Format;
310
611
  }
612
+
613
+ /**
614
+ * Return the immediate canonical resource handle for a source path and
615
+ * promised output. Cache hits explicitly renew MotherLode activity. When
616
+ * `reload: true` finds an existing owner, this synchronous method returns a
617
+ * distinct off-registry candidate and leaves the canonical handle untouched.
618
+ * Calling `Ready()` on that candidate, or using `ReloadResource()` /
619
+ * `ReloadObject()`, prepares it and conditionally commits it through the
620
+ * asynchronous atomic-reload contract.
621
+ *
622
+ * @param {string} path Carbon-style source resource path.
623
+ * @param {object} [options={}] Promised output, semantic resource, source provenance, and reload settings.
624
+ * @returns {CjsResource} Canonical handle, or an off-registry reload candidate.
625
+ * @throws {TypeError} If the path, identity settings, or resource constructor are invalid.
626
+ * @throws {Error} If MotherLode is inactive or displaced-resource cleanup fails.
627
+ */
311
628
  GetResource(path, options = {}) {
629
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
630
+ throw new TypeError("CjsResMan.GetResource options must be an object.");
631
+ }
312
632
  const key = normalizeResourcePath(path);
313
633
  const variant = this.GetResourceVariant(options);
314
- const existing = this.motherLode.Lookup(key, variant);
315
- if (existing && options.reload !== true) return existing;
316
634
  const ext = normalizeResourceExtension(options.ext || getResourceExtension(key));
635
+ if (options.emit !== undefined) {
636
+ const objectLoader = this.GetObjectLoader(ext);
637
+ if (objectLoader) {
638
+ throw CreateObjectLoaderOutputMissingError(ext, options.emit);
639
+ } else {
640
+ const descriptors = this.GetFormatDescriptors(ext);
641
+ const candidates = FilterFormatDescriptors(descriptors, options);
642
+ if (descriptors.length > 0 && candidates.length === 0) {
643
+ throw CreateFormatOutputMissingError(ext, options.emit, descriptors);
644
+ }
645
+ }
646
+ }
647
+ const cacheKey = getMotherLodeKey(key, variant);
648
+ const existing = this.motherLode.Lookup(cacheKey);
649
+ if (existing && options.reload !== true) {
650
+ this.#BindResourceLifecycle(cacheKey, existing);
651
+ this.motherLode.KeepAlive?.(cacheKey);
652
+ return existing;
653
+ }
654
+ if (!existing && options.reload === true) {
655
+ this.InvalidateReadCache(key, {
656
+ source: options.source || this.source
657
+ });
658
+ }
317
659
  const Constructor = this.ResolveResourceConstructor(options);
318
- const resource = this.CreateResource(Constructor, key, ext, options);
319
- this.motherLode.Insert(resource, key, variant);
320
- return resource;
660
+ const resource = this.CreateResource(Constructor, key, ext, options, existing && options.reload === true ? existing : null);
661
+ if (existing && options.reload === true) {
662
+ this.#BindResourceLifecycle(cacheKey, existing);
663
+ this.motherLode.KeepAlive?.(cacheKey);
664
+ const expectedOwnership = this.#RequireResourceOwnership(existing, "reload-candidate:create");
665
+ const generation = this.#nextResourceReloadGeneration++;
666
+ const loaderOptions = Object.freeze(GetResourceLoaderOptions(options, options.source || this.source));
667
+ const candidate = Object.freeze({
668
+ reloadCandidate: true,
669
+ generation,
670
+ owner: this.motherLode,
671
+ key: cacheKey,
672
+ expected: existing,
673
+ expectedOwnership,
674
+ resource,
675
+ loaderOptions
676
+ });
677
+ this.#reloadCandidates.set(resource, candidate);
678
+ this.#reloadGenerations.set(cacheKey, generation);
679
+ if (typeof resource.SetObjectLoader === "function") {
680
+ const reloadOptions = {
681
+ ...loaderOptions,
682
+ reload: true
683
+ };
684
+ resource.SetObjectLoader(loadOptions => this.#GetReloadCandidateObject(resource, {
685
+ ...MergeResourceLoaderOptions(reloadOptions, loadOptions),
686
+ reload: true
687
+ }), reloadOptions);
688
+ }
689
+ return resource;
690
+ }
691
+ const insertion = this.motherLode.Insert(cacheKey, resource, {
692
+ replace: true
693
+ });
694
+ const canonical = insertion?.resource || resource;
695
+ if (insertion?.displaced && insertion.displaced !== canonical) {
696
+ this.#InvalidateResourceOwnership(insertion.displaced);
697
+ } else if (existing && existing !== canonical && this.motherLode.Lookup(cacheKey) !== existing) {
698
+ this.#InvalidateResourceOwnership(existing);
699
+ }
700
+ this.#BindResourceLifecycle(cacheKey, canonical);
701
+ this.motherLode.KeepAlive?.(cacheKey);
702
+ return canonical;
321
703
  }
704
+
705
+ /**
706
+ * Resolve one object outcome for a canonical resource identity. Concurrent
707
+ * callers share only the active operation. Once settled, a resident payload
708
+ * is returned without rereading source data and its explicit payload lease is
709
+ * renewed. If the payload was released, calling this method explicitly starts
710
+ * reconstruction; liveness queries and purge operations never do so.
711
+ * `sourceRevision` selects read-cache provenance but does not replace a
712
+ * resident payload by itself—use one-shot `reload: true` for replacement.
713
+ *
714
+ * @param {string} path Carbon-style source resource path.
715
+ * @param {object} [options={}] Identity, source, format, loader, and queue options.
716
+ * @returns {Promise<*>} In-flight, resident, or reconstructed object outcome.
717
+ * @throws {TypeError|Error} If path, identity, source, format, or conversion configuration is invalid.
718
+ */
322
719
  GetObject(path, options = {}) {
323
720
  const resource = this.GetResource(path, options);
721
+ const operationOptions = MergeResourceLoaderOptions(resource.GetObjectRequest?.() || {}, options);
722
+ if (this.#reloadCandidates.has(resource)) {
723
+ return this.#GetReloadCandidateObject(resource, operationOptions);
724
+ }
725
+ const ownership = this.#RequireResourceOwnership(resource, "object:begin");
324
726
  const existing = this.objectOperations.get(resource);
325
- if (existing) {
727
+ if (existing?.ownership === ownership) {
326
728
  return existing.promise;
327
729
  }
328
- const promise = this.QueueResourceObject(resource, options);
329
- this.objectOperations.set(resource, {
330
- promise
730
+ if (resource.HasPayload?.()) {
731
+ resource.KeepPayloadAlive?.();
732
+ return Promise.resolve(GetPublishedResourceObject(resource));
733
+ }
734
+ const promise = this.QueueResourceObject(resource, operationOptions);
735
+ const operation = {
736
+ promise,
737
+ ownership
738
+ };
739
+ this.objectOperations.set(resource, operation);
740
+ promise.then(() => {
741
+ if (this.objectOperations.get(resource) === operation) {
742
+ this.objectOperations.delete(resource);
743
+ }
744
+ }, () => {
745
+ if (this.objectOperations.get(resource) === operation) {
746
+ this.objectOperations.delete(resource);
747
+ }
331
748
  });
332
749
  return promise;
333
750
  }
@@ -337,112 +754,573 @@ class CjsResMan extends CjsEventEmitter {
337
754
  FetchObject(path, options = {}) {
338
755
  return this.GetObject(path, options);
339
756
  }
757
+
758
+ /**
759
+ * Read and atomically publish a distinct replacement object while the
760
+ * former canonical resource remains available. This is the explicit form of
761
+ * `GetObject(path, { reload: true })`.
762
+ *
763
+ * @param {string} path Carbon-style source resource path.
764
+ * @param {object} [options={}] Identity, source, format, and queue settings.
765
+ * @returns {Promise<*>} Published CPU object outcome from the committed candidate.
766
+ * @throws {TypeError|Error|AggregateError} If candidate creation, conversion, conditional publication, or cleanup fails.
767
+ */
768
+ ReloadObject(path, options = {}) {
769
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
770
+ throw new TypeError("CjsResMan.ReloadObject options must be an object.");
771
+ }
772
+ return this.GetObject(path, {
773
+ ...options,
774
+ reload: true
775
+ });
776
+ }
777
+
778
+ /**
779
+ * Resolve and load one canonical resource handle. One-shot reload creates
780
+ * a distinct candidate, keeps the former good handle canonical through every
781
+ * asynchronous CPU operation, and returns only after an exact-owner conditional
782
+ * publication succeeds. Readiness receives `reload: false` because the
783
+ * candidate's private loader owns the one-shot freshness request.
784
+ *
785
+ * @param {string} path Carbon-style source resource path.
786
+ * @param {object} [options={}] Resource identity, source provenance, reload, format, and queue options.
787
+ * @returns {Promise<CjsResource>} Loaded canonical resource selected by this operation.
788
+ * @throws {TypeError|Error} If identity, source, format, conversion, or cleanup fails.
789
+ */
340
790
  async FetchResource(path, options = {}) {
341
791
  const resource = this.GetResource(path, options);
342
- await resource.Ready(options);
792
+ const readinessOptions = options.reload === true ? {
793
+ ...options,
794
+ reload: false
795
+ } : options;
796
+ await resource.Ready(readinessOptions);
343
797
  return resource;
344
798
  }
799
+
800
+ /**
801
+ * Load and atomically publish a distinct replacement resource while the
802
+ * former canonical handle remains available. This is the explicit form of
803
+ * `FetchResource(path, { reload: true })`.
804
+ *
805
+ * @param {string} path Carbon-style source resource path.
806
+ * @param {object} [options={}] Identity, source, format, and queue settings.
807
+ * @returns {Promise<CjsResource>} Fully loaded committed replacement.
808
+ * @throws {TypeError|Error|AggregateError} If candidate creation, conversion, conditional publication, or cleanup fails.
809
+ */
810
+ ReloadResource(path, options = {}) {
811
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
812
+ throw new TypeError("CjsResMan.ReloadResource options must be an object.");
813
+ }
814
+ return this.FetchResource(path, {
815
+ ...options,
816
+ reload: true
817
+ });
818
+ }
819
+
820
+ /**
821
+ * Run or join one off-registry candidate operation. The returned promise
822
+ * resolves only after exact-owner publication and displaced-owner cleanup
823
+ * both succeed. A committed cleanup failure rejects with the new candidate
824
+ * still canonical and attached on the MotherLode error result.
825
+ *
826
+ * @param {object|Function} resource Candidate resource returned by `GetResource`.
827
+ * @param {object} options Reload source, format, and queue options.
828
+ * @returns {Promise<*>} Candidate object outcome after conditional publication.
829
+ */
830
+ #GetReloadCandidateObject(resource, options) {
831
+ const candidate = this.#reloadCandidates.get(resource) || null;
832
+ if (!candidate) {
833
+ return Promise.reject(ReloadCandidateUnavailableError(resource));
834
+ }
835
+ const existing = this.#reloadOperations.get(resource);
836
+ if (existing?.candidate === candidate) return existing.promise;
837
+ const operation = {
838
+ candidate,
839
+ promise: this.#RunReloadCandidate(candidate, options)
840
+ };
841
+ this.#reloadOperations.set(resource, operation);
842
+ operation.promise.then(() => {
843
+ if (this.#reloadOperations.get(resource) === operation) {
844
+ this.#reloadOperations.delete(resource);
845
+ }
846
+ }, () => {
847
+ if (this.#reloadOperations.get(resource) === operation) {
848
+ this.#reloadOperations.delete(resource);
849
+ }
850
+ });
851
+ return operation.promise;
852
+ }
853
+
854
+ /**
855
+ * Queue source/read/publication work against a detached candidate, then commit
856
+ * it only if its former exact owner and newest-request token remain current.
857
+ * Candidate work is a normal queued root visible to `Wait()` and holds one
858
+ * purge lock on the former good owner until settlement.
859
+ *
860
+ * @param {CjsResourceReloadCandidate} candidate Immutable candidate authority.
861
+ * @param {object} options Reload source, format, queue, and prepare options.
862
+ * @returns {Promise<*>} Published object outcome after a successful commit.
863
+ */
864
+ async #RunReloadCandidate(candidate, options) {
865
+ let committed = false;
866
+ let releaseLock = Noop;
867
+ const finishOperation = this.#BeginResourceOperation(true);
868
+ try {
869
+ this.#AssertReloadCandidate(candidate, "reload:begin");
870
+ releaseLock = this.#AcquireResourcePurgeLock(candidate.expectedOwnership);
871
+ const read = this.#BeginReadOperation(candidate.resource.GetPath(), {
872
+ ...options,
873
+ reload: true
874
+ });
875
+ this.#AssertReloadCandidate(candidate, "reload:requested");
876
+ candidate.resource.error = null;
877
+ candidate.resource.MarkRequested();
878
+ this.#AssertReloadCandidate(candidate, "reload:requested-settled");
879
+ const bytes = await this.#QueueReadResource(read.context, read.options);
880
+ this.#AssertReloadCandidate(candidate, "reload:loading");
881
+ candidate.resource.MarkLoading();
882
+ this.#AssertReloadCandidate(candidate, "reload:loading-settled");
883
+ const object = await this.#PrepareResourceObjectQueued(candidate.resource, bytes, read.options, candidate);
884
+ this.#AssertReloadCandidate(candidate, "reload:commit");
885
+ try {
886
+ const result = candidate.owner.ReplaceExpected(candidate.key, candidate.expected, candidate.resource, {
887
+ commitGuard: () => this.#IsReloadCandidateCurrent(candidate)
888
+ });
889
+ if (!result.committed) {
890
+ throw StaleReloadCandidateError(candidate, "reload:commit-compare");
891
+ }
892
+ committed = true;
893
+ this.#FinalizeCommittedReload(candidate);
894
+ } catch (error) {
895
+ if (error?.code === "CJS_MOTHERLODE_REPLACE_CLEANUP_FAILED" && error.result?.committed === true && error.result.resource === candidate.resource) {
896
+ committed = true;
897
+ try {
898
+ this.#FinalizeCommittedReload(candidate);
899
+ } catch (finalizeError) {
900
+ const combined = new AggregateError([error, finalizeError], `CjsResMan reload committed with cleanup and finalization failures for ${candidate.key}.`);
901
+ combined.code = "CJS_RESMAN_RELOAD_COMMIT_FAILED";
902
+ combined.committed = true;
903
+ combined.resource = candidate.resource;
904
+ combined.result = error.result;
905
+ throw combined;
906
+ }
907
+ }
908
+ throw error;
909
+ }
910
+ return object;
911
+ } catch (error) {
912
+ if (!committed) {
913
+ throw this.#CreateReloadCandidateFailure(candidate, error);
914
+ }
915
+ throw error;
916
+ } finally {
917
+ releaseLock();
918
+ finishOperation();
919
+ if (this.#reloadGenerations.get(candidate.key) === candidate.generation) {
920
+ this.#reloadGenerations.delete(candidate.key);
921
+ }
922
+ }
923
+ }
924
+
925
+ /**
926
+ * Bind a successfully committed candidate and invalidate only the displaced
927
+ * handle's former publication generation. The resource loader is restored
928
+ * to ordinary canonical reconstruction behavior.
929
+ *
930
+ * @param {CjsResourceReloadCandidate} candidate Committed candidate authority.
931
+ * @returns {void}
932
+ */
933
+ #FinalizeCommittedReload(candidate) {
934
+ this.#InvalidateResourceOwnership(candidate.expected);
935
+ this.#reloadCandidates.delete(candidate.resource);
936
+ const errors = [];
937
+ try {
938
+ if (typeof candidate.resource.SetObjectLoader === "function") {
939
+ candidate.resource.SetObjectLoader(loadOptions => this.GetObject(candidate.resource.GetPath(), MergeResourceLoaderOptions(candidate.loaderOptions, loadOptions)), candidate.loaderOptions);
940
+ }
941
+ } catch (error) {
942
+ errors.push(error);
943
+ }
944
+ try {
945
+ this.#BindResourceLifecycle(candidate.key, candidate.resource);
946
+ } catch (error) {
947
+ errors.push(error);
948
+ }
949
+ try {
950
+ candidate.owner.KeepAlive?.(candidate.key);
951
+ } catch (error) {
952
+ errors.push(error);
953
+ }
954
+ if (errors.length) {
955
+ const error = new AggregateError(errors, `CjsResMan committed reload finalization failed for ${candidate.key}.`);
956
+ error.code = "CJS_RESMAN_RELOAD_COMMIT_FINALIZE_FAILED";
957
+ error.committed = true;
958
+ error.resource = candidate.resource;
959
+ throw error;
960
+ }
961
+ }
962
+
963
+ /**
964
+ * Mark and clean a failed or stale detached candidate. The original
965
+ * load/prepare/stale error remains the rejection when cleanup succeeds;
966
+ * state-publication or cleanup errors are aggregated without touching the
967
+ * preserved canonical owner.
968
+ *
969
+ * @param {CjsResourceReloadCandidate} candidate Failed candidate authority.
970
+ * @param {*} cause Original operation failure.
971
+ * @returns {*} Original error or a contextual AggregateError.
972
+ */
973
+ #CreateReloadCandidateFailure(candidate, cause) {
974
+ const errors = [cause];
975
+ try {
976
+ candidate.resource.SetError?.(cause);
977
+ } catch (error) {
978
+ errors.push(error);
979
+ }
980
+ try {
981
+ this.#CleanupReloadCandidate(candidate.resource);
982
+ } catch (error) {
983
+ errors.push(error);
984
+ }
985
+ if (errors.length === 1) return cause;
986
+ const error = new AggregateError(errors, `CjsResMan reload candidate cleanup failed for ${candidate.key}.`);
987
+ error.code = "CJS_RESMAN_RELOAD_CANDIDATE_CLEANUP_FAILED";
988
+ error.resource = candidate.resource;
989
+ error.cause = cause;
990
+ return error;
991
+ }
992
+
993
+ /**
994
+ * Release adapter and CPU-payload ownership accumulated by an off-registry
995
+ * candidate. This cleanup is idempotent for CjsResource-compatible handles
996
+ * and never marks the never-canonical candidate `PURGED`.
997
+ *
998
+ * @param {object|Function} resource Detached reload candidate.
999
+ * @returns {void}
1000
+ * @throws {AggregateError} If adapter destruction, payload release, or lifecycle detachment fails.
1001
+ */
1002
+ #CleanupReloadCandidate(resource) {
1003
+ const errors = [];
1004
+ try {
1005
+ resource.DestroyAdapterResources?.({
1006
+ destroy: true
1007
+ });
1008
+ } catch (error) {
1009
+ errors.push(error);
1010
+ }
1011
+ try {
1012
+ resource.ReleasePayload?.();
1013
+ } catch (error) {
1014
+ errors.push(error);
1015
+ }
1016
+ try {
1017
+ resource.SetLifecycleController?.(null);
1018
+ } catch (error) {
1019
+ errors.push(error);
1020
+ }
1021
+ if (errors.length) {
1022
+ throw new AggregateError(errors, "CjsResMan reload candidate ownership cleanup failed.");
1023
+ }
1024
+ }
345
1025
  Fetch(path, options = {}) {
346
1026
  return options.resource === true || options.requirement !== undefined || options.payload !== undefined ? this.FetchResource(path, options) : this.FetchObject(path, options);
347
1027
  }
348
- async LoadResourceObject(resource, options) {
349
- resource.MarkLoading();
1028
+
1029
+ /**
1030
+ * Read and prepare an existing resource immediately, outside the manager
1031
+ * queues. One balanced purge lock protects the canonical handle for the
1032
+ * complete asynchronous operation; any caller-owned locks remain intact.
1033
+ * Failure is published to the resource before rejection only while its
1034
+ * captured ownership generation remains canonical. A detached operation
1035
+ * preserves its original failure without changing the old handle.
1036
+ *
1037
+ * @param {CjsResource} resource Existing manager-bound resource handle.
1038
+ * @param {object} [options={}] Source, format, semantic outcome, and queue options.
1039
+ * @returns {Promise<*>} Prepared object, or the semantic resource when it owns the payload.
1040
+ * @throws {TypeError|Error} If the resource, source, format, options, or canonical ownership are invalid.
1041
+ */
1042
+ async LoadResourceObject(resource, options = {}) {
1043
+ const read = this.#BeginReadOperation(resource.GetPath(), options);
1044
+ const ownership = this.#RequireResourceOwnership(resource, "direct-load:begin");
1045
+ const releaseLock = this.#AcquireResourcePurgeLock(ownership);
1046
+ const finishOperation = this.#BeginResourceOperation(false);
350
1047
  try {
351
- const bytes = await this.ReadResource(resource.GetPath(), options);
352
- return await this.PrepareResourceObject(resource, bytes, options);
1048
+ this.#AssertResourceOwnership(ownership, "direct-load:loading");
1049
+ resource.error = null;
1050
+ resource.MarkLoading();
1051
+ this.#AssertResourceOwnership(ownership, "direct-load:loading-settled");
1052
+ const bytes = await this.#ReadResource(read.context, read.options);
1053
+ this.#AssertResourceOwnership(ownership, "direct-load:source-settled");
1054
+ return await this.#PrepareResourceObject(resource, bytes, read.options, ownership);
353
1055
  } catch (error) {
354
- resource.SetError(error);
1056
+ if (this.#IsResourceOwnershipCurrent(ownership)) resource.SetError(error);
355
1057
  throw error;
1058
+ } finally {
1059
+ releaseLock();
1060
+ finishOperation();
356
1061
  }
357
1062
  }
1063
+
1064
+ /**
1065
+ * Queue a source read followed by CPU conversion and publication. One balanced
1066
+ * purge lock is acquired before request publication and released only after
1067
+ * success or failure, preventing inactivity sweeps from detaching an active
1068
+ * handle. The lock does not reload data and does not consume caller locks.
1069
+ *
1070
+ * @param {CjsResource} resource Existing manager-bound resource handle.
1071
+ * @param {object} [options={}] Source, format, queue, and semantic outcome options.
1072
+ * @returns {Promise<*>} Promise for the loaded object or semantic resource.
1073
+ * @throws {TypeError} If the resource cannot be queued or its options are invalid.
1074
+ */
358
1075
  QueueResourceObject(resource, options = {}) {
359
- resource.MarkRequested();
360
- const load = this.QueueReadResource(resource.GetPath(), options);
361
- return load.then(bytes => {
1076
+ const read = this.#BeginReadOperation(resource.GetPath(), options);
1077
+ const ownership = this.#RequireResourceOwnership(resource, "queue:begin");
1078
+ const releaseLock = this.#AcquireResourcePurgeLock(ownership);
1079
+ const finishOperation = this.#BeginResourceOperation(true);
1080
+ let load;
1081
+ try {
1082
+ this.#AssertResourceOwnership(ownership, "queue:requested");
1083
+ resource.error = null;
1084
+ resource.MarkRequested();
1085
+ this.#AssertResourceOwnership(ownership, "queue:requested-settled");
1086
+ load = this.#QueueReadResource(read.context, read.options);
1087
+ } catch (error) {
1088
+ releaseLock();
1089
+ finishOperation();
1090
+ throw error;
1091
+ }
1092
+ const operation = load.then(bytes => {
1093
+ this.#AssertResourceOwnership(ownership, "queue:loading");
362
1094
  resource.MarkLoading();
363
- return this.PrepareResourceObjectQueued(resource, bytes, options);
1095
+ this.#AssertResourceOwnership(ownership, "queue:loading-settled");
1096
+ return this.#PrepareResourceObjectQueued(resource, bytes, read.options, ownership);
364
1097
  }).catch(error => {
365
- resource.SetError(error);
1098
+ if (this.#IsResourceOwnershipCurrent(ownership)) resource.SetError(error);
366
1099
  throw error;
367
- });
1100
+ }).finally(releaseLock);
1101
+ operation.then(finishOperation, finishOperation);
1102
+ return operation;
368
1103
  }
1104
+
1105
+ /**
1106
+ * Queue one source read under the selected source/path/revision identity.
1107
+ * Cache policy is tri-state: omitted shares an in-flight or explicitly
1108
+ * retained record, `true` additionally retains success, and `false` bypasses
1109
+ * both sharing and retention. `reload: true` invalidates this source/path
1110
+ * before the new task and does not cancel detached existing consumers.
1111
+ *
1112
+ * @param {string} path Carbon-style source resource path.
1113
+ * @param {CjsResManReadCacheOptions} [options={}] Source, revision, reload, and cache controls forwarded to the read.
1114
+ * @returns {Promise<*>} Promise for source bytes or source-compatible data.
1115
+ * @throws {TypeError} If path, source, revision, or options are invalid.
1116
+ */
369
1117
  QueueReadResource(path, options = {}) {
370
- const source = options.source || this.source;
371
- if (!source || typeof source.Read !== "function") {
372
- return Promise.reject(new TypeError("CjsResMan requires a source with Read(path, options) to load objects."));
373
- }
374
- const key = normalizeResourcePath(path);
375
- if (options.reload === true || options.cacheSource === false) {
376
- return this.QueueTask(CjsResManQueue.BACKGROUND, () => this.ReadResource(key, options), source, {
377
- kind: "load",
378
- path: key
379
- }).promise;
380
- }
381
- let operations = this.queuedSourceOperations.get(source);
382
- if (!operations) {
383
- operations = new Map();
384
- this.queuedSourceOperations.set(source, operations);
385
- }
386
- const existing = operations.get(key);
387
- if (existing) return existing;
388
- const operation = this.QueueTask(CjsResManQueue.BACKGROUND, () => this.ReadResource(key, options), source, {
389
- kind: "load",
390
- path: key
391
- }).promise;
392
- operations.set(key, operation);
393
- operation.then(() => {
394
- if (operations.get(key) === operation) operations.delete(key);
395
- }, () => {
396
- if (operations.get(key) === operation) operations.delete(key);
397
- });
398
- return operation;
1118
+ const read = this.#BeginReadOperation(path, options);
1119
+ return this.#QueueReadResource(read.context, read.options);
399
1120
  }
1121
+
1122
+ /**
1123
+ * Immediately read, transform, and publish one resource payload outside the
1124
+ * manager queues. Canonical resources capture their current ownership
1125
+ * generation and cannot publish after deletion, clearing, or replacement;
1126
+ * historically supported detached resource-like objects remain usable
1127
+ * without manager ownership guards.
1128
+ *
1129
+ * @param {object|Function} resource Resource receiving the final payload.
1130
+ * @param {*} bytes Source bytes or reader-compatible source data.
1131
+ * @param {object} [options={}] Format and semantic outcome options.
1132
+ * @returns {Promise<*>} Final plain payload or semantic resource handle.
1133
+ * @throws {Error} If reading/preparation fails or canonical ownership becomes stale.
1134
+ */
400
1135
  async PrepareResourceObject(resource, bytes, options = {}) {
401
- let object = await this.ReadResourceObjectPayload(resource, bytes, options);
402
- for (const stage of this.ResolvePrepareStages(options)) {
403
- const next = await stage.prepare(object, CreatePrepareContext(this, resource, bytes, options, stage.name));
404
- if (next !== undefined) object = next;
1136
+ const ownership = this.#GetResourceOwnership(resource, "direct-prepare:begin");
1137
+ if (!ownership) {
1138
+ return this.#PrepareResourceObject(resource, bytes, options, null);
1139
+ }
1140
+ const releaseLock = this.#AcquireResourcePurgeLock(ownership);
1141
+ const finishOperation = this.#BeginResourceOperation(false);
1142
+ try {
1143
+ return await this.#PrepareResourceObject(resource, bytes, options, ownership);
1144
+ } finally {
1145
+ releaseLock();
1146
+ finishOperation();
405
1147
  }
406
- return this.PublishResourceObject(resource, object, options);
407
1148
  }
1149
+
1150
+ /**
1151
+ * Queue one reader/format conversion and then CPU publication on the main
1152
+ * manager queue. A canonical standalone call becomes a queued resource root
1153
+ * for `Wait()` and retains one balanced purge lock through settlement.
1154
+ * The read and publication items validate the captured canonical generation
1155
+ * before dispatch and after asynchronous settlement.
1156
+ *
1157
+ * @param {object|Function} resource Resource receiving the final payload.
1158
+ * @param {*} bytes Source bytes or reader-compatible source data.
1159
+ * @param {object} [options={}] Format and semantic outcome options.
1160
+ * @returns {Promise<*>} Final plain payload or semantic resource handle.
1161
+ * @throws {Error} If queueing/preparation fails or canonical ownership becomes stale.
1162
+ */
408
1163
  async PrepareResourceObjectQueued(resource, bytes, options = {}) {
409
- const stages = [Object.freeze({
410
- name: "read",
411
- prepare: () => this.ReadResourceObjectPayload(resource, bytes, options)
412
- }), ...this.ResolvePrepareStages(options), Object.freeze({
413
- name: "publish",
414
- prepare: object => this.PublishResourceObject(resource, object, options)
415
- })];
416
- let object = bytes;
417
- for (const stage of stages) {
418
- const task = this.QueueTask(CjsResManQueue.MAIN, () => stage.prepare(object, CreatePrepareContext(this, resource, bytes, options, stage.name)), resource, {
419
- kind: "prepare",
420
- stage: stage.name,
421
- path: resource.GetPath()
422
- });
423
- const next = await task.promise;
424
- if (next !== undefined) object = next;
1164
+ const ownership = this.#GetResourceOwnership(resource, "queued-prepare:begin");
1165
+ if (!ownership) {
1166
+ return this.#PrepareResourceObjectQueued(resource, bytes, options, null);
425
1167
  }
426
- return object;
427
- }
428
- async ReadResourceObjectPayload(resource, bytes, options = {}) {
429
- const explicitLoader = this.GetObjectLoader(resource.GetExt());
430
- if (explicitLoader) {
431
- return explicitLoader(bytes, CreatePrepareContext(this, resource, bytes, options, "read"));
1168
+ const releaseLock = this.#AcquireResourcePurgeLock(ownership);
1169
+ const finishOperation = this.#BeginResourceOperation(true);
1170
+ try {
1171
+ return await this.#PrepareResourceObjectQueued(resource, bytes, options, ownership);
1172
+ } finally {
1173
+ releaseLock();
1174
+ finishOperation();
432
1175
  }
433
- const descriptor = this.ResolveFormatDescriptor(resource.GetExt(), {
434
- ...options,
435
- bytes
436
- });
437
- return this.ReadFormatOnce(resource, descriptor, bytes, options);
438
1176
  }
439
- PublishResourceObject(resource, object, options = {}) {
440
- let result = object;
1177
+
1178
+ /**
1179
+ * Read and immediately publish one CPU payload with optional hidden
1180
+ * ownership authority. Format classes own every requested conversion;
1181
+ * backend realization runs separately after this CPU publication.
1182
+ *
1183
+ * @param {object|Function} resource Resource receiving the final payload.
1184
+ * @param {*} bytes Source bytes or reader-compatible source data.
1185
+ * @param {object} options Resolved prepare options.
1186
+ * @param {CjsResourceMutationAuthority|null} ownership Captured canonical or reload-candidate authority.
1187
+ * @returns {Promise<*>} Final plain payload or semantic resource handle.
1188
+ */
1189
+ async #PrepareResourceObject(resource, bytes, options, ownership) {
1190
+ this.#AssertOptionalResourceOwnership(ownership, "prepare:read");
1191
+ const object = await this.ReadResourceObjectPayload(resource, bytes, options);
1192
+ this.#AssertOptionalResourceOwnership(ownership, "prepare:read-settled");
1193
+ return this.#PublishResourceObject(ownership, resource, object, options);
1194
+ }
1195
+
1196
+ /**
1197
+ * Queue one main-thread reader/format operation followed by guarded CPU
1198
+ * publication. Both remain distinct budgeted queue items under one resource
1199
+ * root so `Wait()` continues to fence dynamically enqueued publication.
1200
+ *
1201
+ * @param {object|Function} resource Resource receiving the final payload.
1202
+ * @param {*} bytes Source bytes or reader-compatible source data.
1203
+ * @param {object} options Resolved prepare options.
1204
+ * @param {CjsResourceMutationAuthority|null} ownership Captured canonical or reload-candidate authority.
1205
+ * @returns {Promise<*>} Final plain payload or semantic resource handle.
1206
+ */
1207
+ async #PrepareResourceObjectQueued(resource, bytes, options, ownership) {
1208
+ let object = bytes;
1209
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:read:enqueue");
1210
+ const readTask = this.QueueTask(CjsResManQueue.MAIN, () => {
1211
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:read:run");
1212
+ return this.ReadResourceObjectPayload(resource, bytes, options);
1213
+ }, resource, {
1214
+ kind: "prepare",
1215
+ stage: "read",
1216
+ path: resource.GetPath()
1217
+ });
1218
+ const read = await readTask.promise;
1219
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:read:settled");
1220
+ if (read !== undefined) object = read;
1221
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:publish:enqueue");
1222
+ const publishTask = this.QueueTask(CjsResManQueue.MAIN, () => {
1223
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:publish:run");
1224
+ return this.#PublishResourceObject(ownership, resource, object, options);
1225
+ }, resource, {
1226
+ kind: "prepare",
1227
+ stage: "publish",
1228
+ path: resource.GetPath()
1229
+ });
1230
+ const published = await publishTask.promise;
1231
+ this.#AssertOptionalResourceOwnership(ownership, "queue-stage:publish:settled");
1232
+ if (published !== undefined) object = published;
1233
+ return object;
1234
+ }
1235
+
1236
+ /**
1237
+ * Decode source bytes with the currently registered reader for the resource
1238
+ * extension. A direct extension loader wins over registered format candidates;
1239
+ * byte support probes may disambiguate those candidates.
1240
+ *
1241
+ * @param {CjsResource} resource Resource whose reader outcome is requested.
1242
+ * @param {*} bytes Source byte payload.
1243
+ * @param {object} [options={}] Requested output plus per-operation controls.
1244
+ * @returns {Promise<*>} Direct-loader or registered-format reader outcome.
1245
+ * @throws {Error|TypeError} If no registered reader matches or its read contract fails.
1246
+ */
1247
+ async ReadResourceObjectPayload(resource, bytes, options = {}) {
1248
+ const explicitLoader = this.GetObjectLoader(resource.GetExt());
1249
+ if (explicitLoader) {
1250
+ if (options.emit !== undefined) {
1251
+ throw CreateObjectLoaderOutputMissingError(resource.GetExt(), options.emit);
1252
+ }
1253
+ return explicitLoader(bytes, CreatePrepareContext(this, resource, bytes, options, "read"));
1254
+ }
1255
+ const descriptor = this.ResolveFormatDescriptor(resource.GetExt(), {
1256
+ ...options,
1257
+ bytes
1258
+ });
1259
+ return this.ReadFormatOnce(resource, descriptor, bytes, options);
1260
+ }
1261
+
1262
+ /**
1263
+ * Publish the final CPU object outcome and loaded state. Every
1264
+ * CjsResource-compatible handle owns the reader/converter result through
1265
+ * `SetPayload`, making CPU payloads visible to MotherLode retention policy.
1266
+ * Base resources also expose the payload through the compatibility `object`
1267
+ * alias; semantic subclasses continue to return and alias the resource while
1268
+ * retaining their validated payload privately.
1269
+ *
1270
+ * @param {CjsResource} resource Target canonical resource handle.
1271
+ * @param {*} object Final reader or format-converter outcome.
1272
+ * @param {object} [options={}] Semantic resource validation/publication options.
1273
+ * @returns {*} Plain payload for a base resource, or the semantic resource handle.
1274
+ * @throws {TypeError|Error} If ownership, semantic payload validation, or loaded-state publication fails.
1275
+ */
1276
+ PublishResourceObject(resource, object, options = {}) {
1277
+ const ownership = this.#GetResourceOwnership(resource, "publish:begin");
1278
+ if (!ownership) {
1279
+ return this.#PublishResourceObjectValue(resource, object, options);
1280
+ }
1281
+ const finishOperation = this.#BeginResourceOperation(false);
1282
+ try {
1283
+ return this.#PublishResourceObject(ownership, resource, object, options);
1284
+ } finally {
1285
+ finishOperation();
1286
+ }
1287
+ }
1288
+
1289
+ /**
1290
+ * Publish only while the captured resource authority remains canonical.
1291
+ * The post-publication check catches synchronous reentrant ownership changes
1292
+ * from state listeners before the caller can treat obsolete work as success.
1293
+ *
1294
+ * @param {CjsResourceMutationAuthority|null} ownership Captured canonical or reload-candidate authority.
1295
+ * @param {object|Function} resource Resource receiving the final payload.
1296
+ * @param {*} object Final reader/converter outcome.
1297
+ * @param {object} options Semantic resource publication options.
1298
+ * @returns {*} Plain payload or semantic resource handle.
1299
+ */
1300
+ #PublishResourceObject(ownership, resource, object, options) {
1301
+ this.#AssertOptionalResourceOwnership(ownership, "publish");
1302
+ const result = this.#PublishResourceObjectValue(resource, object, options);
1303
+ this.#AssertOptionalResourceOwnership(ownership, "publish-settled");
1304
+ return result;
1305
+ }
1306
+
1307
+ /**
1308
+ * Apply the unguarded synchronous payload/state mutation after the caller
1309
+ * has established any required canonical authority.
1310
+ *
1311
+ * @param {object|Function} resource Resource receiving the final payload.
1312
+ * @param {*} object Final reader/converter outcome.
1313
+ * @param {object} options Semantic resource publication options.
1314
+ * @returns {*} Plain payload or semantic resource handle.
1315
+ */
1316
+ #PublishResourceObjectValue(resource, object, options) {
1317
+ let result = object;
441
1318
  if (resource.constructor !== _CjsResource && typeof resource.SetPayload === "function") {
442
1319
  resource.SetPayload(object, options);
443
1320
  resource.object = resource;
444
1321
  result = resource;
445
1322
  } else {
1323
+ resource.SetPayload?.(object, options);
446
1324
  resource.object = object;
447
1325
  }
448
1326
  if (!resource.IsPrepared?.()) resource.MarkLoaded();
@@ -452,61 +1330,58 @@ class CjsResMan extends CjsEventEmitter {
452
1330
  const entries = Array.isArray(paths) ? paths : [paths];
453
1331
  return Promise.all(entries.map(path => this.LoadObject(path, options)));
454
1332
  }
1333
+
1334
+ /**
1335
+ * Return the currently registered direct reader for an input extension.
1336
+ *
1337
+ * @param {string} ext Input extension with or without a leading dot.
1338
+ * @returns {Function|null} Registered loader or `null`.
1339
+ */
455
1340
  GetObjectLoader(ext) {
456
1341
  return this.objectLoaders.get(normalizeResourceExtension(ext)) || null;
457
1342
  }
1343
+
1344
+ /**
1345
+ * Return detached references to the current format descriptors for one input
1346
+ * extension. Each descriptor contains its facade and frozen defaults.
1347
+ *
1348
+ * @param {string} inputType Input extension with or without a leading dot.
1349
+ * @returns {object[]} Descriptor list in registration order.
1350
+ */
458
1351
  GetFormatDescriptors(inputType) {
459
1352
  return [...(this.formats.get(normalizeResourceExtension(inputType)) || [])];
460
1353
  }
1354
+
1355
+ /**
1356
+ * Resolve one current format registration descriptor. Output/media filters
1357
+ * run first; optional source bytes may then run support probes to disambiguate.
1358
+ *
1359
+ * @param {string} inputType Input extension with or without a leading dot.
1360
+ * @param {object} [options={}] Format, output, media type, and optional byte selectors.
1361
+ * @returns {object} Selected immutable registration descriptor.
1362
+ * @throws {Error} If no candidate matches or multiple candidates remain.
1363
+ */
461
1364
  ResolveFormatDescriptor(inputType, options = {}) {
462
1365
  const key = normalizeResourceExtension(inputType);
463
- let candidates = this.GetFormatDescriptors(key);
464
- if (options.format) {
465
- candidates = candidates.filter(({
466
- Format
467
- }) => Format === options.format || Format.id === options.format || Format.name === options.format);
468
- }
469
- if (options.emit) {
470
- candidates = candidates.filter(({
471
- Format
472
- }) => [...(Format.outputTypes || []), ...(Format.debugOutputTypes || [])].includes(options.emit));
473
- }
474
- if (options.mediaType) {
475
- candidates = candidates.filter(({
476
- Format
477
- }) => (Format.mediaTypes || []).includes(options.mediaType));
478
- }
479
- if (candidates.length > 1 && options.bytes !== undefined) {
480
- const supported = candidates.filter(({
481
- Format,
482
- defaults
483
- }) => {
484
- if (typeof Format.isSupported !== "function") return false;
485
- const report = Format.isSupported(options.bytes, {
486
- ...defaults,
487
- ...(options.formatOptions || {})
488
- });
489
- return report && report.supported !== false;
490
- });
491
- if (supported.length === 1) candidates = supported;
492
- }
493
- if (candidates.length === 0) {
494
- const error = new Error(`No format registered for .${key}`);
495
- error.code = "CJS_RESOURCE_FORMAT_MISSING";
496
- error.ext = key;
497
- throw error;
498
- }
499
- if (candidates.length > 1) {
500
- const error = new Error(`Ambiguous formats registered for .${key}`);
501
- error.code = "CJS_RESOURCE_FORMAT_AMBIGUOUS";
502
- error.ext = key;
503
- error.formats = candidates.map(({
504
- Format
505
- }) => Format.name);
506
- throw error;
1366
+ const descriptors = this.GetFormatDescriptors(key);
1367
+ const candidates = FilterFormatDescriptors(descriptors, options);
1368
+ if (descriptors.length > 0 && candidates.length === 0 && options.emit !== undefined) {
1369
+ throw CreateFormatOutputMissingError(key, options.emit, descriptors);
507
1370
  }
508
- return candidates[0];
1371
+ return ResolveFormatDescriptorCandidates(candidates, key, options);
509
1372
  }
1373
+
1374
+ /**
1375
+ * Invoke one format descriptor using its frozen registration defaults and
1376
+ * the request's explicit format options. Static async/sync readers take
1377
+ * precedence over instance `ReadAsync`/`Read` compatibility methods.
1378
+ *
1379
+ * @param {object} descriptor Selected immutable registration descriptor.
1380
+ * @param {*} bytes Source bytes supplied to the format facade.
1381
+ * @param {object} [options={}] Output, class, and `formatOptions` overrides.
1382
+ * @returns {Promise<*>} Parsed format outcome.
1383
+ * @throws {TypeError|Error} If the facade lacks a reader or reading fails.
1384
+ */
510
1385
  async ReadFormat(descriptor, bytes, options = {}) {
511
1386
  const {
512
1387
  Format,
@@ -516,7 +1391,9 @@ class CjsResMan extends CjsEventEmitter {
516
1391
  ...defaults,
517
1392
  ...(options.formatOptions || {})
518
1393
  };
519
- if (options.emit !== undefined) formatOptions.emit = options.emit;
1394
+ if (options.emit !== undefined) {
1395
+ formatOptions.emit = FindDeclaredOutput(GetFormatOutputs(Format), options.emit) ?? options.emit;
1396
+ }
520
1397
  if (options.classes !== undefined) formatOptions.classes = options.classes;
521
1398
  if (typeof Format.readAsync === "function") {
522
1399
  return Format.readAsync(bytes, formatOptions);
@@ -533,13 +1410,171 @@ class CjsResMan extends CjsEventEmitter {
533
1410
  }
534
1411
  throw new TypeError(`${Format.name} does not expose a read operation.`);
535
1412
  }
1413
+
1414
+ /**
1415
+ * Look up and explicitly renew an existing canonical resource without
1416
+ * creating, loading, or preparing a new handle.
1417
+ *
1418
+ * @param {string} path Carbon-style source resource path.
1419
+ * @param {object} [options={}] Identity-defining resource outcome settings.
1420
+ * @returns {CjsResource|null} Existing canonical handle, or `null` when absent.
1421
+ * @throws {TypeError} If the path or identity settings cannot be normalized.
1422
+ */
536
1423
  Lookup(path, options = {}) {
537
- return this.motherLode.Lookup(path, this.GetResourceVariant(options));
1424
+ const normalizedPath = normalizeResourcePath(path);
1425
+ const key = getMotherLodeKey(normalizedPath, this.GetResourceVariant(options));
1426
+ const resource = this.motherLode.Lookup(key);
1427
+ if (resource) {
1428
+ this.#BindResourceLifecycle(key, resource);
1429
+ this.motherLode.KeepAlive?.(key);
1430
+ }
1431
+ return resource;
538
1432
  }
1433
+
1434
+ /**
1435
+ * Forget and clean canonical resources for a source path. Omitting options
1436
+ * removes every resolved variant; supplying options removes only the exact
1437
+ * identity derived from those outcome settings. Explicitly retained source
1438
+ * and format records are independent and remain available; call
1439
+ * {@link CjsResMan#InvalidateReadCache} when they must also be forgotten.
1440
+ *
1441
+ * @param {string} path Carbon-style source resource path.
1442
+ * @param {object|null} [options=null] Exact identity settings, or `null` for all variants.
1443
+ * @returns {boolean} Whether at least one canonical resource was removed.
1444
+ * @throws {TypeError} If the path or identity settings cannot be normalized.
1445
+ * @throws {Error|AggregateError} If cleanup fails after identities are forgotten.
1446
+ */
539
1447
  Delete(path, options = null) {
540
- return options === null || options === undefined ? this.motherLode.DeleteAll(path) : this.motherLode.Delete(path, this.GetResourceVariant(options));
1448
+ if (options !== null && options !== undefined) {
1449
+ const normalizedPath = normalizeResourcePath(path);
1450
+ const key = getMotherLodeKey(normalizedPath, this.GetResourceVariant(options));
1451
+ const resource = this.motherLode.Lookup(key);
1452
+ try {
1453
+ return this.motherLode.Delete(key);
1454
+ } finally {
1455
+ if (resource && this.motherLode.Lookup(key) !== resource) {
1456
+ this.#InvalidateResourceOwnership(resource);
1457
+ }
1458
+ this.#reloadGenerations.delete(key);
1459
+ }
1460
+ }
1461
+ const normalizedPath = normalizeResourcePath(path);
1462
+ const prefix = `${normalizedPath}\u0000`;
1463
+ const entries = typeof this.motherLode?.Entries === "function" ? [...this.motherLode.Entries()].filter(([key]) => key === normalizedPath || key.startsWith(prefix)) : [];
1464
+ try {
1465
+ return this.motherLode.DeleteAllVariants(normalizedPath);
1466
+ } finally {
1467
+ for (const [key, resource] of entries) {
1468
+ this.#reloadGenerations.delete(key);
1469
+ if (this.motherLode.Lookup(key) !== resource) {
1470
+ this.#InvalidateResourceOwnership(resource);
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ /**
1477
+ * Run one explicit deterministic identity/payload inactivity sweep.
1478
+ * This delegates policy and cleanup to MotherLode; it never fetches or
1479
+ * reloads a purged resource.
1480
+ *
1481
+ * @param {object} [options={}] MotherLode frame/time limits and cleanup policy.
1482
+ * @returns {object} Immutable purge counts and affected canonical keys.
1483
+ * @throws {TypeError} If the configured MotherLode lacks purge support or policy is invalid.
1484
+ * @throws {AggregateError} If one or more candidate resources fail cleanup.
1485
+ */
1486
+ PurgeInactive(options = {}) {
1487
+ if (typeof this.motherLode?.PurgeInactive !== "function") {
1488
+ throw new TypeError("CjsResMan MotherLode does not support PurgeInactive().");
1489
+ }
1490
+ return this.motherLode.PurgeInactive(options);
1491
+ }
1492
+
1493
+ /**
1494
+ * Replace or disable the opt-in automatic inactivity policy. Configuration
1495
+ * resets cadence, so the first subsequent pump performs a sweep immediately.
1496
+ * Frame-based limits are rejected because manager updates are not a reliable
1497
+ * renderer-frame clock. The normalized policy is frozen and never reloads
1498
+ * resources.
1499
+ *
1500
+ * @param {CjsResManAutoPurgePolicy|false|null} [policy=null] Time-based policy, or `false`/`null` to disable automatic sweeping.
1501
+ * @returns {CjsResMan} This resource manager.
1502
+ * @throws {TypeError} If the policy, threshold, cleanup control, or clock is invalid.
1503
+ */
1504
+ SetAutoPurgePolicy(policy = null) {
1505
+ this.#autoPurgePolicy = NormalizeAutoPurgePolicy(policy);
1506
+ this.#lastAutoPurgeTime = null;
1507
+ return this;
1508
+ }
1509
+
1510
+ /**
1511
+ * Return the immutable normalized automatic inactivity policy.
1512
+ *
1513
+ * @returns {Readonly<CjsResManAutoPurgePolicy>|null} Active policy, or `null` when automatic sweeping is disabled.
1514
+ */
1515
+ GetAutoPurgePolicy() {
1516
+ return this.#autoPurgePolicy;
1517
+ }
1518
+
1519
+ /**
1520
+ * Report whether {@link CjsResMan#Update} may run automatic inactivity
1521
+ * sweeps. This is a pure query and does not advance cadence or activity.
1522
+ *
1523
+ * @returns {boolean} Whether an automatic purge policy is configured.
1524
+ */
1525
+ IsAutoPurgeEnabled() {
1526
+ return this.#autoPurgePolicy !== null;
1527
+ }
1528
+
1529
+ /**
1530
+ * Run the configured automatic sweep when its minimum time interval has
1531
+ * elapsed. The first pump after configuration runs immediately. A regressing
1532
+ * clock rebases cadence and skips that pump; failures consume the interval so
1533
+ * repeated updates cannot form a cleanup-error storm. This method never
1534
+ * creates, fetches, prepares, or reloads a resource.
1535
+ *
1536
+ * @param {CjsResManAutoPurgePumpOptions} [options={}] Optional deterministic timestamp.
1537
+ * @returns {object|null} Immutable MotherLode purge result when due, otherwise `null`.
1538
+ * @throws {TypeError} If options, the clock result, or MotherLode purge policy are invalid.
1539
+ * @throws {AggregateError} If a due sweep cannot clean one or more inactive resources.
1540
+ */
1541
+ PumpAutoPurge(options = {}) {
1542
+ const pump = NormalizeAutoPurgePumpOptions(options);
1543
+ const policy = this.#autoPurgePolicy;
1544
+ if (!policy) return null;
1545
+ const time = pump.time === undefined ? policy.now() : pump.time;
1546
+ AssertNonNegativeNumber(time, "automatic purge time");
1547
+ if (this.#lastAutoPurgeTime !== null) {
1548
+ if (time < this.#lastAutoPurgeTime) {
1549
+ this.#lastAutoPurgeTime = time;
1550
+ return null;
1551
+ }
1552
+ if (time - this.#lastAutoPurgeTime < policy.intervalMilliseconds) {
1553
+ return null;
1554
+ }
1555
+ }
1556
+ this.#lastAutoPurgeTime = time;
1557
+ return this.PurgeInactive({
1558
+ time,
1559
+ maxIdleMilliseconds: policy.maxIdleMilliseconds,
1560
+ payloadMaxIdleMilliseconds: policy.payloadMaxIdleMilliseconds,
1561
+ destroyAdapters: policy.destroyAdapters,
1562
+ releasePayload: policy.releasePayload,
1563
+ ...(policy.cleanup === undefined ? {} : {
1564
+ cleanup: policy.cleanup
1565
+ })
1566
+ });
541
1567
  }
1568
+
1569
+ /**
1570
+ * Cancel queued-but-not-started work, remove every canonical resource through
1571
+ * MotherLode cleanup, and reset all in-flight deduplication ledgers.
1572
+ *
1573
+ * @returns {CjsResMan} This empty resource manager.
1574
+ * @throws {AggregateError} If one or more canonical resources fail cleanup.
1575
+ */
542
1576
  Clear() {
1577
+ this.#InvalidateMotherLodeOwnership(this.motherLode);
543
1578
  this._loadQueue.Clear();
544
1579
  this._prepareQueue.Clear();
545
1580
  this.motherLode.Clear();
@@ -547,48 +1582,289 @@ class CjsResMan extends CjsEventEmitter {
547
1582
  this.sourceOperations = new WeakMap();
548
1583
  this.queuedSourceOperations = new WeakMap();
549
1584
  this.formatOperations = new WeakMap();
1585
+ this.#reloadGenerations.clear();
1586
+ this.#lastAutoPurgeTime = null;
550
1587
  return this;
551
1588
  }
552
- async ReadResource(path, options = {}) {
1589
+
1590
+ /**
1591
+ * Detach reusable source and parsed-format records for one source path.
1592
+ * Omitting `sourceRevision` removes every revision for the selected source;
1593
+ * supplying it removes only that string/finite-number revision. Existing
1594
+ * consumers keep their promises: invalidation does not abort, reject, touch
1595
+ * MotherLode/payload ownership, or perform source work.
1596
+ *
1597
+ * `Delete()` deliberately remains resource-identity-only. Use this method
1598
+ * when explicitly retained read caches must also be forgotten; `Clear()`
1599
+ * resets every read ledger.
1600
+ *
1601
+ * @param {string} path Carbon-style source resource path.
1602
+ * @param {CjsResManReadCacheOptions} [options={}] Optional `source` and exact `sourceRevision` selection.
1603
+ * @returns {Readonly<{path: string, queuedSource: number, source: number, format: number}>} Frozen detached-record counts.
1604
+ * @throws {TypeError} If path, source, revision, or options are invalid.
1605
+ */
1606
+ InvalidateReadCache(path, options = {}) {
1607
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
1608
+ throw new TypeError("CjsResMan.InvalidateReadCache options must be an object.");
1609
+ }
1610
+ const normalizedPath = normalizeResourcePath(path);
1611
+ const hasRevision = Object.prototype.hasOwnProperty.call(options, "sourceRevision");
1612
+ const revisionKey = hasRevision ? NormalizeSourceRevision(options.sourceRevision) : null;
1613
+ const source = options.source || this.source;
1614
+ if (!source) {
1615
+ return Object.freeze({
1616
+ path: normalizedPath,
1617
+ queuedSource: 0,
1618
+ source: 0,
1619
+ format: 0
1620
+ });
1621
+ }
1622
+ if (typeof source !== "object" && typeof source !== "function" || typeof source.Read !== "function") {
1623
+ throw new TypeError("CjsResMan.InvalidateReadCache source must provide Read(path, options).");
1624
+ }
1625
+ return this.#InvalidateReadCache(source, normalizedPath, revisionKey);
1626
+ }
1627
+
1628
+ /**
1629
+ * Read source data under source/path/revision cache identity. Cache policy is
1630
+ * tri-state: omitted shares in-flight or explicitly retained work, `true`
1631
+ * retains success, and `false` bypasses sharing and retention. Failures are
1632
+ * never retained. A joining `true` request upgrades the shared record.
1633
+ *
1634
+ * `reload: true` invalidates the selected source/path synchronously before
1635
+ * beginning fresh work; it does not cancel already detached consumers.
1636
+ *
1637
+ * @param {string} path Carbon-style source resource path.
1638
+ * @param {CjsResManReadCacheOptions} [options={}] Source, revision, reload, and cache controls forwarded to the source.
1639
+ * @returns {Promise<*>} Promise for bytes or source-compatible data.
1640
+ * @throws {TypeError} If path, source, revision, cache policy, or options are invalid.
1641
+ */
1642
+ ReadResource(path, options = {}) {
1643
+ const read = this.#BeginReadOperation(path, options);
1644
+ return this.#ReadResource(read.context, read.options);
1645
+ }
1646
+
1647
+ /**
1648
+ * Normalize one source provenance context and invalidate stale path records
1649
+ * once at the outermost reload boundary.
1650
+ *
1651
+ * @param {string} path Source path.
1652
+ * @param {object} options Read options.
1653
+ * @returns {{context: Readonly<CjsResourceReadContext>, options: object}} Context and detached operation options.
1654
+ * @throws {TypeError} If source provenance or options are invalid.
1655
+ */
1656
+ #BeginReadOperation(path, options) {
1657
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
1658
+ throw new TypeError("CjsResMan read options must be an object.");
1659
+ }
1660
+ const normalizedPath = normalizeResourcePath(path);
1661
+ const inherited = READ_CONTEXTS.get(options);
1662
+ if (inherited && inherited.resMan === this && inherited.path === normalizedPath) {
1663
+ return {
1664
+ context: inherited,
1665
+ options
1666
+ };
1667
+ }
553
1668
  const source = options.source || this.source;
554
- if (!source || typeof source.Read !== "function") {
1669
+ if (!source || typeof source !== "object" && typeof source !== "function" || typeof source.Read !== "function") {
555
1670
  throw new TypeError("CjsResMan requires a source with Read(path, options) to load objects.");
556
1671
  }
557
- if (options.reload === true || options.cacheSource === false) {
558
- return source.Read(path, options);
1672
+ const sourceRevision = options.sourceRevision;
1673
+ const revisionKey = NormalizeSourceRevision(sourceRevision);
1674
+ NormalizeCachePolicy(options.cacheSource, "cacheSource");
1675
+ NormalizeCachePolicy(options.cacheFormat, "cacheFormat");
1676
+ const operationOptions = {
1677
+ ...options
1678
+ };
1679
+ const context = Object.freeze({
1680
+ resMan: this,
1681
+ source,
1682
+ path: normalizedPath,
1683
+ sourceRevision,
1684
+ revisionKey
1685
+ });
1686
+ READ_CONTEXTS.set(operationOptions, context);
1687
+ if (options.reload === true) {
1688
+ this.#InvalidateReadCache(source, normalizedPath, null);
559
1689
  }
560
- let operations = this.sourceOperations.get(source);
561
- if (!operations) {
562
- operations = new Map();
563
- this.sourceOperations.set(source, operations);
1690
+ return {
1691
+ context,
1692
+ options: operationOptions
1693
+ };
1694
+ }
1695
+
1696
+ /**
1697
+ * Queue one normalized source operation and deduplicate according to the
1698
+ * caller's cache policy.
1699
+ *
1700
+ * @param {Readonly<CjsResourceReadContext>} context Normalized provenance.
1701
+ * @param {object} options Detached read options.
1702
+ * @returns {Promise<*>} Promise for source bytes/data.
1703
+ */
1704
+ #QueueReadResource(context, options) {
1705
+ const cachePolicy = NormalizeCachePolicy(options.cacheSource, "cacheSource");
1706
+ const operations = GetOwnerOperations(this.queuedSourceOperations, context.source, true);
1707
+ const key = GetReadOperationKey(context);
1708
+ const bypassExisting = options.reload === true || cachePolicy === false;
1709
+ const existing = bypassExisting ? null : operations.get(key);
1710
+ if (existing) {
1711
+ if (cachePolicy === true) {
1712
+ existing.retain = true;
1713
+ const sourceRecord = GetOwnerOperations(this.sourceOperations, context.source, false)?.get(key);
1714
+ if (sourceRecord) sourceRecord.retain = true;
1715
+ }
1716
+ return existing.promise;
564
1717
  }
565
- const key = normalizeResourcePath(path);
566
- const existing = operations.get(key);
567
- if (existing) return existing;
568
- const operation = Promise.resolve().then(() => source.Read(path, options));
569
- operations.set(key, operation);
570
- operation.then(() => {
571
- if (options.cacheSource !== true && operations.get(key) === operation) operations.delete(key);
1718
+
1719
+ /** @type {CjsResourceReadOperationRecord} */
1720
+ const record = {
1721
+ promise: null,
1722
+ path: context.path,
1723
+ revisionKey: context.revisionKey,
1724
+ retain: cachePolicy === true
1725
+ };
1726
+ record.promise = this.QueueTask(CjsResManQueue.BACKGROUND, () => this.#ReadResource(context, options, record), context.source, {
1727
+ kind: "load",
1728
+ path: context.path
1729
+ }).promise;
1730
+ if (cachePolicy !== false) operations.set(key, record);
1731
+ record.promise.then(value => {
1732
+ if (cachePolicy === false || operations.get(key) !== record) return;
1733
+ if (record.retain) this.#RetainSourceResult(context, value);
1734
+ operations.delete(key);
572
1735
  }, () => {
573
- if (operations.get(key) === operation) operations.delete(key);
1736
+ if (cachePolicy !== false && operations.get(key) === record) operations.delete(key);
1737
+ });
1738
+ return record.promise;
1739
+ }
1740
+
1741
+ /**
1742
+ * Run or join one normalized direct source operation.
1743
+ *
1744
+ * @param {Readonly<CjsResourceReadContext>} context Normalized provenance.
1745
+ * @param {object} options Detached read options.
1746
+ * @param {CjsResourceReadOperationRecord|null} [queuedRecord=null] Parent queued record whose retention may be promoted by a later join.
1747
+ * @returns {Promise<*>} Promise for source bytes/data.
1748
+ */
1749
+ #ReadResource(context, options, queuedRecord = null) {
1750
+ const cachePolicy = NormalizeCachePolicy(options.cacheSource, "cacheSource");
1751
+ const operations = GetOwnerOperations(this.sourceOperations, context.source, true);
1752
+ const key = GetReadOperationKey(context);
1753
+ const bypassExisting = options.reload === true || cachePolicy === false;
1754
+ const existing = bypassExisting ? null : operations.get(key);
1755
+ if (existing) {
1756
+ if (cachePolicy === true || queuedRecord?.retain) existing.retain = true;
1757
+ return existing.promise;
1758
+ }
1759
+
1760
+ /** @type {CjsResourceReadOperationRecord} */
1761
+ const record = {
1762
+ promise: null,
1763
+ path: context.path,
1764
+ revisionKey: context.revisionKey,
1765
+ retain: cachePolicy === true || queuedRecord?.retain === true
1766
+ };
1767
+ record.promise = Promise.resolve().then(() => context.source.Read(context.path, options));
1768
+ if (cachePolicy !== false) operations.set(key, record);
1769
+ record.promise.then(() => {
1770
+ if (cachePolicy !== false && !record.retain && operations.get(key) === record) {
1771
+ operations.delete(key);
1772
+ }
1773
+ }, () => {
1774
+ if (cachePolicy !== false && operations.get(key) === record) operations.delete(key);
1775
+ });
1776
+ return record.promise;
1777
+ }
1778
+
1779
+ /**
1780
+ * Retain a settled source result when a queued caller upgraded cache policy
1781
+ * after the direct source promise had already completed its cleanup reaction.
1782
+ *
1783
+ * @param {Readonly<CjsResourceReadContext>} context Normalized provenance.
1784
+ * @param {*} value Settled source result.
1785
+ * @returns {void}
1786
+ */
1787
+ #RetainSourceResult(context, value) {
1788
+ const operations = GetOwnerOperations(this.sourceOperations, context.source, true);
1789
+ const key = GetReadOperationKey(context);
1790
+ const existing = operations.get(key);
1791
+ if (existing) {
1792
+ existing.retain = true;
1793
+ return;
1794
+ }
1795
+ operations.set(key, {
1796
+ promise: Promise.resolve(value),
1797
+ path: context.path,
1798
+ revisionKey: context.revisionKey,
1799
+ retain: true
574
1800
  });
575
- return operation;
576
1801
  }
577
- CreateResource(Constructor, path, ext, options = {}) {
1802
+
1803
+ /**
1804
+ * Remove future-reuse records matching one selected source and path.
1805
+ * A null revision removes all revisions.
1806
+ *
1807
+ * @param {object|Function} source Selected source.
1808
+ * @param {string} path Normalized source path.
1809
+ * @param {string|null} revisionKey Exact revision key or `null` for all.
1810
+ * @returns {Readonly<{path: string, queuedSource: number, source: number, format: number}>} Frozen detached counts.
1811
+ */
1812
+ #InvalidateReadCache(source, path, revisionKey) {
1813
+ const queuedSource = RemoveOperationRecords(GetOwnerOperations(this.queuedSourceOperations, source, false), path, revisionKey);
1814
+ const sourceCount = RemoveOperationRecords(GetOwnerOperations(this.sourceOperations, source, false), path, revisionKey);
1815
+ let format = 0;
1816
+ const descriptors = this.formatOperations.get(source);
1817
+ if (descriptors) {
1818
+ for (const [descriptor, operations] of descriptors) {
1819
+ format += RemoveOperationRecords(operations, path, revisionKey);
1820
+ if (operations.size === 0) descriptors.delete(descriptor);
1821
+ }
1822
+ if (descriptors.size === 0) this.formatOperations.delete(source);
1823
+ }
1824
+ return Object.freeze({
1825
+ path,
1826
+ queuedSource,
1827
+ source: sourceCount,
1828
+ format
1829
+ });
1830
+ }
1831
+
1832
+ /**
1833
+ * Construct and initialize one resource handle, optionally rejecting a
1834
+ * singleton alias before `Initialize()` can mutate the protected owner.
1835
+ *
1836
+ * @param {Function} Constructor Resource constructor selected for the outcome.
1837
+ * @param {string} path Normalized Carbon-style resource path.
1838
+ * @param {string} ext Normalized resource extension.
1839
+ * @param {object} [options={}] Constructor values, requested output, semantic requirement, and source provenance.
1840
+ * @param {object|Function|null} [disallowedAlias=null] Existing owner that a staged candidate must not reuse.
1841
+ * @returns {CjsResource} Initialized resource-compatible handle.
1842
+ * @throws {TypeError|Error} If construction, candidate identity, or initialization is invalid.
1843
+ */
1844
+ CreateResource(Constructor, path, ext, options = {}, disallowedAlias = null) {
578
1845
  const resource = new Constructor(options.values);
579
1846
  if (!resource || typeof resource.Initialize !== "function") {
580
1847
  throw new TypeError("Resource constructor must create a CjsResource-compatible object.");
581
1848
  }
1849
+ if (disallowedAlias && resource === disallowedAlias) {
1850
+ throw ReloadCandidateAliasError(path, resource);
1851
+ }
582
1852
  resource.Initialize(path, ext, NormalizeRequirement(options.requirement || options.payload || ""));
583
1853
  if (typeof resource.SetObjectLoader === "function") {
584
- const identityOptions = GetIdentityOptions(options);
585
- resource.SetObjectLoader(loadOptions => this.GetObject(path, {
586
- ...identityOptions,
587
- ...loadOptions
588
- }));
1854
+ const loaderOptions = GetResourceLoaderOptions(options, options.source || this.source);
1855
+ resource.SetObjectLoader(loadOptions => this.GetObject(path, MergeResourceLoaderOptions(loaderOptions, loadOptions)), loaderOptions);
589
1856
  }
590
1857
  return resource;
591
1858
  }
1859
+
1860
+ /**
1861
+ * Resolve the current resource constructor for a semantic request. Explicit
1862
+ * requirement/payload selection precedes an emitted-output registration;
1863
+ * otherwise the generic `CjsResource` handle is used.
1864
+ *
1865
+ * @param {object} [options={}] Semantic requirement, payload, and emit request.
1866
+ * @returns {Function} Registered resource constructor or `CjsResource`.
1867
+ */
592
1868
  ResolveResourceConstructor(options = {}) {
593
1869
  const requested = NormalizeRequirement(options.requirement || options.payload);
594
1870
  if (requested && this.resourceTypes.has(requested)) return this.resourceTypes.get(requested);
@@ -596,86 +1872,855 @@ class CjsResMan extends CjsEventEmitter {
596
1872
  if (emitted && this.resourceTypes.has(emitted)) return this.resourceTypes.get(emitted);
597
1873
  return _CjsResource;
598
1874
  }
1875
+
1876
+ /**
1877
+ * Resolve the promised output tag used with the normalized source path.
1878
+ *
1879
+ * An explicit `variant` wins. Otherwise the requested emitted output is the
1880
+ * promise, followed by the semantic requirement/payload for resource-only
1881
+ * requests. Reader classes, constructors, and format options
1882
+ * are execution details and never enter this identity.
1883
+ *
1884
+ * @param {object} [options={}] Resource request containing an optional output tag.
1885
+ * @returns {string} Normalized promised output tag, or an empty string for the default outcome.
1886
+ * @throws {TypeError} If options or its selected tag is invalid.
1887
+ */
599
1888
  GetResourceVariant(options = {}) {
600
- const identity = GetIdentityOptions(options);
601
- return Object.keys(identity).length ? StableSerialize(identity) : "";
1889
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
1890
+ throw new TypeError("CjsResMan resource variant options must be an object.");
1891
+ }
1892
+ if (Object.prototype.hasOwnProperty.call(options, "variant") && options.variant !== undefined) {
1893
+ if (typeof options.variant !== "string" || options.variant.trim() === "") {
1894
+ throw new TypeError("CjsResMan explicit resource variant must be a non-empty string.");
1895
+ }
1896
+ return NormalizeResourceVariant(options.variant);
1897
+ }
1898
+ return NormalizeResourceVariant(options.emit ?? options.requirement ?? options.payload ?? "");
602
1899
  }
603
- ReadFormatOnce(resource, descriptor, bytes, options = {}) {
1900
+
1901
+ /**
1902
+ * Retain a low-level queue task only until settlement so `Wait()` can take a
1903
+ * synchronous snapshot without making completed values long-lived.
1904
+ *
1905
+ * @param {object} task Queue task returned by `CjsResManWorkQueue.Add()`.
1906
+ * @returns {object} The supplied task.
1907
+ */
1908
+ #TrackQueueTask(task) {
1909
+ this.#queueOperations.set(task, task.promise);
1910
+ task.promise.then(() => {
1911
+ if (this.#queueOperations.get(task) === task.promise) {
1912
+ this.#queueOperations.delete(task);
1913
+ }
1914
+ }, () => {
1915
+ if (this.#queueOperations.get(task) === task.promise) {
1916
+ this.#queueOperations.delete(task);
1917
+ }
1918
+ });
1919
+ return task;
1920
+ }
1921
+
1922
+ /**
1923
+ * Open one active resource-mutation record. Queued records additionally own
1924
+ * a settlement promise visible to contemporaneous `Wait()` snapshots;
1925
+ * direct records block unsafe MotherLode replacement but remain outside that
1926
+ * two-queue fence.
1927
+ *
1928
+ * @param {boolean} queued Whether the operation belongs to the queued fence.
1929
+ * @returns {() => void} Idempotent record completion callback.
1930
+ */
1931
+ #BeginResourceOperation(queued) {
1932
+ this.#activeResourceOperations += 1;
1933
+ const id = queued ? this.#nextResourceOperationId++ : 0;
1934
+ let resolveDone = Noop;
1935
+ const done = queued ? new Promise(resolve => {
1936
+ resolveDone = resolve;
1937
+ }) : null;
1938
+ if (queued) this.#resourceOperations.set(id, done);
1939
+ let finished = false;
1940
+ return () => {
1941
+ if (finished) return;
1942
+ finished = true;
1943
+ this.#activeResourceOperations = Math.max(0, this.#activeResourceOperations - 1);
1944
+ if (queued && this.#resourceOperations.get(id) === done) {
1945
+ this.#resourceOperations.delete(id);
1946
+ }
1947
+ resolveDone();
1948
+ };
1949
+ }
1950
+
1951
+ /**
1952
+ * Return the known canonical authority for a resource and reject a known
1953
+ * stale generation. A never-bound resource returns `null` so historical
1954
+ * detached prepare/publication helpers remain compatible.
1955
+ *
1956
+ * @param {object|Function} resource Candidate resource handle.
1957
+ * @param {string} phase Operation phase used for stale diagnostics.
1958
+ * @returns {CjsResourceOwnership|null} Current authority or `null` when never bound.
1959
+ * @throws {Error} If a previously bound resource is no longer canonical.
1960
+ */
1961
+ #GetResourceOwnership(resource, phase) {
1962
+ const ownership = this.#resourceOwnership.get(resource) || null;
1963
+ if (!ownership) return null;
1964
+ this.#AssertResourceOwnership(ownership, phase);
1965
+ return ownership;
1966
+ }
1967
+
1968
+ /**
1969
+ * Require a current manager-owned canonical resource authority.
1970
+ *
1971
+ * @param {object|Function} resource Candidate resource handle.
1972
+ * @param {string} phase Operation phase used for diagnostics.
1973
+ * @returns {CjsResourceOwnership} Current canonical authority.
1974
+ * @throws {Error} If the resource is unknown or no longer canonical.
1975
+ */
1976
+ #RequireResourceOwnership(resource, phase) {
1977
+ const ownership = this.#GetResourceOwnership(resource, phase);
1978
+ if (ownership) return ownership;
1979
+ throw ResourceNotOwnedError(resource, phase);
1980
+ }
1981
+
1982
+ /**
1983
+ * Test whether an asynchronous operation still owns publication authority.
1984
+ * Registry lookup failures are treated as stale ownership so they cannot
1985
+ * replace an operation's more useful underlying source/stage rejection.
1986
+ *
1987
+ * @param {CjsResourceOwnership} ownership Captured authority.
1988
+ * @returns {boolean} Whether the exact generation remains canonical.
1989
+ */
1990
+ #IsResourceOwnershipCurrent(ownership) {
1991
+ try {
1992
+ return Boolean(ownership && this.motherLode === ownership.owner && this.#resourceOwnership.get(ownership.resource) === ownership && !this.#invalidResourceOwnership.has(ownership.resource) && ownership.owner.Lookup(ownership.key) === ownership.resource && !ownership.resource.IsPurged?.());
1993
+ } catch {
1994
+ return false;
1995
+ }
1996
+ }
1997
+
1998
+ /**
1999
+ * Assert exact canonical publication authority for one operation phase.
2000
+ *
2001
+ * @param {CjsResourceOwnership} ownership Captured authority.
2002
+ * @param {string} phase Human-readable operation phase.
2003
+ * @returns {void}
2004
+ * @throws {Error} Stable stale-operation error when ownership changed.
2005
+ */
2006
+ #AssertResourceOwnership(ownership, phase) {
2007
+ if (!this.#IsResourceOwnershipCurrent(ownership)) {
2008
+ throw StaleResourceOperationError(ownership, phase);
2009
+ }
2010
+ }
2011
+
2012
+ /**
2013
+ * Assert a captured authority when a compatibility path supplied one.
2014
+ *
2015
+ * @param {CjsResourceMutationAuthority|null} ownership Optional canonical or reload-candidate authority.
2016
+ * @param {string} phase Human-readable operation phase.
2017
+ * @returns {void}
2018
+ */
2019
+ #AssertOptionalResourceOwnership(ownership, phase) {
2020
+ if (!ownership) return;
2021
+ if (ownership.reloadCandidate === true) {
2022
+ this.#AssertReloadCandidate(ownership, phase);
2023
+ return;
2024
+ }
2025
+ this.#AssertResourceOwnership(ownership, phase);
2026
+ }
2027
+
2028
+ /**
2029
+ * Test the exact former owner plus newest-request token for an off-registry
2030
+ * candidate. Lookup failures are treated as stale so detached work cannot
2031
+ * resurrect a deleted, cleared, replaced, or superseded identity.
2032
+ *
2033
+ * @param {CjsResourceReloadCandidate} candidate Candidate authority.
2034
+ * @returns {boolean} Whether this candidate alone may still commit.
2035
+ */
2036
+ #IsReloadCandidateCurrent(candidate) {
2037
+ try {
2038
+ return Boolean(candidate && candidate.reloadCandidate === true && this.motherLode === candidate.owner && this.#reloadCandidates.get(candidate.resource) === candidate && this.#reloadGenerations.get(candidate.key) === candidate.generation && this.#resourceOwnership.get(candidate.expected) === candidate.expectedOwnership && this.#IsResourceOwnershipCurrent(candidate.expectedOwnership) && candidate.owner.Lookup(candidate.key) === candidate.expected);
2039
+ } catch {
2040
+ return false;
2041
+ }
2042
+ }
2043
+
2044
+ /**
2045
+ * Assert that a staged candidate still owns newest-request publication
2046
+ * authority for one operation phase.
2047
+ *
2048
+ * @param {CjsResourceReloadCandidate} candidate Candidate authority.
2049
+ * @param {string} phase Human-readable operation phase.
2050
+ * @returns {void}
2051
+ * @throws {Error} Stable stale-candidate error when authority changed.
2052
+ */
2053
+ #AssertReloadCandidate(candidate, phase) {
2054
+ if (!this.#IsReloadCandidateCurrent(candidate)) {
2055
+ throw StaleReloadCandidateError(candidate, phase);
2056
+ }
2057
+ }
2058
+
2059
+ /**
2060
+ * Invalidate the currently recorded generation for a removed resource.
2061
+ *
2062
+ * @param {object|Function} resource Removed or displaced resource.
2063
+ * @returns {void}
2064
+ */
2065
+ #InvalidateResourceOwnership(resource) {
2066
+ if (resource && (typeof resource === "object" || typeof resource === "function")) {
2067
+ this.#invalidResourceOwnership.add(resource);
2068
+ }
2069
+ }
2070
+
2071
+ /**
2072
+ * Invalidate every generation currently owned by one registry. This runs
2073
+ * before Clear cancellation/cleanup so reentrant observers cannot retain
2074
+ * authority after canonical ownership removal begins.
2075
+ *
2076
+ * @param {CjsMotherLode|null|undefined} owner Registry losing ownership.
2077
+ * @returns {void}
2078
+ */
2079
+ #InvalidateMotherLodeOwnership(owner) {
2080
+ if (typeof owner?.Entries !== "function") return;
2081
+ for (const [, resource] of owner.Entries()) {
2082
+ const ownership = this.#resourceOwnership.get(resource);
2083
+ if (ownership?.owner === owner) this.#invalidResourceOwnership.add(resource);
2084
+ }
2085
+ }
2086
+
2087
+ /**
2088
+ * Add one operation-owned purge lock and release it only while the exact
2089
+ * captured canonical record still exists. If removal/replacement made the
2090
+ * operation stale, its former MotherLode record (and lock count) no longer
2091
+ * exists, so releasing through a rebound resource would corrupt a new owner.
2092
+ *
2093
+ * @param {CjsResourceOwnership} ownership Captured canonical authority.
2094
+ * @returns {() => void} Idempotent balanced release callback.
2095
+ */
2096
+ #AcquireResourcePurgeLock(ownership) {
604
2097
  const {
605
- Format
606
- } = descriptor;
607
- let operations = this.formatOperations.get(Format);
608
- if (!operations) {
609
- operations = new Map();
610
- this.formatOperations.set(Format, operations);
611
- }
612
- const key = `${resource.GetPath()}\u0000${StableSerialize({
613
- emit: options.emit,
614
- mediaType: options.mediaType,
615
- classes: options.classes,
616
- formatOptions: options.formatOptions
617
- })}`;
618
- const existing = operations.get(key);
619
- if (existing) return existing;
620
- const operation = Promise.resolve().then(() => this.ReadFormat(descriptor, bytes, options));
621
- operations.set(key, operation);
622
- operation.then(() => {
623
- if (options.cacheFormat !== true && operations.get(key) === operation) operations.delete(key);
2098
+ owner,
2099
+ key
2100
+ } = ownership;
2101
+ if (typeof owner?.Lock !== "function" || typeof owner?.Unlock !== "function") {
2102
+ return Noop;
2103
+ }
2104
+ this.#AssertResourceOwnership(ownership, "lock:acquire");
2105
+ owner.Lock(key);
2106
+ let released = false;
2107
+ return () => {
2108
+ if (released) return;
2109
+ released = true;
2110
+ if (this.#IsResourceOwnershipCurrent(ownership)) owner.Unlock(key);
2111
+ };
2112
+ }
2113
+
2114
+ /**
2115
+ * Bind resource-facing liveness methods to one canonical MotherLode key.
2116
+ * The controller contains no loader/reload callback, so touching a detached
2117
+ * or purged handle cannot start browser/network work.
2118
+ *
2119
+ * @param {string} key Canonical MotherLode identity.
2120
+ * @param {object|Function} resource Canonical resource handle.
2121
+ * @returns {object|Function} The supplied resource.
2122
+ */
2123
+ #BindResourceLifecycle(key, resource) {
2124
+ const owner = this.motherLode;
2125
+ let ownership = this.#resourceOwnership.get(resource) || null;
2126
+ if (!ownership || ownership.owner !== owner || ownership.key !== key || ownership.resource !== resource || !this.#IsResourceOwnershipCurrent(ownership)) {
2127
+ ownership = Object.freeze({
2128
+ generation: this.#nextResourceOwnershipGeneration++,
2129
+ owner,
2130
+ key,
2131
+ resource
2132
+ });
2133
+ this.#resourceOwnership.set(resource, ownership);
2134
+ this.#invalidResourceOwnership.delete(resource);
2135
+ }
2136
+ if (typeof resource?.SetLifecycleController !== "function") return resource;
2137
+ resource.SetLifecycleController(Object.freeze({
2138
+ isCurrent: () => this.#IsResourceOwnershipCurrent(ownership),
2139
+ keepAlive: options => this.#IsResourceOwnershipCurrent(ownership) ? owner.KeepAlive?.(key, options) : null,
2140
+ keepPayloadAlive: options => {
2141
+ if (!this.#IsResourceOwnershipCurrent(ownership)) return null;
2142
+ if (typeof owner.KeepPayloadAlive === "function") {
2143
+ return owner.KeepPayloadAlive(key, options);
2144
+ }
2145
+ return owner.KeepAlive?.(key, options);
2146
+ },
2147
+ lock: () => this.#IsResourceOwnershipCurrent(ownership) ? owner.Lock?.(key) || 0 : 0,
2148
+ unlock: () => this.#IsResourceOwnershipCurrent(ownership) ? owner.Unlock?.(key) || 0 : 0
2149
+ }));
2150
+ if (resource.HasPayload?.() && this.#IsResourceOwnershipCurrent(ownership)) {
2151
+ owner.KeepPayloadAlive?.(key);
2152
+ }
2153
+ return resource;
2154
+ }
2155
+
2156
+ /**
2157
+ * Bind lifecycle callbacks for resources supplied by a configured custom
2158
+ * MotherLode before CjsResMan begins serving them.
2159
+ *
2160
+ * @returns {CjsResMan} This resource manager.
2161
+ */
2162
+ #BindMotherLodeResources() {
2163
+ if (typeof this.motherLode?.Entries !== "function") return this;
2164
+ for (const [key, resource] of this.motherLode.Entries()) {
2165
+ this.#BindResourceLifecycle(key, resource);
2166
+ }
2167
+ return this;
2168
+ }
2169
+
2170
+ /**
2171
+ * Run or join one format read under source/path/revision, frozen descriptor,
2172
+ * and effective output-option identity. Descriptor identity prevents a
2173
+ * re-registration with different defaults from reusing an old parse, while
2174
+ * source identity prevents same-path reads from different sources colliding.
2175
+ *
2176
+ * `cacheFormat` uses the same tri-state policy as source caching: omitted is
2177
+ * in-flight by default, `true` retains success, and `false` bypasses sharing
2178
+ * and retention. `reload: true` never joins a prior parse. Failures are never
2179
+ * retained, and a joining `true` request upgrades a shared record. Requests
2180
+ * with option instances that cannot be represented safely bypass format
2181
+ * sharing regardless of cache policy.
2182
+ *
2183
+ * @param {CjsResource} resource Resource whose normalized path identifies the source data.
2184
+ * @param {object} descriptor Frozen registered format descriptor.
2185
+ * @param {*} bytes Source bytes or source-compatible reader input.
2186
+ * @param {object} [options={}] Source provenance, reload, cacheFormat, emit, classes, and format options.
2187
+ * @returns {Promise<*>} Promise for the parsed or converted format result.
2188
+ * @throws {TypeError|Error} If resource, source, revision, descriptor, cache policy, or format execution is invalid.
2189
+ */
2190
+ ReadFormatOnce(resource, descriptor, bytes, options = {}) {
2191
+ const read = this.#BeginReadOperation(resource.GetPath(), options);
2192
+ const cachePolicy = NormalizeCachePolicy(read.options.cacheFormat, "cacheFormat");
2193
+ const key = GetFormatOperationKey(read.context, read.options);
2194
+ const cacheableOptions = key !== null && cachePolicy !== false;
2195
+ const descriptorOperations = cacheableOptions ? GetFormatDescriptorOperations(this.formatOperations, read.context.source, descriptor, true) : null;
2196
+ const bypassExisting = read.options.reload === true || !cacheableOptions;
2197
+ const existing = bypassExisting ? null : descriptorOperations.get(key);
2198
+ if (existing) {
2199
+ if (cachePolicy === true) existing.retain = true;
2200
+ return existing.promise;
2201
+ }
2202
+
2203
+ /** @type {CjsResourceReadOperationRecord} */
2204
+ const record = {
2205
+ promise: Promise.resolve().then(() => this.ReadFormat(descriptor, bytes, read.options)),
2206
+ path: read.context.path,
2207
+ revisionKey: read.context.revisionKey,
2208
+ retain: cachePolicy === true
2209
+ };
2210
+ if (cacheableOptions) descriptorOperations.set(key, record);
2211
+ record.promise.then(() => {
2212
+ if (cacheableOptions && !record.retain && descriptorOperations.get(key) === record) {
2213
+ descriptorOperations.delete(key);
2214
+ }
624
2215
  }, () => {
625
- if (operations.get(key) === operation) operations.delete(key);
2216
+ if (cacheableOptions && descriptorOperations.get(key) === record) {
2217
+ descriptorOperations.delete(key);
2218
+ }
626
2219
  });
627
- return operation;
2220
+ return record.promise;
628
2221
  }
629
2222
  }
630
- function StableSerialize(value, seen = new WeakSet()) {
2223
+
2224
+ /**
2225
+ * Copy supported registered format defaults into a deeply frozen snapshot.
2226
+ * Plain objects and arrays may be cyclic. Functions and primitives are stable
2227
+ * leaves; values with hidden mutable state are rejected because freezing their
2228
+ * wrapper would not make the represented behavior immutable.
2229
+ *
2230
+ * @param {object} defaults Registered format defaults.
2231
+ * @param {WeakMap<object, object>} [seen=new WeakMap()] Source-to-snapshot cycle map.
2232
+ * @returns {Readonly<object>} Deeply frozen plain defaults snapshot.
2233
+ * @throws {TypeError} If defaults contain unsupported objects, accessors, symbols, or byte buffers.
2234
+ */
2235
+ function SnapshotFormatDefaults(defaults, seen = new WeakMap()) {
2236
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
2237
+ throw new TypeError("CjsResMan format defaults must be a plain object.");
2238
+ }
2239
+ return SnapshotFormatDefaultValue(defaults, seen, "defaults");
2240
+ }
2241
+
2242
+ /**
2243
+ * Snapshot one value in a registered format-default tree.
2244
+ *
2245
+ * @param {*} value Source value.
2246
+ * @param {WeakMap<object, object>} seen Source-to-snapshot cycle map.
2247
+ * @param {string} path Diagnostic property path.
2248
+ * @returns {*} Frozen snapshot value or stable primitive/function leaf.
2249
+ * @throws {TypeError} If the value cannot be made structurally immutable.
2250
+ */
2251
+ function SnapshotFormatDefaultValue(value, seen, path) {
2252
+ if (value === null || value === undefined) return value;
2253
+ const type = typeof value;
2254
+ if (["string", "number", "boolean", "bigint"].includes(type)) return value;
2255
+ if (type === "function") return value;
2256
+ if (type !== "object" || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
2257
+ throw new TypeError(`CjsResMan format ${path} cannot be snapshotted immutably.`);
2258
+ }
2259
+ const prior = seen.get(value);
2260
+ if (prior) return prior;
2261
+ if (Array.isArray(value)) {
2262
+ const snapshot = [];
2263
+ seen.set(value, snapshot);
2264
+ for (let index = 0; index < value.length; index += 1) {
2265
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
2266
+ throw new TypeError(`CjsResMan format ${path} must not contain sparse arrays.`);
2267
+ }
2268
+ snapshot.push(SnapshotFormatDefaultValue(value[index], seen, `${path}[${index}]`));
2269
+ }
2270
+ return Object.freeze(snapshot);
2271
+ }
2272
+ const prototype = Object.getPrototypeOf(value);
2273
+ if (prototype !== Object.prototype && prototype !== null) {
2274
+ throw new TypeError(`CjsResMan format ${path} must contain only plain objects and arrays.`);
2275
+ }
2276
+ if (Object.getOwnPropertySymbols(value).length > 0) {
2277
+ throw new TypeError(`CjsResMan format ${path} must not contain symbol keys.`);
2278
+ }
2279
+ const snapshot = prototype === null ? Object.create(null) : {};
2280
+ seen.set(value, snapshot);
2281
+ for (const key of Object.getOwnPropertyNames(value)) {
2282
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
2283
+ if (!descriptor.enumerable || !("value" in descriptor)) {
2284
+ throw new TypeError(`CjsResMan format ${path}.${key} must be an enumerable data property.`);
2285
+ }
2286
+ snapshot[key] = SnapshotFormatDefaultValue(descriptor.value, seen, `${path}.${key}`);
2287
+ }
2288
+ return Object.freeze(snapshot);
2289
+ }
2290
+
2291
+ /**
2292
+ * Normalize an opaque caller/source revision without interpreting content.
2293
+ * String and number tokens remain type-distinct; finite numeric `-0` and `0`
2294
+ * intentionally identify the same revision.
2295
+ *
2296
+ * @param {string|number|undefined} value Caller-provided source content token.
2297
+ * @returns {string} Type-stable internal revision key.
2298
+ * @throws {TypeError} If the token is neither a string nor finite number.
2299
+ */
2300
+ function NormalizeSourceRevision(value) {
631
2301
  if (value === undefined) return "undefined";
632
- if (value === null || typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
633
- if (typeof value === "string") return JSON.stringify(value);
634
- if (typeof value === "function") return `[Function:${value.name || "anonymous"}]`;
635
- if (ArrayBuffer.isView(value)) {
636
- return `[${value.constructor.name}:${value.byteOffset}:${value.byteLength}]`;
2302
+ if (typeof value === "string") return `string:${value}`;
2303
+ if (typeof value === "number" && Number.isFinite(value)) {
2304
+ return `number:${Object.is(value, -0) ? 0 : value}`;
637
2305
  }
638
- if (value instanceof ArrayBuffer) return `[ArrayBuffer:${value.byteLength}]`;
639
- if (seen.has(value)) return "[Circular]";
2306
+ throw new TypeError("CjsResMan sourceRevision must be a string or finite number.");
2307
+ }
2308
+
2309
+ /**
2310
+ * Validate tri-state source/format cache policy.
2311
+ *
2312
+ * @param {boolean|undefined} value Cache policy value.
2313
+ * @param {string} name Option name used in failures.
2314
+ * @returns {boolean|undefined} The validated policy.
2315
+ * @throws {TypeError} If the value is not boolean or omitted.
2316
+ */
2317
+ function NormalizeCachePolicy(value, name) {
2318
+ if (value !== undefined && typeof value !== "boolean") {
2319
+ throw new TypeError(`CjsResMan ${name} must be a boolean when supplied.`);
2320
+ }
2321
+ return value;
2322
+ }
2323
+
2324
+ /**
2325
+ * Get the operation map owned by one source object.
2326
+ *
2327
+ * @param {WeakMap<object|Function, Map<string, CjsResourceReadOperationRecord>>} ledger Source-keyed ledger.
2328
+ * @param {object|Function} owner Source owner.
2329
+ * @param {boolean} create Whether a missing map should be allocated.
2330
+ * @returns {Map<string, CjsResourceReadOperationRecord>|null} Owner map or `null`.
2331
+ */
2332
+ function GetOwnerOperations(ledger, owner, create) {
2333
+ let operations = ledger.get(owner);
2334
+ if (!operations && create) {
2335
+ operations = new Map();
2336
+ ledger.set(owner, operations);
2337
+ }
2338
+ return operations || null;
2339
+ }
2340
+
2341
+ /**
2342
+ * Get the source/path/revision operation key for one normalized read context.
2343
+ *
2344
+ * @param {Readonly<CjsResourceReadContext>} context Normalized provenance.
2345
+ * @returns {string} Internal read-operation key.
2346
+ */
2347
+ function GetReadOperationKey(context) {
2348
+ return `${context.path}\u0000${context.revisionKey}`;
2349
+ }
2350
+
2351
+ /**
2352
+ * Get the descriptor-specific format operation map for a selected source.
2353
+ * Frozen descriptor identity isolates changed registration defaults.
2354
+ *
2355
+ * @param {WeakMap<object|Function, Map<object, Map<string, CjsResourceReadOperationRecord>>>} ledger Format ledger.
2356
+ * @param {object|Function} source Selected source owner.
2357
+ * @param {object} descriptor Frozen registered descriptor.
2358
+ * @param {boolean} create Whether missing maps should be allocated.
2359
+ * @returns {Map<string, CjsResourceReadOperationRecord>|null} Descriptor map or `null`.
2360
+ */
2361
+ function GetFormatDescriptorOperations(ledger, source, descriptor, create) {
2362
+ let descriptors = ledger.get(source);
2363
+ if (!descriptors && create) {
2364
+ descriptors = new Map();
2365
+ ledger.set(source, descriptors);
2366
+ }
2367
+ if (!descriptors) return null;
2368
+ let operations = descriptors.get(descriptor);
2369
+ if (!operations && create) {
2370
+ operations = new Map();
2371
+ descriptors.set(descriptor, operations);
2372
+ }
2373
+ return operations || null;
2374
+ }
2375
+
2376
+ /**
2377
+ * Build a descriptor-local format cache key from source provenance and the
2378
+ * output options that materially affect the format reader result.
2379
+ * Requests containing non-canonical mutable option objects bypass format-cache
2380
+ * sharing instead of risking a false key match.
2381
+ *
2382
+ * @param {Readonly<CjsResourceReadContext>} context Normalized provenance.
2383
+ * @param {object} options Format request options.
2384
+ * @returns {string|null} Internal format-operation key, or `null` to bypass caching.
2385
+ */
2386
+ function GetFormatOperationKey(context, options) {
2387
+ const material = [options.emit, options.mediaType, options.classes, options.formatOptions];
2388
+ if (!material.every(value => IsCanonicalFormatCacheValue(value))) return null;
2389
+ return `${GetReadOperationKey(context)}\u0000${material.map(value => SerializeFormatCacheValue(value)).join("\u0001")}`;
2390
+ }
2391
+
2392
+ /**
2393
+ * Return true when a format cache option can be represented without guessing
2394
+ * hidden mutable state. Plain objects/arrays, functions by identity, and
2395
+ * byte-addressable buffers/views are supported. Other class instances bypass
2396
+ * format-cache sharing.
2397
+ *
2398
+ * @param {*} value Candidate material format option.
2399
+ * @param {WeakSet<object|Function>} [seen=new WeakSet()] Cycle guard.
2400
+ * @returns {boolean} Whether deterministic cache serialization is supported.
2401
+ */
2402
+ function IsCanonicalFormatCacheValue(value, seen = new WeakSet()) {
2403
+ if (value === null || value === undefined) return true;
2404
+ if (["string", "number", "boolean", "bigint"].includes(typeof value)) return true;
2405
+ if (typeof value === "function") return true;
2406
+ if (typeof value !== "object") return false;
2407
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return true;
2408
+ if (seen.has(value)) return true;
640
2409
  seen.add(value);
641
- if (Array.isArray(value)) return `[${value.map(entry => StableSerialize(entry, seen)).join(",")}]`;
642
- return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${StableSerialize(value[key], seen)}`).join(",")}}`;
2410
+ if (Array.isArray(value)) {
2411
+ return value.every(entry => IsCanonicalFormatCacheValue(entry, seen));
2412
+ }
2413
+ const prototype = Object.getPrototypeOf(value);
2414
+ if (prototype !== Object.prototype && prototype !== null) return false;
2415
+ if (Object.getOwnPropertySymbols(value).length > 0) return false;
2416
+ return Object.getOwnPropertyNames(value).every(key => {
2417
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
2418
+ return descriptor.enumerable && "value" in descriptor && IsCanonicalFormatCacheValue(descriptor.value, seen);
2419
+ });
643
2420
  }
644
- function GetIdentityOptions(options = {}) {
645
- const identity = {};
646
- for (const key of ["requirement", "payload", "emit", "mediaType", "format", "classes", "formatOptions", "pipeline", "preparePipeline", "prepareStages"]) {
647
- if (options[key] !== undefined) identity[key] = options[key];
2421
+
2422
+ /**
2423
+ * Serialize one supported material format option with process-local identity
2424
+ * for objects and functions. Identity prevents same-named constructors and
2425
+ * separate option graphs from colliding; visible plain-object/array/buffer
2426
+ * contents ensure mutation changes the next operation key.
2427
+ *
2428
+ * @param {*} value Canonically supported format option.
2429
+ * @param {WeakSet<object|Function>} [seen=new WeakSet()] Cycle guard.
2430
+ * @returns {string} Deterministic cache-local representation.
2431
+ */
2432
+ function SerializeFormatCacheValue(value, seen = new WeakSet()) {
2433
+ if (value === undefined) return "undefined";
2434
+ if (value === null) return "null";
2435
+ if (typeof value === "string") return `string:${JSON.stringify(value)}`;
2436
+ if (typeof value === "number") {
2437
+ if (Number.isNaN(value)) return "number:NaN";
2438
+ if (Object.is(value, -0)) return "number:-0";
2439
+ return `number:${String(value)}`;
2440
+ }
2441
+ if (typeof value === "boolean") return `boolean:${value}`;
2442
+ if (typeof value === "bigint") return `bigint:${value}`;
2443
+ if (typeof value === "function") return `function:${GetLocalValueIdentity(value)}`;
2444
+ const identity = GetLocalValueIdentity(value);
2445
+ if (seen.has(value)) return `reference:${identity}`;
2446
+ seen.add(value);
2447
+ if (ArrayBuffer.isView(value)) {
2448
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
2449
+ return `view:${identity}:${value.constructor.name}:${[...bytes].join(",")}`;
2450
+ }
2451
+ if (value instanceof ArrayBuffer) {
2452
+ return `buffer:${identity}:${[...new Uint8Array(value)].join(",")}`;
2453
+ }
2454
+ if (Array.isArray(value)) {
2455
+ return `array:${identity}:[${value.map(entry => SerializeFormatCacheValue(entry, seen)).join(",")}]`;
2456
+ }
2457
+ return `object:${identity}:{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${SerializeFormatCacheValue(value[key], seen)}`).join(",")}}`;
2458
+ }
2459
+
2460
+ /**
2461
+ * Return a process-local identity for a parsed-format cache object or function.
2462
+ *
2463
+ * @param {object|Function} value Candidate option identity owner.
2464
+ * @returns {number} Stable identity for the life of the value.
2465
+ */
2466
+ function GetLocalValueIdentity(value) {
2467
+ let identity = LOCAL_VALUE_IDENTITIES.get(value);
2468
+ if (!identity) {
2469
+ identity = nextLocalValueIdentity++;
2470
+ LOCAL_VALUE_IDENTITIES.set(value, identity);
648
2471
  }
649
2472
  return identity;
650
2473
  }
651
- function NormalizeRequirement(value) {
652
- return value === null || value === undefined ? "" : String(value).trim().toLowerCase();
2474
+
2475
+ /**
2476
+ * Remove operation records matching a normalized path and optional revision.
2477
+ * Detached promises continue for their existing consumers.
2478
+ *
2479
+ * @param {Map<string, CjsResourceReadOperationRecord>|null} operations Candidate operation map.
2480
+ * @param {string} path Normalized source path.
2481
+ * @param {string|null} revisionKey Exact revision or `null` for all revisions.
2482
+ * @returns {number} Number of detached records.
2483
+ */
2484
+ function RemoveOperationRecords(operations, path, revisionKey) {
2485
+ if (!operations) return 0;
2486
+ let removed = 0;
2487
+ for (const [key, record] of operations) {
2488
+ if (record.path !== path) continue;
2489
+ if (revisionKey !== null && record.revisionKey !== revisionKey) continue;
2490
+ if (operations.delete(key)) removed += 1;
2491
+ }
2492
+ return removed;
653
2493
  }
654
- function NormalizePipelineName(value) {
655
- return value === null || value === undefined ? "" : String(value).trim().toLowerCase();
2494
+
2495
+ /**
2496
+ * Copy the compact requested-output fields needed for explicit payload
2497
+ * reconstruction. This request is retained with the handle but never enters
2498
+ * MotherLode identity beyond its `variant`/`emit` output tag.
2499
+ *
2500
+ * @param {object} [options={}] Original resource request.
2501
+ * @returns {object} Shallow reconstruction request without cache/reload policy.
2502
+ */
2503
+ function GetResourceRequestOptions(options = {}) {
2504
+ const request = {};
2505
+ for (const key of RESOURCE_REQUEST_OPTION_KEYS) {
2506
+ if (options[key] !== undefined) request[key] = options[key];
2507
+ }
2508
+ return request;
656
2509
  }
657
- function NormalizePrepareStages(value) {
658
- if (value === null || value === undefined) return [];
659
- const entries = Array.isArray(value) ? value : [value];
660
- return entries.map((entry, index) => {
661
- const prepare = typeof entry === "function" ? entry : entry?.prepare || entry?.run || entry?.handler;
662
- if (typeof prepare !== "function") {
663
- throw new TypeError("CjsResMan prepare stages require a function or prepare/run/handler method.");
2510
+
2511
+ /**
2512
+ * Capture the compact output request plus effective source provenance needed
2513
+ * to reconstruct a released resource payload. Cache policy and one-shot reload
2514
+ * are intentionally not retained. Retaining the effective source prevents an
2515
+ * old revision token from being applied to a later manager default source.
2516
+ *
2517
+ * @param {object} [options={}] Requested output and source provenance options.
2518
+ * @param {object|Function|null} [effectiveSource=null] Source selected when the resource was created.
2519
+ * @returns {object} Detached loader options safe to retain with the resource.
2520
+ */
2521
+ function GetResourceLoaderOptions(options = {}, effectiveSource = null) {
2522
+ const loaderOptions = GetResourceRequestOptions(options);
2523
+ if (effectiveSource) loaderOptions.source = effectiveSource;
2524
+ for (const key of ["sourceRevision", "ext"]) {
2525
+ if (Object.prototype.hasOwnProperty.call(options, key)) {
2526
+ loaderOptions[key] = options[key];
664
2527
  }
665
- const name = typeof entry === "function" ? entry.name || `stage${index + 1}` : entry.name || prepare.name || `stage${index + 1}`;
666
- return Object.freeze({
667
- name: String(name),
668
- prepare
669
- });
670
- });
2528
+ }
2529
+ return loaderOptions;
671
2530
  }
672
- function NormalizePreparePipelineEntries(value) {
673
- if (value === null || value === undefined) return [];
674
- if (value instanceof Map) return [...value.entries()];
675
- if (typeof value !== "object" || Array.isArray(value)) {
676
- throw new TypeError("CjsResMan preparePipelines must be an object or Map.");
2531
+
2532
+ /**
2533
+ * Merge per-operation controls with a resource's compact reconstruction
2534
+ * request. Once the handle has a promised-output field, all output selectors
2535
+ * remain pinned to the retained request so `Ready()` cannot publish a
2536
+ * different result under the existing MotherLode identity. Retained source
2537
+ * provenance is pinned as well; cache/reload controls remain explicit per-call
2538
+ * overrides.
2539
+ *
2540
+ * @param {object} base Retained reconstruction request.
2541
+ * @param {object} [overrides={}] Per-call operation controls.
2542
+ * @returns {object} Merged request with stable promised-output fields.
2543
+ */
2544
+ function MergeResourceLoaderOptions(base, overrides = {}) {
2545
+ const result = {
2546
+ ...base,
2547
+ ...overrides
2548
+ };
2549
+ const pinsOutput = RESOURCE_OUTPUT_OPTION_KEYS.some(key => Object.prototype.hasOwnProperty.call(base, key) && base[key] !== undefined);
2550
+ if (pinsOutput) {
2551
+ for (const key of RESOURCE_OUTPUT_OPTION_KEYS) {
2552
+ if (Object.prototype.hasOwnProperty.call(base, key) && base[key] !== undefined) {
2553
+ result[key] = base[key];
2554
+ } else {
2555
+ delete result[key];
2556
+ }
2557
+ }
2558
+ }
2559
+ for (const key of RESOURCE_PROVENANCE_OPTION_KEYS) {
2560
+ if (Object.prototype.hasOwnProperty.call(base, key)) result[key] = base[key];
2561
+ }
2562
+ return result;
2563
+ }
2564
+
2565
+ /**
2566
+ * Filter current format descriptors using request material available before
2567
+ * source bytes are read. Byte-dependent support probes are deferred.
2568
+ *
2569
+ * @param {readonly object[]} descriptors Registered descriptors for one extension.
2570
+ * @param {object} options Format, output, and media selection.
2571
+ * @returns {object[]} Matching descriptors in registration order.
2572
+ */
2573
+ function FilterFormatDescriptors(descriptors, options) {
2574
+ let candidates = [...descriptors];
2575
+ if (options.format) {
2576
+ candidates = candidates.filter(({
2577
+ Format
2578
+ }) => Format === options.format || Format.id === options.format || Format.name === options.format);
2579
+ }
2580
+ if (options.emit !== undefined) {
2581
+ candidates = candidates.filter(({
2582
+ Format
2583
+ }) => FindDeclaredOutput(GetFormatOutputs(Format), options.emit) !== null);
2584
+ }
2585
+ if (options.mediaType) {
2586
+ candidates = candidates.filter(({
2587
+ Format
2588
+ }) => (Format.mediaTypes || []).includes(options.mediaType));
2589
+ }
2590
+ return candidates;
2591
+ }
2592
+
2593
+ /**
2594
+ * Return one format facade's normal and diagnostic output declarations.
2595
+ *
2596
+ * @param {Function} Format Registered format facade.
2597
+ * @returns {Array<*>} Declared output values in selection order.
2598
+ */
2599
+ function GetFormatOutputs(Format) {
2600
+ return [...(Format.outputTypes || []), ...(Format.debugOutputTypes || [])];
2601
+ }
2602
+
2603
+ /**
2604
+ * Resolve a case-insensitive output selector to its canonical declared
2605
+ * spelling. MotherLode tags stay lowercase while readers receive declarations
2606
+ * such as `cmfJson` or `gr2Json` exactly as authored.
2607
+ *
2608
+ * @param {readonly *[]} outputs Declared output values.
2609
+ * @param {*} requested Requested output selector.
2610
+ * @returns {string|null} Canonical declaration, or `null` when unsupported.
2611
+ */
2612
+ function FindDeclaredOutput(outputs, requested) {
2613
+ const normalized = NormalizeResourceVariant(requested);
2614
+ for (const output of outputs) {
2615
+ if (typeof output !== "string") continue;
2616
+ if (NormalizeResourceVariant(output) === normalized) return output;
2617
+ }
2618
+ return null;
2619
+ }
2620
+
2621
+ /**
2622
+ * Create the stable failure used when a registered input format cannot emit a
2623
+ * requested output tag.
2624
+ *
2625
+ * @param {string} ext Normalized source extension.
2626
+ * @param {*} emit Requested output tag.
2627
+ * @param {readonly object[]} descriptors Registered input-format descriptors.
2628
+ * @returns {Error} Contextual unsupported-output error.
2629
+ */
2630
+ function CreateFormatOutputMissingError(ext, emit, descriptors) {
2631
+ const error = new Error(`No format registered for .${ext} emits ${JSON.stringify(emit)}.`);
2632
+ error.code = "CJS_RESOURCE_FORMAT_OUTPUT_MISSING";
2633
+ error.ext = ext;
2634
+ error.emit = emit;
2635
+ error.formats = descriptors.map(({
2636
+ Format
2637
+ }) => Format.name);
2638
+ return error;
2639
+ }
2640
+
2641
+ /**
2642
+ * Create the stable failure used when a direct object loader cannot satisfy a
2643
+ * forced output request.
2644
+ *
2645
+ * @param {string} ext Normalized source extension.
2646
+ * @param {*} emit Requested output tag.
2647
+ * @returns {Error} Contextual unsupported-output error.
2648
+ */
2649
+ function CreateObjectLoaderOutputMissingError(ext, emit) {
2650
+ const error = new Error(`Direct loader for .${ext} exposes only its unforced default; it does not emit ${JSON.stringify(emit)}.`);
2651
+ error.code = "CJS_RESOURCE_FORMAT_OUTPUT_MISSING";
2652
+ error.ext = ext;
2653
+ error.emit = emit;
2654
+ error.formats = [];
2655
+ return error;
2656
+ }
2657
+
2658
+ /**
2659
+ * Resolve one descriptor from the current candidate set. When bytes are
2660
+ * present, support probes may disambiguate candidates.
2661
+ *
2662
+ * @param {readonly object[]} descriptors Candidate descriptors from current registration.
2663
+ * @param {string} ext Normalized resource extension.
2664
+ * @param {object} options Effective format options and optional source bytes.
2665
+ * @returns {object} Selected descriptor.
2666
+ * @throws {Error} If the candidate set is missing or remains ambiguous.
2667
+ */
2668
+ function ResolveFormatDescriptorCandidates(descriptors, ext, options) {
2669
+ let candidates = [...descriptors];
2670
+ if (candidates.length > 1 && options.bytes !== undefined) {
2671
+ const supported = candidates.filter(descriptor => {
2672
+ const {
2673
+ Format,
2674
+ defaults
2675
+ } = descriptor;
2676
+ const isSupported = Format.isSupported;
2677
+ if (typeof isSupported !== "function") return false;
2678
+ const report = isSupported.call(Format, options.bytes, {
2679
+ ...defaults,
2680
+ ...(options.formatOptions || {})
2681
+ });
2682
+ return report && report.supported !== false;
2683
+ });
2684
+ if (supported.length === 1) candidates = supported;
2685
+ }
2686
+ if (candidates.length === 0) {
2687
+ const error = new Error(`No format registered for .${ext}`);
2688
+ error.code = "CJS_RESOURCE_FORMAT_MISSING";
2689
+ error.ext = ext;
2690
+ throw error;
2691
+ }
2692
+ if (candidates.length > 1) {
2693
+ const error = new Error(`Ambiguous formats registered for .${ext}`);
2694
+ error.code = "CJS_RESOURCE_FORMAT_AMBIGUOUS";
2695
+ error.ext = ext;
2696
+ error.formats = candidates.map(({
2697
+ Format
2698
+ }) => Format.name);
2699
+ throw error;
677
2700
  }
678
- return Object.entries(value);
2701
+ return candidates[0];
2702
+ }
2703
+ function NormalizeRequirement(value) {
2704
+ return value === null || value === undefined ? "" : String(value).trim().toLowerCase();
2705
+ }
2706
+
2707
+ /**
2708
+ * Normalize one human-readable promised output tag.
2709
+ *
2710
+ * @param {*} value Candidate `variant`, `emit`, requirement, or payload tag.
2711
+ * @returns {string} Trimmed lowercase tag, or an empty string.
2712
+ * @throws {TypeError} If a non-empty tag is not a string or contains the internal key delimiter.
2713
+ */
2714
+ function NormalizeResourceVariant(value) {
2715
+ if (value === null || value === undefined || value === "") return "";
2716
+ if (typeof value !== "string") {
2717
+ throw new TypeError("CjsResMan resource variant must be a string.");
2718
+ }
2719
+ const normalized = value.trim().toLowerCase();
2720
+ if (normalized.includes("\u0000")) {
2721
+ throw new TypeError("CjsResMan resource variant may not contain a null character.");
2722
+ }
2723
+ return normalized;
679
2724
  }
680
2725
  function CreatePrepareContext(resMan, resource, bytes, options, stage) {
681
2726
  return Object.freeze({
@@ -688,6 +2733,18 @@ function CreatePrepareContext(resMan, resource, bytes, options, stage) {
688
2733
  resMan
689
2734
  });
690
2735
  }
2736
+
2737
+ /**
2738
+ * Recover the public object result represented by a resident resource payload.
2739
+ * The publication contract returns semantic subclasses as their resource
2740
+ * handle and base resources as their plain payload.
2741
+ *
2742
+ * @param {CjsResource} resource Resource with an attached CPU payload.
2743
+ * @returns {*} Resident public object outcome.
2744
+ */
2745
+ function GetPublishedResourceObject(resource) {
2746
+ return resource.constructor !== _CjsResource ? resource : resource.GetPayload();
2747
+ }
691
2748
  function AssertPositiveInteger(value, name) {
692
2749
  if (!Number.isInteger(value) || value < 1) {
693
2750
  throw new TypeError(`CjsResMan ${name} must be a positive integer.`);
@@ -703,6 +2760,214 @@ function AssertNonNegativeNumber(value, name) {
703
2760
  throw new TypeError(`CjsResMan ${name} must be a non-negative finite number.`);
704
2761
  }
705
2762
  }
2763
+
2764
+ /**
2765
+ * Normalize and freeze an opt-in time-based automatic purge policy.
2766
+ *
2767
+ * @param {CjsResManAutoPurgePolicy|false|null} policy Caller policy or disable marker.
2768
+ * @returns {Readonly<CjsResManAutoPurgePolicy>|null} Frozen normalized policy, or `null` when disabled.
2769
+ * @throws {TypeError} If fields, limits, cleanup controls, or the cadence clock are invalid.
2770
+ */
2771
+ function NormalizeAutoPurgePolicy(policy) {
2772
+ if (policy === null || policy === undefined || policy === false) return null;
2773
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
2774
+ throw new TypeError("CjsResMan autoPurgePolicy must be an object, false, or null.");
2775
+ }
2776
+ const allowed = new Set(["intervalMilliseconds", "maxIdleMilliseconds", "payloadMaxIdleMilliseconds", "destroyAdapters", "releasePayload", "cleanup", "now"]);
2777
+ const unsupported = Object.keys(policy).filter(key => !allowed.has(key));
2778
+ if (unsupported.length) {
2779
+ throw new TypeError(`CjsResMan autoPurgePolicy does not support: ${unsupported.join(", ")}.`);
2780
+ }
2781
+ const intervalMilliseconds = policy.intervalMilliseconds ?? 1000;
2782
+ AssertNonNegativeNumber(intervalMilliseconds, "autoPurgePolicy.intervalMilliseconds");
2783
+ for (const name of ["maxIdleMilliseconds", "payloadMaxIdleMilliseconds"]) {
2784
+ if (policy[name] !== undefined) {
2785
+ AssertNonNegativeNumber(policy[name], `autoPurgePolicy.${name}`);
2786
+ }
2787
+ }
2788
+ if (policy.maxIdleMilliseconds === undefined && policy.payloadMaxIdleMilliseconds === undefined) {
2789
+ throw new TypeError("CjsResMan autoPurgePolicy requires an identity or payload inactivity limit.");
2790
+ }
2791
+ for (const name of ["destroyAdapters", "releasePayload"]) {
2792
+ if (policy[name] !== undefined && typeof policy[name] !== "boolean") {
2793
+ throw new TypeError(`CjsResMan autoPurgePolicy.${name} must be a boolean.`);
2794
+ }
2795
+ }
2796
+ if (policy.cleanup !== undefined && policy.cleanup !== false && typeof policy.cleanup !== "function") {
2797
+ throw new TypeError("CjsResMan autoPurgePolicy.cleanup must be a function or false.");
2798
+ }
2799
+ if (policy.now !== undefined && typeof policy.now !== "function") {
2800
+ throw new TypeError("CjsResMan autoPurgePolicy.now must be a function.");
2801
+ }
2802
+ return Object.freeze({
2803
+ intervalMilliseconds,
2804
+ maxIdleMilliseconds: policy.maxIdleMilliseconds,
2805
+ payloadMaxIdleMilliseconds: policy.payloadMaxIdleMilliseconds,
2806
+ destroyAdapters: policy.destroyAdapters ?? true,
2807
+ releasePayload: policy.releasePayload ?? true,
2808
+ ...(policy.cleanup === undefined ? {} : {
2809
+ cleanup: policy.cleanup
2810
+ }),
2811
+ now: policy.now || DefaultAutoPurgeNow
2812
+ });
2813
+ }
2814
+
2815
+ /**
2816
+ * Validate one automatic-purge pump request without advancing cadence.
2817
+ *
2818
+ * @param {CjsResManAutoPurgePumpOptions} options Per-call pump options.
2819
+ * @returns {CjsResManAutoPurgePumpOptions} Validated options.
2820
+ * @throws {TypeError} If options or an explicit timestamp are invalid.
2821
+ */
2822
+ function NormalizeAutoPurgePumpOptions(options) {
2823
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
2824
+ throw new TypeError("CjsResMan automatic purge options must be an object.");
2825
+ }
2826
+ const unsupported = Object.keys(options).filter(key => key !== "time");
2827
+ if (unsupported.length) {
2828
+ throw new TypeError(`CjsResMan automatic purge options do not support: ${unsupported.join(", ")}.`);
2829
+ }
2830
+ if (options.time !== undefined) {
2831
+ AssertNonNegativeNumber(options.time, "automatic purge time");
2832
+ }
2833
+ return options;
2834
+ }
2835
+
2836
+ /**
2837
+ * Create the stable failure used when synchronous MotherLode replacement
2838
+ * would detach ownership beneath active asynchronous resource mutation.
2839
+ *
2840
+ * @param {number} activeOperations Number of queued and direct mutations.
2841
+ * @returns {Error} Contextual active-operation error.
2842
+ */
2843
+ function ActiveResourceOperationsError(activeOperations) {
2844
+ const error = new Error(`CjsResMan cannot replace MotherLode while ${activeOperations} resource operation(s) are active.`);
2845
+ error.code = "CJS_RESMAN_ACTIVE_RESOURCE_OPERATIONS";
2846
+ error.activeOperations = activeOperations;
2847
+ return error;
2848
+ }
2849
+
2850
+ /**
2851
+ * Create the stable failure used when a manager-only load API receives a
2852
+ * resource that was never bound to this manager.
2853
+ *
2854
+ * @param {*} resource Candidate resource.
2855
+ * @param {string} phase Operation phase.
2856
+ * @returns {Error} Contextual ownership error.
2857
+ */
2858
+ function ResourceNotOwnedError(resource, phase) {
2859
+ const path = GetResourceDiagnosticPath(resource);
2860
+ const error = new Error(`CjsResMan does not own a canonical resource at ${path}.`);
2861
+ error.code = "CJS_RESMAN_RESOURCE_NOT_OWNED";
2862
+ error.resource = resource;
2863
+ error.path = path;
2864
+ error.phase = phase;
2865
+ return error;
2866
+ }
2867
+
2868
+ /**
2869
+ * Create the stable failure used when otherwise-successful obsolete work
2870
+ * reaches a canonical mutation boundary.
2871
+ *
2872
+ * @param {CjsResourceOwnership} ownership Captured stale authority.
2873
+ * @param {string} phase Operation phase that detected staleness.
2874
+ * @returns {Error} Contextual stale-operation error.
2875
+ */
2876
+ function StaleResourceOperationError(ownership, phase) {
2877
+ const path = GetResourceDiagnosticPath(ownership?.resource);
2878
+ const error = new Error(`CjsResMan resource operation became stale during ${phase}: ${path}.`);
2879
+ error.code = "CJS_RESMAN_STALE_RESOURCE_OPERATION";
2880
+ error.resource = ownership?.resource || null;
2881
+ error.path = path;
2882
+ error.key = ownership?.key || null;
2883
+ error.generation = ownership?.generation || 0;
2884
+ error.phase = phase;
2885
+ return error;
2886
+ }
2887
+
2888
+ /**
2889
+ * Reject a resource constructor that returns the protected canonical singleton
2890
+ * when a distinct reload candidate is required.
2891
+ *
2892
+ * @param {string} path Requested resource path.
2893
+ * @param {object|Function} resource Aliased canonical resource.
2894
+ * @returns {Error} Stable candidate-alias error.
2895
+ */
2896
+ function ReloadCandidateAliasError(path, resource) {
2897
+ const error = new Error(`CjsResMan reload candidate aliases the canonical resource: ${path}.`);
2898
+ error.code = "CJS_RESMAN_RELOAD_CANDIDATE_ALIAS";
2899
+ error.path = path;
2900
+ error.resource = resource;
2901
+ return error;
2902
+ }
2903
+
2904
+ /**
2905
+ * Create the stable failure used when a candidate loader no longer has a
2906
+ * staged authority record.
2907
+ *
2908
+ * @param {object|Function} resource Candidate resource.
2909
+ * @returns {Error} Stable unavailable-candidate error.
2910
+ */
2911
+ function ReloadCandidateUnavailableError(resource) {
2912
+ const path = GetResourceDiagnosticPath(resource);
2913
+ const error = new Error(`CjsResMan reload candidate is no longer available: ${path}.`);
2914
+ error.code = "CJS_RESMAN_RELOAD_CANDIDATE_UNAVAILABLE";
2915
+ error.path = path;
2916
+ error.resource = resource;
2917
+ return error;
2918
+ }
2919
+
2920
+ /**
2921
+ * Create the stable failure used when a deleted, cleared, replaced, or
2922
+ * superseded reload candidate reaches a mutation or commit boundary.
2923
+ *
2924
+ * @param {CjsResourceReloadCandidate} candidate Captured candidate authority.
2925
+ * @param {string} phase Operation phase that detected staleness.
2926
+ * @returns {Error} Contextual stale-candidate error.
2927
+ */
2928
+ function StaleReloadCandidateError(candidate, phase) {
2929
+ const path = GetResourceDiagnosticPath(candidate?.resource);
2930
+ const error = new Error(`CjsResMan reload candidate became stale during ${phase}: ${path}.`);
2931
+ error.code = "CJS_RESMAN_STALE_RELOAD_CANDIDATE";
2932
+ error.resource = candidate?.resource || null;
2933
+ error.expected = candidate?.expected || null;
2934
+ error.path = path;
2935
+ error.key = candidate?.key || null;
2936
+ error.generation = candidate?.generation || 0;
2937
+ error.phase = phase;
2938
+ return error;
2939
+ }
2940
+
2941
+ /**
2942
+ * Read a resource path for diagnostics without allowing an unusual custom
2943
+ * resource getter to hide the primary ownership error.
2944
+ *
2945
+ * @param {*} resource Candidate resource.
2946
+ * @returns {string} Best-effort resource path label.
2947
+ */
2948
+ function GetResourceDiagnosticPath(resource) {
2949
+ try {
2950
+ return String(resource?.GetPath?.() || resource?.path || "<unknown>");
2951
+ } catch {
2952
+ return "<unknown>";
2953
+ }
2954
+ }
2955
+
2956
+ /**
2957
+ * Perform no action for lifecycle-compatible optional operations.
2958
+ *
2959
+ * @returns {void}
2960
+ */
2961
+ function Noop() {}
2962
+
2963
+ /**
2964
+ * Return the wall-clock timestamp used by default automatic purge cadence.
2965
+ *
2966
+ * @returns {number} Milliseconds since the Unix epoch.
2967
+ */
2968
+ function DefaultAutoPurgeNow() {
2969
+ return Date.now();
2970
+ }
706
2971
  function DefaultQueueScheduler(callback) {
707
2972
  return setTimeout(callback, 0);
708
2973
  }