@flighthq/assets 0.2.0 → 0.2.1-next.410.a23f189

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/assets",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-next.410.a23f189",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -32,9 +32,9 @@
32
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@flighthq/loader": "0.2.0",
36
- "@flighthq/signals": "0.2.0",
37
- "@flighthq/types": "0.2.0"
35
+ "@flighthq/loader": "0.2.1-next.410.a23f189",
36
+ "@flighthq/signals": "0.2.1-next.410.a23f189",
37
+ "@flighthq/types": "0.2.1-next.410.a23f189"
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "^5.3.0"
@@ -168,6 +168,53 @@ describe('getAssetRefCount', () => {
168
168
  });
169
169
 
170
170
  describe('loadAssetGroup', () => {
171
+ it('makes successfully loaded assets accessible even when another group member fails', async () => {
172
+ const library = createAssetLibrary();
173
+ let failNextId: string | null = null;
174
+ const disposed: unknown[] = [];
175
+ const adapter = {
176
+ load(descriptor: Readonly<AssetDescriptor>): Promise<{ id: string }> {
177
+ if (descriptor.id === failNextId) {
178
+ return Promise.reject(new Error(`load failed: ${descriptor.id}`));
179
+ }
180
+ return Promise.resolve({ id: descriptor.id });
181
+ },
182
+ dispose(value: { id: string }): void {
183
+ disposed.push(value);
184
+ },
185
+ };
186
+ registerAssetLoader(library, 'image', adapter);
187
+ loadAssetManifest(library, [
188
+ { id: 'ok-1', url: 'ok-1.bin', type: 'image', group: 'mixed' },
189
+ { id: 'bad', url: 'bad.bin', type: 'image', group: 'mixed' },
190
+ { id: 'ok-2', url: 'ok-2.bin', type: 'image', group: 'mixed' },
191
+ ]);
192
+ failNextId = 'bad';
193
+
194
+ // loadAssetGroup routes each acquire through @flighthq/loader, which rejects the per-item handle on
195
+ // failure. That handle is void'd by loadAssetGroup (error-policy: continue), so the rejection
196
+ // surfaces as an unhandled rejection. Suppress it for this test.
197
+ const rejections: unknown[] = [];
198
+ const capture = (reason: unknown) => {
199
+ rejections.push(reason);
200
+ };
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ (globalThis as any).process.on('unhandledRejection', capture);
203
+
204
+ await loadAssetGroup(library, 'mixed');
205
+ // Drain the microtask queue so the rejection fires within the listener's window.
206
+ await tick();
207
+
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ (globalThis as any).process.removeListener('unhandledRejection', capture);
210
+ expect(rejections).toHaveLength(1);
211
+
212
+ expect(getAsset(library, 'ok-1')).toEqual({ id: 'ok-1' });
213
+ expect(getAsset(library, 'ok-2')).toEqual({ id: 'ok-2' });
214
+ // The failed asset never became resident.
215
+ expect(getAsset(library, 'bad')).toBeNull();
216
+ });
217
+
171
218
  it('preloads a group through the loader with bounded concurrency and aggregate progress', async () => {
172
219
  const library = createAssetLibrary();
173
220
  const mock = createMockAdapter();
@@ -254,6 +301,23 @@ describe('registerAssetLoader', () => {
254
301
  });
255
302
 
256
303
  describe('releaseAsset', () => {
304
+ it('disposes the orphaned value when released while the load is still in flight', async () => {
305
+ const { library, mock } = libraryWith('hero');
306
+ const promise = acquireAsset(library, 'hero');
307
+ // Release before the load settles — refcount drops to zero, entry removed.
308
+ releaseAsset(library, 'hero');
309
+ expect(getAssetRefCount(library, 'hero')).toBe(0);
310
+ expect(getAsset(library, 'hero')).toBeNull();
311
+
312
+ // Let the load resolve — the continuation detects the orphan and disposes the value.
313
+ mock.flush();
314
+ await promise;
315
+ expect(mock.disposed).toHaveLength(1);
316
+ expect(mock.disposed[0]).toEqual({ id: 'hero' });
317
+ // The value is not retained.
318
+ expect(getAsset(library, 'hero')).toBeNull();
319
+ });
320
+
257
321
  it('disposes and drops the asset at reference count zero', async () => {
258
322
  const { library, mock } = libraryWith('hero');
259
323
  const promise = acquireAsset(library, 'hero');
@@ -279,6 +343,22 @@ describe('releaseAsset', () => {
279
343
  expect(mock.disposed).toEqual([]);
280
344
  expect(getAssetRefCount(library, 'hero')).toBe(0);
281
345
  });
346
+
347
+ it('keeps the asset loaded when one of two holders releases', async () => {
348
+ const { library, mock } = libraryWith('hero');
349
+ const first = acquireAsset<{ id: string }>(library, 'hero');
350
+ const second = acquireAsset<{ id: string }>(library, 'hero');
351
+ expect(mock.loadCalls).toBe(1);
352
+ mock.flush();
353
+ const [a, b] = await Promise.all([first, second]);
354
+ expect(a).toBe(b);
355
+ expect(getAssetRefCount(library, 'hero')).toBe(2);
356
+
357
+ releaseAsset(library, 'hero');
358
+ expect(getAssetRefCount(library, 'hero')).toBe(1);
359
+ expect(mock.disposed).toEqual([]);
360
+ expect(getAsset(library, 'hero')).toBe(a);
361
+ });
282
362
  });
283
363
 
284
364
  describe('releaseAssetGroup', () => {