@flighthq/assets 0.1.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.
@@ -0,0 +1,12 @@
1
+ import type { AssetGroupLoadOptions, AssetLibrary, AssetLoaderAdapter, AssetManifest, AssetType } from '@flighthq/types';
2
+ export declare function acquireAsset<T = unknown>(library: Readonly<AssetLibrary>, id: string): Promise<T>;
3
+ export declare function createAssetLibrary(): AssetLibrary;
4
+ export declare function disposeAssetLibrary(library: Readonly<AssetLibrary>): void;
5
+ export declare function getAsset<T = unknown>(library: Readonly<AssetLibrary>, id: string): T | null;
6
+ export declare function getAssetRefCount(library: Readonly<AssetLibrary>, id: string): number;
7
+ export declare function loadAssetGroup(library: Readonly<AssetLibrary>, name: string, options?: Readonly<AssetGroupLoadOptions>): Promise<void>;
8
+ export declare function loadAssetManifest(library: Readonly<AssetLibrary>, manifest: AssetManifest): void;
9
+ export declare function registerAssetLoader<T>(library: Readonly<AssetLibrary>, type: AssetType, adapter: Readonly<AssetLoaderAdapter<T>>): void;
10
+ export declare function releaseAsset(library: Readonly<AssetLibrary>, id: string): void;
11
+ export declare function releaseAssetGroup(library: Readonly<AssetLibrary>, name: string): void;
12
+ //# sourceMappingURL=assetLibrary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetLibrary.d.ts","sourceRoot":"","sources":["../src/assetLibrary.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,qBAAqB,EACrB,YAAY,EAEZ,kBAAkB,EAClB,aAAa,EACb,SAAS,EACV,MAAM,iBAAiB,CAAC;AAQzB,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAkCjG;AAKD,wBAAgB,kBAAkB,IAAI,YAAY,CAQjD;AAID,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAYzE;AAID,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAG3F;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAGpF;AAOD,wBAAsB,cAAc,CAClC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GACxC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAID,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAYhG;AAKD,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC/B,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GACvC,IAAI,CAEN;AAKD,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAO9E;AAKD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIrF"}
@@ -0,0 +1,176 @@
1
+ import { createResourceLoader, disposeResourceLoader, queueResourceLoad, startResourceLoad } from '@flighthq/loader';
2
+ import { connectSignal, emitSignal } from '@flighthq/signals';
3
+ // Increments the reference count for `id` and resolves its loaded value. If the asset is already
4
+ // resident, resolves immediately. If a load is already in flight (a concurrent acquire), the same
5
+ // promise is shared, so exactly one adapter.load runs per id (dedup). Otherwise the descriptor is
6
+ // resolved to its type's adapter and the load begins, storing the value at reference count one.
7
+ // Rejects (an async sentinel, not a throw) when no descriptor is recorded for the id or no adapter is
8
+ // registered for its type — the two misuse cases the library cannot recover from on its own.
9
+ export function acquireAsset(library, id) {
10
+ const runtime = library.runtime;
11
+ const descriptor = runtime.descriptors.get(id);
12
+ if (descriptor === undefined) {
13
+ return Promise.reject(new Error(`assets: no descriptor for id "${id}" (loadAssetManifest first)`));
14
+ }
15
+ const adapter = runtime.adapters.get(descriptor.type);
16
+ if (adapter === undefined) {
17
+ return Promise.reject(new Error(`assets: no loader for type "${descriptor.type}" (registerAssetLoader first)`));
18
+ }
19
+ const existing = runtime.entries.get(id);
20
+ if (existing !== undefined) {
21
+ existing.refcount++;
22
+ if (existing.resident)
23
+ return Promise.resolve(existing.value);
24
+ // A load is in flight — share it so only one adapter.load runs per id.
25
+ return existing.loadPromise;
26
+ }
27
+ const entry = { value: undefined, refcount: 1, loadPromise: null, resident: false };
28
+ runtime.entries.set(id, entry);
29
+ const loadPromise = adapter.load(descriptor).then((value) => {
30
+ if (runtime.entries.get(id) !== entry || entry.refcount <= 0) {
31
+ // Released before the load settled — free the orphaned resource deterministically.
32
+ adapter.dispose(value);
33
+ return value;
34
+ }
35
+ entry.value = value;
36
+ entry.resident = true;
37
+ entry.loadPromise = null;
38
+ return value;
39
+ });
40
+ entry.loadPromise = loadPromise;
41
+ return loadPromise;
42
+ }
43
+ // Allocates an empty asset library — an open adapter registry, an empty descriptor map, an empty cache,
44
+ // and an empty group index. Registers no adapters and knows how to load nothing until the caller opts
45
+ // in with registerAssetLoader.
46
+ export function createAssetLibrary() {
47
+ const runtime = {
48
+ adapters: new Map(),
49
+ descriptors: new Map(),
50
+ entries: new Map(),
51
+ groups: new Map(),
52
+ };
53
+ return { runtime };
54
+ }
55
+ // Disposes every resident asset through its registered adapter and empties the library — adapters,
56
+ // descriptors, cache entries, and groups. Leaves the library reusable but stripped of all state.
57
+ export function disposeAssetLibrary(library) {
58
+ const runtime = library.runtime;
59
+ for (const [id, entry] of runtime.entries) {
60
+ if (!entry.resident)
61
+ continue;
62
+ const descriptor = runtime.descriptors.get(id);
63
+ const adapter = descriptor !== undefined ? runtime.adapters.get(descriptor.type) : undefined;
64
+ if (adapter !== undefined)
65
+ adapter.dispose(entry.value);
66
+ }
67
+ runtime.adapters.clear();
68
+ runtime.descriptors.clear();
69
+ runtime.entries.clear();
70
+ runtime.groups.clear();
71
+ }
72
+ // Returns the resident value for `id` synchronously, or null if it is not loaded (never acquired, or
73
+ // still loading). Never triggers a load — use acquireAsset for that.
74
+ export function getAsset(library, id) {
75
+ const entry = library.runtime.entries.get(id);
76
+ return entry !== undefined && entry.resident ? entry.value : null;
77
+ }
78
+ // Returns the live holder count for `id`: how many acquires have not yet been matched by a release.
79
+ // Zero for an id that was never acquired or has already been freed at reference count zero.
80
+ export function getAssetRefCount(library, id) {
81
+ const entry = library.runtime.entries.get(id);
82
+ return entry !== undefined ? entry.refcount : 0;
83
+ }
84
+ // Preloads a named group through @flighthq/loader: every member that is not already resident is
85
+ // scheduled as a loader item (bounded concurrency, aggregate progress via options.progress), and every
86
+ // member is acquired (reference count incremented) so the whole group stays resident until
87
+ // releaseAssetGroup. Resolves once all scheduled loads settle. A group with no recorded members
88
+ // resolves immediately.
89
+ export async function loadAssetGroup(library, name, options) {
90
+ const runtime = library.runtime;
91
+ const ids = runtime.groups.get(name);
92
+ if (ids === undefined || ids.length === 0)
93
+ return;
94
+ const loader = createResourceLoader();
95
+ const progress = options?.progress;
96
+ if (progress !== undefined) {
97
+ connectSignal(loader.onProgress, (loaded, total) => {
98
+ emitSignal(progress, { loaded, total });
99
+ });
100
+ }
101
+ for (const id of ids) {
102
+ const entry = runtime.entries.get(id);
103
+ if (entry !== undefined && entry.resident) {
104
+ // Already loaded — hold a group reference without scheduling a redundant load.
105
+ void acquireAsset(library, id);
106
+ continue;
107
+ }
108
+ // Route the actual load through the loader for bounded concurrency; acquireAsset dedups and holds
109
+ // the group's reference.
110
+ queueResourceLoad(loader, () => acquireAsset(library, id));
111
+ }
112
+ await new Promise((resolve) => {
113
+ connectSignal(loader.onComplete, () => resolve());
114
+ startResourceLoad(loader);
115
+ });
116
+ disposeResourceLoader(loader);
117
+ }
118
+ // Records every descriptor's id → descriptor mapping and its group membership. Does not load anything —
119
+ // acquireAsset and loadAssetGroup perform the loads. Re-recording an id overwrites its descriptor.
120
+ export function loadAssetManifest(library, manifest) {
121
+ const runtime = library.runtime;
122
+ for (const descriptor of manifest) {
123
+ runtime.descriptors.set(descriptor.id, descriptor);
124
+ if (descriptor.group === undefined)
125
+ continue;
126
+ let members = runtime.groups.get(descriptor.group);
127
+ if (members === undefined) {
128
+ members = [];
129
+ runtime.groups.set(descriptor.group, members);
130
+ }
131
+ if (!members.includes(descriptor.id))
132
+ members.push(descriptor.id);
133
+ }
134
+ }
135
+ // Binds an asset type to how it loads and how it frees. The registry is open and last-write-wins, so a
136
+ // user adds their own (vendor-prefixed) types and can override a prior binding. The library depends on
137
+ // no resource package; the adapter is where a concrete decoder is wired in.
138
+ export function registerAssetLoader(library, type, adapter) {
139
+ library.runtime.adapters.set(type, adapter);
140
+ }
141
+ // Decrements the reference count for `id`. When it reaches zero the asset is immediately disposed
142
+ // through its registered adapter and dropped from the cache (deterministic free). Releasing an id that
143
+ // is not held — never acquired, or already freed at zero — is a no-op.
144
+ export function releaseAsset(library, id) {
145
+ const runtime = library.runtime;
146
+ const entry = runtime.entries.get(id);
147
+ if (entry === undefined)
148
+ return;
149
+ entry.refcount--;
150
+ if (entry.refcount > 0)
151
+ return;
152
+ disposeAssetEntry(runtime, id, entry);
153
+ }
154
+ // Releases the group reference held by loadAssetGroup for each member, mirroring the per-member acquire
155
+ // it performed. A member reaching reference count zero is disposed and dropped. A group with no
156
+ // recorded members is a no-op.
157
+ export function releaseAssetGroup(library, name) {
158
+ const ids = library.runtime.groups.get(name);
159
+ if (ids === undefined)
160
+ return;
161
+ for (const id of ids)
162
+ releaseAsset(library, id);
163
+ }
164
+ // Drops the cache entry and, when the asset actually decoded, frees it through its adapter. An entry
165
+ // whose load never settled (released mid-flight) has nothing decoded to free; the in-flight load's own
166
+ // continuation disposes the orphaned value once it resolves.
167
+ function disposeAssetEntry(runtime, id, entry) {
168
+ runtime.entries.delete(id);
169
+ if (!entry.resident)
170
+ return;
171
+ const descriptor = runtime.descriptors.get(id);
172
+ const adapter = descriptor !== undefined ? runtime.adapters.get(descriptor.type) : undefined;
173
+ if (adapter !== undefined)
174
+ adapter.dispose(entry.value);
175
+ }
176
+ //# sourceMappingURL=assetLibrary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetLibrary.js","sourceRoot":"","sources":["../src/assetLibrary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './assetLibrary';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './assetLibrary';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@flighthq/assets",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/loader": "0.1.0",
31
+ "@flighthq/signals": "0.1.0",
32
+ "@flighthq/types": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.3.0"
36
+ },
37
+ "description": "Id-keyed asset library over @flighthq/loader — manifests, group preload, refcounted ownership, and an open per-type loader registry",
38
+ "sideEffects": false
39
+ }
@@ -0,0 +1,318 @@
1
+ import { connectSignal, createSignal } from '@flighthq/signals';
2
+ import type { AssetDescriptor, AssetLoadProgress, AssetManifest } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import {
6
+ acquireAsset,
7
+ createAssetLibrary,
8
+ disposeAssetLibrary,
9
+ getAsset,
10
+ getAssetRefCount,
11
+ loadAssetGroup,
12
+ loadAssetManifest,
13
+ registerAssetLoader,
14
+ releaseAsset,
15
+ releaseAssetGroup,
16
+ } from './assetLibrary';
17
+
18
+ // A mock loader adapter: counts load calls, records disposed values, and holds each load open until
19
+ // flush() so tests can observe in-flight state (dedup, bounded concurrency). Each load resolves a
20
+ // fresh, stable value object keyed by id, so identity assertions distinguish shared vs. re-loaded.
21
+ function createMockAdapter() {
22
+ let loadCalls = 0;
23
+ let inFlight = 0;
24
+ let peak = 0;
25
+ const disposed: unknown[] = [];
26
+ const pending: Array<() => void> = [];
27
+
28
+ return {
29
+ adapter: {
30
+ load(descriptor: Readonly<AssetDescriptor>): Promise<{ id: string }> {
31
+ loadCalls++;
32
+ inFlight++;
33
+ peak = Math.max(peak, inFlight);
34
+ return new Promise<{ id: string }>((resolve) => {
35
+ pending.push(() => {
36
+ inFlight--;
37
+ resolve({ id: descriptor.id });
38
+ });
39
+ });
40
+ },
41
+ dispose(value: { id: string }): void {
42
+ disposed.push(value);
43
+ },
44
+ },
45
+ disposed,
46
+ flush(): void {
47
+ const wave = pending.splice(0);
48
+ for (const settle of wave) settle();
49
+ },
50
+ get inFlight() {
51
+ return inFlight;
52
+ },
53
+ get loadCalls() {
54
+ return loadCalls;
55
+ },
56
+ get peak() {
57
+ return peak;
58
+ },
59
+ get pendingCount() {
60
+ return pending.length;
61
+ },
62
+ };
63
+ }
64
+
65
+ // Registers `type`, records a one-descriptor manifest for `id`, and returns the mock so a test can
66
+ // acquire and drive the load.
67
+ function libraryWith(id: string, type = 'image') {
68
+ const library = createAssetLibrary();
69
+ const mock = createMockAdapter();
70
+ registerAssetLoader(library, type, mock.adapter);
71
+ loadAssetManifest(library, [{ id, url: `${id}.bin`, type }]);
72
+ return { library, mock };
73
+ }
74
+
75
+ // Runs pending microtasks so loader continuations settle between flush waves.
76
+ function tick(): Promise<void> {
77
+ return new Promise((resolve) => setTimeout(resolve, 0));
78
+ }
79
+
80
+ describe('acquireAsset', () => {
81
+ it('resolves the adapter loaded value and calls load once', async () => {
82
+ const { library, mock } = libraryWith('hero');
83
+ const promise = acquireAsset<{ id: string }>(library, 'hero');
84
+ expect(mock.loadCalls).toBe(1);
85
+ mock.flush();
86
+ const value = await promise;
87
+ expect(value).toEqual({ id: 'hero' });
88
+ expect(getAsset(library, 'hero')).toBe(value);
89
+ });
90
+
91
+ it('shares one in-flight load across concurrent acquires (dedup)', async () => {
92
+ const { library, mock } = libraryWith('hero');
93
+ const first = acquireAsset<{ id: string }>(library, 'hero');
94
+ const second = acquireAsset<{ id: string }>(library, 'hero');
95
+ expect(mock.loadCalls).toBe(1);
96
+ expect(getAssetRefCount(library, 'hero')).toBe(2);
97
+ mock.flush();
98
+ const [a, b] = await Promise.all([first, second]);
99
+ expect(a).toBe(b);
100
+ expect(getAsset(library, 'hero')).toBe(a);
101
+ });
102
+
103
+ it('rejects when no descriptor is recorded for the id', async () => {
104
+ const library = createAssetLibrary();
105
+ await expect(acquireAsset(library, 'missing')).rejects.toThrow(/no descriptor/);
106
+ });
107
+
108
+ it('rejects when no adapter is registered for the descriptor type', async () => {
109
+ const library = createAssetLibrary();
110
+ loadAssetManifest(library, [{ id: 'hero', url: 'hero.bin', type: 'image' }]);
111
+ await expect(acquireAsset(library, 'hero')).rejects.toThrow(/no loader/);
112
+ });
113
+ });
114
+
115
+ describe('createAssetLibrary', () => {
116
+ it('creates an empty library with no resident assets', () => {
117
+ const library = createAssetLibrary();
118
+ expect(getAsset(library, 'hero')).toBeNull();
119
+ expect(getAssetRefCount(library, 'hero')).toBe(0);
120
+ });
121
+ });
122
+
123
+ describe('disposeAssetLibrary', () => {
124
+ it('disposes every resident asset and empties the library', async () => {
125
+ const { library, mock } = libraryWith('hero');
126
+ await (() => {
127
+ const p = acquireAsset(library, 'hero');
128
+ mock.flush();
129
+ return p;
130
+ })();
131
+ // A second acquire on the same id shares the resident value (refcount 2, one loaded value).
132
+ await acquireAsset(library, 'hero');
133
+ expect(getAssetRefCount(library, 'hero')).toBe(2);
134
+
135
+ disposeAssetLibrary(library);
136
+ expect(mock.disposed).toEqual([{ id: 'hero' }]);
137
+ expect(getAsset(library, 'hero')).toBeNull();
138
+ expect(getAssetRefCount(library, 'hero')).toBe(0);
139
+ });
140
+ });
141
+
142
+ describe('getAsset', () => {
143
+ it('returns null before load and the value once resident', async () => {
144
+ const { library, mock } = libraryWith('hero');
145
+ expect(getAsset(library, 'hero')).toBeNull();
146
+ const promise = acquireAsset(library, 'hero');
147
+ // Still loading — not yet resident.
148
+ expect(getAsset(library, 'hero')).toBeNull();
149
+ mock.flush();
150
+ const value = await promise;
151
+ expect(getAsset(library, 'hero')).toBe(value);
152
+ });
153
+ });
154
+
155
+ describe('getAssetRefCount', () => {
156
+ it('counts acquires and drops to zero when freed', async () => {
157
+ const { library, mock } = libraryWith('hero');
158
+ const promise = acquireAsset(library, 'hero');
159
+ mock.flush();
160
+ await promise;
161
+ await acquireAsset(library, 'hero');
162
+ expect(getAssetRefCount(library, 'hero')).toBe(2);
163
+ releaseAsset(library, 'hero');
164
+ expect(getAssetRefCount(library, 'hero')).toBe(1);
165
+ releaseAsset(library, 'hero');
166
+ expect(getAssetRefCount(library, 'hero')).toBe(0);
167
+ });
168
+ });
169
+
170
+ describe('loadAssetGroup', () => {
171
+ it('preloads a group through the loader with bounded concurrency and aggregate progress', async () => {
172
+ const library = createAssetLibrary();
173
+ const mock = createMockAdapter();
174
+ registerAssetLoader(library, 'image', mock.adapter);
175
+
176
+ const count = 10;
177
+ const manifest: AssetManifest = Array.from({ length: count }, (_unused, i) => ({
178
+ id: `tile-${i}`,
179
+ url: `tile-${i}.bin`,
180
+ type: 'image',
181
+ group: 'level',
182
+ }));
183
+ loadAssetManifest(library, manifest);
184
+
185
+ const ticks: AssetLoadProgress[] = [];
186
+ const progress = createSignal<(p: Readonly<AssetLoadProgress>) => void>();
187
+ connectSignal(progress, (p) => {
188
+ ticks.push({ loaded: p.loaded, total: p.total });
189
+ });
190
+
191
+ const done = loadAssetGroup(library, 'level', { progress });
192
+ // The loader dispatches at most its default concurrency (6) at once.
193
+ expect(mock.peak).toBe(6);
194
+
195
+ let settled = false;
196
+ void done.then(() => {
197
+ settled = true;
198
+ });
199
+ while (!settled) {
200
+ mock.flush();
201
+ await tick();
202
+ }
203
+ await done;
204
+
205
+ expect(mock.loadCalls).toBe(count);
206
+ expect(mock.peak).toBeLessThanOrEqual(6);
207
+ for (let i = 0; i < count; i++) {
208
+ expect(getAsset(library, `tile-${i}`)).toEqual({ id: `tile-${i}` });
209
+ expect(getAssetRefCount(library, `tile-${i}`)).toBe(1);
210
+ }
211
+ expect(ticks[ticks.length - 1]).toEqual({ loaded: count, total: count });
212
+ });
213
+
214
+ it('resolves immediately for an unknown or empty group', async () => {
215
+ const library = createAssetLibrary();
216
+ await expect(loadAssetGroup(library, 'nope')).resolves.toBeUndefined();
217
+ });
218
+ });
219
+
220
+ describe('loadAssetManifest', () => {
221
+ it('records descriptors and group membership without loading', async () => {
222
+ const library = createAssetLibrary();
223
+ const mock = createMockAdapter();
224
+ registerAssetLoader(library, 'image', mock.adapter);
225
+ loadAssetManifest(library, [
226
+ { id: 'a', url: 'a.bin', type: 'image', group: 'boot' },
227
+ { id: 'b', url: 'b.bin', type: 'image', group: 'boot' },
228
+ ]);
229
+ // Nothing loaded from recording alone.
230
+ expect(mock.loadCalls).toBe(0);
231
+ expect(getAsset(library, 'a')).toBeNull();
232
+ // The recorded descriptor makes acquire resolvable.
233
+ const promise = acquireAsset(library, 'a');
234
+ mock.flush();
235
+ await promise;
236
+ expect(getAsset(library, 'a')).toEqual({ id: 'a' });
237
+ });
238
+ });
239
+
240
+ describe('registerAssetLoader', () => {
241
+ it('is last-write-wins for a type', async () => {
242
+ const library = createAssetLibrary();
243
+ const first = createMockAdapter();
244
+ const second = createMockAdapter();
245
+ registerAssetLoader(library, 'image', first.adapter);
246
+ registerAssetLoader(library, 'image', second.adapter);
247
+ loadAssetManifest(library, [{ id: 'hero', url: 'hero.bin', type: 'image' }]);
248
+ const promise = acquireAsset(library, 'hero');
249
+ second.flush();
250
+ await promise;
251
+ expect(first.loadCalls).toBe(0);
252
+ expect(second.loadCalls).toBe(1);
253
+ });
254
+ });
255
+
256
+ describe('releaseAsset', () => {
257
+ it('disposes and drops the asset at reference count zero', async () => {
258
+ const { library, mock } = libraryWith('hero');
259
+ const promise = acquireAsset(library, 'hero');
260
+ mock.flush();
261
+ const value = await promise;
262
+ await acquireAsset(library, 'hero');
263
+
264
+ releaseAsset(library, 'hero');
265
+ // Still held by the second acquire — not disposed.
266
+ expect(mock.disposed).toEqual([]);
267
+ expect(getAsset(library, 'hero')).toBe(value);
268
+
269
+ releaseAsset(library, 'hero');
270
+ // Last holder gone — disposed once and dropped.
271
+ expect(mock.disposed).toEqual([value]);
272
+ expect(getAsset(library, 'hero')).toBeNull();
273
+ });
274
+
275
+ it('is a no-op when releasing below zero', () => {
276
+ const { library, mock } = libraryWith('hero');
277
+ releaseAsset(library, 'hero');
278
+ releaseAsset(library, 'hero');
279
+ expect(mock.disposed).toEqual([]);
280
+ expect(getAssetRefCount(library, 'hero')).toBe(0);
281
+ });
282
+ });
283
+
284
+ describe('releaseAssetGroup', () => {
285
+ it('releases and disposes every group member', async () => {
286
+ const library = createAssetLibrary();
287
+ const mock = createMockAdapter();
288
+ registerAssetLoader(library, 'image', mock.adapter);
289
+ loadAssetManifest(library, [
290
+ { id: 'a', url: 'a.bin', type: 'image', group: 'level' },
291
+ { id: 'b', url: 'b.bin', type: 'image', group: 'level' },
292
+ ]);
293
+
294
+ const done = loadAssetGroup(library, 'level');
295
+ let settled = false;
296
+ void done.then(() => {
297
+ settled = true;
298
+ });
299
+ while (!settled) {
300
+ mock.flush();
301
+ await tick();
302
+ }
303
+ await done;
304
+
305
+ expect(getAssetRefCount(library, 'a')).toBe(1);
306
+ expect(getAssetRefCount(library, 'b')).toBe(1);
307
+
308
+ releaseAssetGroup(library, 'level');
309
+ expect(mock.disposed).toEqual([{ id: 'a' }, { id: 'b' }]);
310
+ expect(getAsset(library, 'a')).toBeNull();
311
+ expect(getAsset(library, 'b')).toBeNull();
312
+ });
313
+
314
+ it('is a no-op for an unknown group', () => {
315
+ const library = createAssetLibrary();
316
+ expect(() => releaseAssetGroup(library, 'nope')).not.toThrow();
317
+ });
318
+ });