pelican-engine 0.0.0-stage → 0.3.1

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 (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,648 @@
1
+ export class ResourceError extends Error {
2
+ diagnostic;
3
+ code;
4
+ constructor(diagnostic) {
5
+ super(diagnostic.message);
6
+ this.diagnostic = diagnostic;
7
+ this.name = "ResourceError";
8
+ this.code = diagnostic.code;
9
+ }
10
+ }
11
+ let managerSequence = 0;
12
+ const messages = {
13
+ TC_RESOURCE_INVALID_IDENTITY: "Resource type and key must be non-empty strings.",
14
+ TC_RESOURCE_INVALID_LOADER: "A loader requires load and dispose functions.",
15
+ TC_RESOURCE_LOADER_EXISTS: "A loader is already registered for this resource type.",
16
+ TC_RESOURCE_LOADER_MISSING: "Register a loader for this resource type before acquiring it.",
17
+ TC_RESOURCE_INVALID_DESCRIPTOR: "Descriptors must be finite, acyclic JSON data without accessors or custom prototypes.",
18
+ TC_RESOURCE_DESCRIPTOR_CONFLICT: "This type/key already has a different descriptor; release the old identity before replacing it.",
19
+ TC_RESOURCE_INVALID_SCOPE: "Use an open scope created by this resource manager.",
20
+ TC_RESOURCE_DISPOSED: "The resource manager has been disposed.",
21
+ TC_RESOURCE_CANCELLED: "This consumer was cancelled or its ownership was released.",
22
+ TC_RESOURCE_LOAD_FAILED: "Resource loading failed; the identity can be acquired again to retry.",
23
+ TC_RESOURCE_DISPOSE_FAILED: "Resource disposal failed; other owned resources will still be released.",
24
+ TC_RESOURCE_STALE_HANDLE: "Resource handle is stale, foreign, or has an incorrect type.",
25
+ TC_RESOURCE_NOT_READY: "The resource is still loading.",
26
+ TC_RESOURCE_INVALID_SNAPSHOT: "Resource snapshot format, uniqueness, state, or descriptor validation failed.",
27
+ TC_RESOURCE_ESTIMATE_FAILED: "Resource byte estimate is unavailable or invalid; bytes remain unknown.",
28
+ TC_RESOURCE_OBSERVER_FAILED: "A resource event observer failed and was detached.",
29
+ };
30
+ /** Descriptors/exceptions never enter diagnostics. URL credentials/query/fragment are removed. */
31
+ function safeIdentity(value) {
32
+ if (value === undefined)
33
+ return null;
34
+ return value
35
+ .replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s]*@/gi, "$1[redacted]@")
36
+ .split(/[?#]/, 1)[0]
37
+ .replace(/[\r\n\x00-\x1f]/g, "_")
38
+ .slice(0, 160);
39
+ }
40
+ /** Stricter than JSON.stringify: never silently lose identity data or invoke getters/toJSON. */
41
+ function jsonData(input, seen = new Set()) {
42
+ if (input === null || typeof input === "string" || typeof input === "boolean")
43
+ return input;
44
+ if (typeof input === "number" && Number.isFinite(input))
45
+ return Object.is(input, -0) ? 0 : input;
46
+ if (typeof input !== "object" || input === null || seen.has(input))
47
+ throw new Error("invalid JSON");
48
+ const proto = Object.getPrototypeOf(input);
49
+ if (!Array.isArray(input) && proto !== Object.prototype && proto !== null)
50
+ throw new Error("invalid prototype");
51
+ if (Object.getOwnPropertySymbols(input).length)
52
+ throw new Error("symbol property");
53
+ seen.add(input);
54
+ try {
55
+ const properties = Object.getOwnPropertyDescriptors(input);
56
+ if (Array.isArray(input)) {
57
+ if (Object.keys(properties).length !== input.length + 1)
58
+ throw new Error("sparse/extended array");
59
+ const result = [];
60
+ for (let i = 0; i < input.length; i++) {
61
+ const property = properties[String(i)];
62
+ if (!property || !("value" in property) || !property.enumerable)
63
+ throw new Error("invalid array member");
64
+ result.push(jsonData(property.value, seen));
65
+ }
66
+ return result;
67
+ }
68
+ const result = {};
69
+ for (const key of Object.keys(properties).sort()) {
70
+ const property = properties[key];
71
+ if (!("value" in property) || !property.enumerable)
72
+ throw new Error("accessor/non-enumerable");
73
+ Object.defineProperty(result, key, {
74
+ value: jsonData(property.value, seen),
75
+ enumerable: true,
76
+ writable: true,
77
+ configurable: true,
78
+ });
79
+ }
80
+ return result;
81
+ }
82
+ finally {
83
+ seen.delete(input);
84
+ }
85
+ }
86
+ function freezeData(value) {
87
+ if (value !== null && typeof value === "object") {
88
+ for (const child of Object.values(value))
89
+ freezeData(child);
90
+ Object.freeze(value);
91
+ }
92
+ return value;
93
+ }
94
+ export class ResourceManager {
95
+ id = ++managerSequence;
96
+ loaders = new Map();
97
+ identities = new Map();
98
+ slots = new Map();
99
+ generations = [];
100
+ free = [];
101
+ scopes = new Map();
102
+ scopeSequence = 0;
103
+ disposed = false;
104
+ disposePromise;
105
+ loads = new Set();
106
+ disposals = new Set();
107
+ observers = new Set();
108
+ diagnostics = [];
109
+ events = [];
110
+ sequence = 0;
111
+ historyLimit;
112
+ counters = {
113
+ loadsStarted: 0,
114
+ loadsCompleted: 0,
115
+ loadsFailed: 0,
116
+ loadsAborted: 0,
117
+ disposalsStarted: 0,
118
+ disposalsCompleted: 0,
119
+ disposalsFailed: 0,
120
+ staleResultsDisposed: 0,
121
+ consumerCancellations: 0,
122
+ peakEntries: 0,
123
+ };
124
+ constructor(options = {}) {
125
+ this.historyLimit = options.historyLimit ?? 256;
126
+ if (!Number.isSafeInteger(this.historyLimit) || this.historyLimit < 0)
127
+ throw new RangeError("historyLimit must be a nonnegative safe integer");
128
+ }
129
+ registerLoader(type, loader) {
130
+ this.assertOpen("register");
131
+ this.identity(type, "_", "register");
132
+ if (!loader ||
133
+ typeof loader.load !== "function" ||
134
+ typeof loader.dispose !== "function" ||
135
+ (loader.estimateBytes !== undefined &&
136
+ typeof loader.estimateBytes !== "function"))
137
+ throw this.error("TC_RESOURCE_INVALID_LOADER", "register", { type });
138
+ if (this.loaders.has(type))
139
+ throw this.error("TC_RESOURCE_LOADER_EXISTS", "register", { type });
140
+ this.loaders.set(type, {
141
+ load: loader.load.bind(loader),
142
+ dispose: loader.dispose.bind(loader),
143
+ estimateBytes: loader.estimateBytes?.bind(loader),
144
+ });
145
+ }
146
+ createScope(nameOrParent, parent) {
147
+ this.assertOpen("acquire");
148
+ const parentToken = typeof nameOrParent === "string" ? parent : (nameOrParent ?? parent);
149
+ const parentState = parentToken === undefined
150
+ ? undefined
151
+ : this.scope(parentToken, "acquire");
152
+ const state = {
153
+ token: undefined,
154
+ closed: false,
155
+ consumers: new Set(),
156
+ children: new Set(),
157
+ parent: parentState,
158
+ };
159
+ const token = Object.freeze({
160
+ manager: this.id,
161
+ id: ++this.scopeSequence,
162
+ get released() {
163
+ return state.closed;
164
+ },
165
+ release: () => this.releaseScope(state),
166
+ });
167
+ state.token = token;
168
+ this.scopes.set(token, state);
169
+ parentState?.children.add(state);
170
+ return token;
171
+ }
172
+ async acquire(type, key, descriptor, scope, options = {}) {
173
+ this.assertOpen("acquire");
174
+ this.identity(type, key, "acquire");
175
+ const owner = this.scope(scope, "acquire");
176
+ const normalized = this.descriptor(descriptor, "acquire", { type, key });
177
+ const canonical = JSON.stringify(normalized);
178
+ const loader = this.loaders.get(type);
179
+ if (!loader)
180
+ throw this.error("TC_RESOURCE_LOADER_MISSING", "acquire", { type, key });
181
+ if (options.signal?.aborted)
182
+ throw this.error("TC_RESOURCE_CANCELLED", "cancel", { type, key });
183
+ let entry = this.identities.get(type)?.get(key);
184
+ if (entry && entry.canonical !== canonical)
185
+ throw this.error("TC_RESOURCE_DESCRIPTOR_CONFLICT", "acquire", {
186
+ type,
187
+ key,
188
+ });
189
+ const fresh = entry === undefined;
190
+ if (!entry) {
191
+ const index = this.free.pop() ?? this.generations.length;
192
+ const generation = (this.generations[index] ?? 0) + 1;
193
+ this.generations[index] = generation;
194
+ entry = {
195
+ type,
196
+ key,
197
+ canonical,
198
+ descriptor: freezeData(normalized),
199
+ loader,
200
+ handle: Object.freeze({ manager: this.id, index, generation, type }),
201
+ controller: new AbortController(),
202
+ state: "pending",
203
+ current: true,
204
+ consumers: new Set(),
205
+ bytes: { status: "unknown" },
206
+ };
207
+ let byKey = this.identities.get(type);
208
+ if (!byKey)
209
+ this.identities.set(type, (byKey = new Map()));
210
+ byKey.set(key, entry);
211
+ this.slots.set(index, entry);
212
+ this.counters.peakEntries = Math.max(this.counters.peakEntries, this.slots.size);
213
+ }
214
+ const target = entry;
215
+ const promise = new Promise((resolve, reject) => {
216
+ const consumer = {
217
+ entry: target,
218
+ scope: owner,
219
+ active: true,
220
+ waiting: true,
221
+ resolve: resolve,
222
+ reject,
223
+ };
224
+ target.consumers.add(consumer);
225
+ owner.consumers.add(consumer);
226
+ if (options.signal) {
227
+ const abort = () => {
228
+ void this.releaseConsumer(consumer, "cancel");
229
+ };
230
+ options.signal.addEventListener("abort", abort, { once: true });
231
+ consumer.detach = () => options.signal.removeEventListener("abort", abort);
232
+ }
233
+ if (target.state === "loaded")
234
+ this.deliver(consumer);
235
+ });
236
+ if (fresh)
237
+ this.startLoad(target);
238
+ return promise;
239
+ }
240
+ get(handle) {
241
+ const entry = this.entryFor(handle, "get");
242
+ if (entry.state !== "loaded")
243
+ throw this.error("TC_RESOURCE_NOT_READY", "get", entry);
244
+ return entry.value;
245
+ }
246
+ /** Own references only for a specified scope; deduplicates multiple leases. */
247
+ snapshot(scope) {
248
+ this.assertOpen("snapshot");
249
+ const entries = scope === undefined
250
+ ? [...this.slots.values()]
251
+ : [
252
+ ...new Set([...this.scope(scope, "snapshot").consumers].map((c) => c.entry)),
253
+ ];
254
+ entries.sort((a, b) => a.type < b.type
255
+ ? -1
256
+ : a.type > b.type
257
+ ? 1
258
+ : a.key < b.key
259
+ ? -1
260
+ : a.key > b.key
261
+ ? 1
262
+ : 0);
263
+ return {
264
+ format: "pelican.resources",
265
+ version: 1,
266
+ resources: entries.map((e) => ({
267
+ type: e.type,
268
+ key: e.key,
269
+ descriptor: jsonData(e.descriptor),
270
+ state: e.state,
271
+ })),
272
+ };
273
+ }
274
+ /** Additively rebuild identities into scope. Rollback releases only this transaction's references. */
275
+ async restore(snapshot, scope, options = {}) {
276
+ this.assertOpen("restore");
277
+ const owner = this.scope(scope, "restore");
278
+ const validated = this.validateSnapshot(snapshot);
279
+ // Validate every conflict and loader before changing a single live reference.
280
+ for (const resource of validated.resources) {
281
+ if (!this.loaders.has(resource.type))
282
+ throw this.error("TC_RESOURCE_LOADER_MISSING", "restore", resource);
283
+ const live = this.identities.get(resource.type)?.get(resource.key);
284
+ if (live && live.canonical !== JSON.stringify(resource.descriptor))
285
+ throw this.error("TC_RESOURCE_DESCRIPTOR_CONFLICT", "restore", resource);
286
+ }
287
+ if (options.signal?.aborted)
288
+ throw this.error("TC_RESOURCE_CANCELLED", "restore");
289
+ const transaction = this.createScope(scope);
290
+ const controller = new AbortController();
291
+ const abort = () => controller.abort();
292
+ options.signal?.addEventListener("abort", abort, { once: true });
293
+ try {
294
+ const pending = validated.resources.map((resource) => this.acquire(resource.type, resource.key, resource.descriptor, transaction, { signal: controller.signal }));
295
+ const leases = await Promise.all(pending);
296
+ if (owner.closed || this.disposed || options.signal?.aborted)
297
+ throw this.error("TC_RESOURCE_CANCELLED", "restore");
298
+ const temporary = this.scope(transaction, "restore");
299
+ for (const consumer of temporary.consumers) {
300
+ consumer.scope = owner;
301
+ owner.consumers.add(consumer);
302
+ }
303
+ temporary.consumers.clear();
304
+ await transaction.release();
305
+ this.emit("TC_RESOURCE_RESTORED", "restore");
306
+ return leases;
307
+ }
308
+ catch (error) {
309
+ controller.abort();
310
+ await transaction.release();
311
+ throw error;
312
+ }
313
+ finally {
314
+ options.signal?.removeEventListener("abort", abort);
315
+ }
316
+ }
317
+ inspect() {
318
+ const entries = [...this.slots.values()].map((e) => ({
319
+ handle: e.handle,
320
+ key: safeIdentity(e.key),
321
+ type: safeIdentity(e.type),
322
+ state: e.state,
323
+ refCount: e.consumers.size,
324
+ bytes: { ...e.bytes },
325
+ }));
326
+ const knownBytes = entries.reduce((sum, e) => sum + (e.bytes.status === "known" ? e.bytes.value : 0), 0);
327
+ const unknownEntries = entries.filter((e) => e.bytes.status === "unknown").length;
328
+ const pending = entries.filter((e) => e.state === "pending").length;
329
+ return {
330
+ disposed: this.disposed,
331
+ owned: {
332
+ entries: entries.length,
333
+ pending,
334
+ loaded: entries.length - pending,
335
+ references: entries.reduce((sum, e) => sum + e.refCount, 0),
336
+ scopes: this.scopes.size,
337
+ inFlightLoads: this.loads.size,
338
+ inFlightDisposals: this.disposals.size,
339
+ },
340
+ bytes: unknownEntries
341
+ ? { status: "unknown", knownBytes, unknownEntries }
342
+ : { status: "known", value: knownBytes, knownBytes, unknownEntries },
343
+ entries,
344
+ counters: { ...this.counters },
345
+ };
346
+ }
347
+ getDiagnostics() {
348
+ return this.diagnostics.map((d) => ({ ...d }));
349
+ }
350
+ getEvents() {
351
+ return this.events.map((e) => ({ ...e }));
352
+ }
353
+ subscribe(observer) {
354
+ this.assertOpen("observe");
355
+ this.observers.add(observer);
356
+ return () => {
357
+ this.observers.delete(observer);
358
+ };
359
+ }
360
+ /** Includes ignored-abort loaders; callers must bound this wait for untrusted providers. */
361
+ async whenIdle() {
362
+ while (this.loads.size || this.disposals.size)
363
+ await Promise.all([...this.loads, ...this.disposals]);
364
+ }
365
+ /** Invalidates ownership immediately; does not wait for a loader ignoring abort. */
366
+ dispose() {
367
+ if (this.disposePromise)
368
+ return this.disposePromise;
369
+ this.disposed = true;
370
+ let complete;
371
+ this.disposePromise = new Promise((resolve) => {
372
+ complete = resolve;
373
+ });
374
+ const closing = [...this.scopes.values()].map((scope) => this.releaseScope(scope));
375
+ void Promise.all([...closing, ...this.disposals]).then(complete);
376
+ this.observers.clear();
377
+ return this.disposePromise;
378
+ }
379
+ startLoad(entry) {
380
+ this.counters.loadsStarted++;
381
+ const task = Promise.resolve().then(async () => {
382
+ if (!entry.current)
383
+ return;
384
+ this.emit("TC_RESOURCE_LOAD_STARTED", "load", entry);
385
+ try {
386
+ const value = await entry.loader.load(entry.descriptor, entry.controller.signal);
387
+ this.counters.loadsCompleted++;
388
+ if (!entry.current || this.disposed) {
389
+ this.counters.staleResultsDisposed++;
390
+ await this.disposeValue(entry, value);
391
+ return;
392
+ }
393
+ entry.value = value;
394
+ entry.state = "loaded";
395
+ if (entry.loader.estimateBytes) {
396
+ try {
397
+ const bytes = entry.loader.estimateBytes(value);
398
+ if (typeof bytes === "number" &&
399
+ Number.isSafeInteger(bytes) &&
400
+ bytes >= 0)
401
+ entry.bytes = { status: "known", value: bytes };
402
+ else if (bytes !== undefined)
403
+ this.error("TC_RESOURCE_ESTIMATE_FAILED", "load", entry, "warning");
404
+ }
405
+ catch {
406
+ this.error("TC_RESOURCE_ESTIMATE_FAILED", "load", entry, "warning");
407
+ }
408
+ }
409
+ for (const consumer of [...entry.consumers])
410
+ this.deliver(consumer);
411
+ this.emit("TC_RESOURCE_LOADED", "load", entry);
412
+ }
413
+ catch {
414
+ this.counters.loadsFailed++;
415
+ if (!entry.current)
416
+ return;
417
+ const failure = this.error("TC_RESOURCE_LOAD_FAILED", "load", entry);
418
+ this.invalidate(entry);
419
+ for (const consumer of [...entry.consumers])
420
+ this.finishConsumer(consumer, failure);
421
+ this.emit("TC_RESOURCE_LOAD_FAILED", "load", entry);
422
+ }
423
+ });
424
+ this.loads.add(task);
425
+ void task.then(() => {
426
+ this.loads.delete(task);
427
+ });
428
+ }
429
+ deliver(consumer) {
430
+ if (!consumer.active || !consumer.waiting)
431
+ return;
432
+ consumer.waiting = false;
433
+ consumer.detach?.();
434
+ const lease = Object.freeze({
435
+ handle: consumer.entry.handle,
436
+ get released() {
437
+ return !consumer.active;
438
+ },
439
+ release: () => this.releaseConsumer(consumer, "release"),
440
+ });
441
+ consumer.resolve(lease);
442
+ }
443
+ finishConsumer(consumer, failure) {
444
+ consumer.active = false;
445
+ consumer.detach?.();
446
+ consumer.entry.consumers.delete(consumer);
447
+ consumer.scope.consumers.delete(consumer);
448
+ if (consumer.waiting && failure) {
449
+ consumer.waiting = false;
450
+ consumer.reject(failure);
451
+ }
452
+ }
453
+ releaseConsumer(consumer, mode) {
454
+ if (consumer.releasePromise)
455
+ return consumer.releasePromise;
456
+ if (!consumer.active)
457
+ return Promise.resolve();
458
+ const entry = consumer.entry;
459
+ const failure = consumer.waiting
460
+ ? this.error("TC_RESOURCE_CANCELLED", mode, entry)
461
+ : undefined;
462
+ this.finishConsumer(consumer, failure);
463
+ if (mode === "cancel")
464
+ this.counters.consumerCancellations++;
465
+ const last = entry.current && entry.consumers.size === 0;
466
+ if (last) {
467
+ this.invalidate(entry);
468
+ if (entry.state === "pending") {
469
+ this.counters.loadsAborted++;
470
+ entry.controller.abort();
471
+ this.emit("TC_RESOURCE_LOAD_ABORTED", "cancel", entry);
472
+ }
473
+ }
474
+ consumer.releasePromise =
475
+ last && entry.state === "loaded"
476
+ ? this.disposeValue(entry, entry.value)
477
+ : Promise.resolve();
478
+ this.emit("TC_RESOURCE_RELEASED", mode, entry);
479
+ return consumer.releasePromise;
480
+ }
481
+ releaseScope(scope) {
482
+ if (scope.releasePromise)
483
+ return scope.releasePromise;
484
+ scope.closed = true;
485
+ this.scopes.delete(scope.token);
486
+ scope.parent?.children.delete(scope);
487
+ let complete;
488
+ scope.releasePromise = new Promise((resolve) => {
489
+ complete = resolve;
490
+ });
491
+ void Promise.all([
492
+ ...[...scope.children].map((child) => this.releaseScope(child)),
493
+ ...[...scope.consumers].map((consumer) => this.releaseConsumer(consumer, "release")),
494
+ ]).then(complete);
495
+ return scope.releasePromise;
496
+ }
497
+ disposeValue(entry, value) {
498
+ this.counters.disposalsStarted++;
499
+ const task = Promise.resolve().then(async () => {
500
+ try {
501
+ await entry.loader.dispose(value);
502
+ this.counters.disposalsCompleted++;
503
+ this.emit("TC_RESOURCE_DISPOSED", "dispose", entry);
504
+ }
505
+ catch {
506
+ this.counters.disposalsFailed++;
507
+ this.error("TC_RESOURCE_DISPOSE_FAILED", "dispose", entry);
508
+ this.emit("TC_RESOURCE_DISPOSE_FAILED", "dispose", entry);
509
+ }
510
+ finally {
511
+ entry.value = undefined;
512
+ }
513
+ });
514
+ this.disposals.add(task);
515
+ void task.then(() => {
516
+ this.disposals.delete(task);
517
+ });
518
+ return task;
519
+ }
520
+ invalidate(entry) {
521
+ if (!entry.current)
522
+ return;
523
+ entry.current = false;
524
+ this.slots.delete(entry.handle.index);
525
+ const byKey = this.identities.get(entry.type);
526
+ if (byKey.get(entry.key) === entry)
527
+ byKey.delete(entry.key);
528
+ if (!byKey.size)
529
+ this.identities.delete(entry.type);
530
+ this.free.push(entry.handle.index);
531
+ }
532
+ entryFor(handle, mode) {
533
+ const entry = handle && this.slots.get(handle.index);
534
+ if (!entry ||
535
+ handle.manager !== this.id ||
536
+ handle.generation !== entry.handle.generation ||
537
+ handle.type !== entry.type)
538
+ throw this.error("TC_RESOURCE_STALE_HANDLE", mode);
539
+ return entry;
540
+ }
541
+ scope(token, mode) {
542
+ const scope = this.scopes.get(token);
543
+ if (!scope || scope.closed)
544
+ throw this.error("TC_RESOURCE_INVALID_SCOPE", mode);
545
+ return scope;
546
+ }
547
+ identity(type, key, mode) {
548
+ if (typeof type !== "string" ||
549
+ !type.length ||
550
+ typeof key !== "string" ||
551
+ !key.length)
552
+ throw this.error("TC_RESOURCE_INVALID_IDENTITY", mode);
553
+ }
554
+ descriptor(value, mode, identity) {
555
+ try {
556
+ return jsonData(value);
557
+ }
558
+ catch {
559
+ throw this.error("TC_RESOURCE_INVALID_DESCRIPTOR", mode, identity);
560
+ }
561
+ }
562
+ assertOpen(mode) {
563
+ if (this.disposed)
564
+ throw this.error("TC_RESOURCE_DISPOSED", mode);
565
+ }
566
+ validateSnapshot(input) {
567
+ let snapshot;
568
+ try {
569
+ snapshot = jsonData(input);
570
+ }
571
+ catch {
572
+ throw this.error("TC_RESOURCE_INVALID_SNAPSHOT", "restore");
573
+ }
574
+ const invalid = () => this.error("TC_RESOURCE_INVALID_SNAPSHOT", "restore");
575
+ if (!snapshot ||
576
+ Array.isArray(snapshot) ||
577
+ typeof snapshot !== "object" ||
578
+ snapshot.format !== "pelican.resources" ||
579
+ snapshot.version !== 1 ||
580
+ !Array.isArray(snapshot.resources) ||
581
+ Object.keys(snapshot).sort().join() !== "format,resources,version")
582
+ throw invalid();
583
+ const identities = new Map();
584
+ for (const item of snapshot.resources) {
585
+ if (!item ||
586
+ Array.isArray(item) ||
587
+ typeof item !== "object" ||
588
+ Object.keys(item).sort().join() !== "descriptor,key,state,type" ||
589
+ typeof item.type !== "string" ||
590
+ !item.type.length ||
591
+ typeof item.key !== "string" ||
592
+ !item.key.length ||
593
+ (item.state !== "pending" && item.state !== "loaded"))
594
+ throw invalid();
595
+ let keys = identities.get(item.type);
596
+ if (!keys)
597
+ identities.set(item.type, (keys = new Set()));
598
+ if (keys.has(item.key))
599
+ throw invalid();
600
+ keys.add(item.key);
601
+ }
602
+ return snapshot;
603
+ }
604
+ error(code, mode, identity, severity = "error") {
605
+ const diagnostic = Object.freeze({
606
+ code,
607
+ mode,
608
+ severity,
609
+ key: safeIdentity(identity?.key),
610
+ type: safeIdentity(identity?.type),
611
+ message: messages[code] ?? "Resource operation failed.",
612
+ });
613
+ this.retain(this.diagnostics, diagnostic);
614
+ return new ResourceError(diagnostic);
615
+ }
616
+ emit(code, mode, entry) {
617
+ const event = Object.freeze({
618
+ sequence: ++this.sequence,
619
+ code,
620
+ mode,
621
+ key: safeIdentity(entry?.key),
622
+ type: safeIdentity(entry?.type),
623
+ handle: entry
624
+ ? Object.freeze({ ...entry.handle, type: safeIdentity(entry.type) })
625
+ : null,
626
+ });
627
+ this.retain(this.events, event);
628
+ for (const observer of [...this.observers]) {
629
+ try {
630
+ observer(event);
631
+ }
632
+ catch {
633
+ this.observers.delete(observer);
634
+ this.error("TC_RESOURCE_OBSERVER_FAILED", "observe", entry, "warning");
635
+ }
636
+ }
637
+ }
638
+ retain(history, item) {
639
+ if (!this.historyLimit)
640
+ return;
641
+ history.push(item);
642
+ if (history.length > this.historyLimit)
643
+ history.shift();
644
+ }
645
+ }
646
+ export function createResourceManager(options) {
647
+ return new ResourceManager(options);
648
+ }