@flighthq/audio 0.3.0-next.1032.e2b99fc → 0.3.0-next.1220.6982c21
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioResourceFrom.d.ts","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAShF,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,CAUnH;AAID,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAKxB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,EACV,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAGxB;AAKD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"audioResourceFrom.d.ts","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAShF,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,CAUnH;AAID,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAKxB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,EACV,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAGxB;AAKD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAWxB;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAgBxB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAIxB;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,GAAG,IAAI,CAM1F"}
|
|
@@ -36,10 +36,24 @@ export async function loadAudioResourceFromBytes(context, bytes, mimeType, signa
|
|
|
36
36
|
signal?.throwIfAborted();
|
|
37
37
|
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
38
38
|
const audioBuffer = await context.decodeAudioData(buffer);
|
|
39
|
+
// decodeAudioData cannot be cancelled, so an abort landing mid-decode cannot stop the work — but it
|
|
40
|
+
// must still stop the result, or an aborted load resolves with a resource the caller no longer wants
|
|
41
|
+
// and is indistinguishable from one it asked for. This is the whole family's abort barrier: every
|
|
42
|
+
// loader here funnels through this function, so checking after the await is what makes "an aborted
|
|
43
|
+
// load never resolves" true for all of them rather than only for the pre-abort fast path above.
|
|
44
|
+
signal?.throwIfAborted();
|
|
39
45
|
return createAudioResource(audioBuffer);
|
|
40
46
|
}
|
|
41
47
|
export async function loadAudioResourceFromUrl(context, url, signal) {
|
|
42
48
|
const response = await fetch(url, { signal });
|
|
49
|
+
// fetch only rejects on a network-level failure, so a 404/500 arrives here as a perfectly good
|
|
50
|
+
// response whose body is an error page. Decoding that reports "unable to decode audio data" — the
|
|
51
|
+
// wrong failure, blaming the codec for a transport problem, after paying for a decode that never had
|
|
52
|
+
// a chance. Reporting the status keeps this function's existing reject-on-failure contract; whether
|
|
53
|
+
// this family should reject at all or return the empty-resource sentinel is the separate parked
|
|
54
|
+
// question in the assessment Backlog.
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(`Failed to load audio: ${url} (${response.status} ${response.statusText})`);
|
|
43
57
|
const arrayBuffer = await response.arrayBuffer();
|
|
44
58
|
return loadAudioResourceFromBytes(context, new Uint8Array(arrayBuffer), response.headers.get('content-type') ?? undefined, signal);
|
|
45
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioResourceFrom.js","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audioResourceFrom.js","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,6FAA6F;AAC7F,gGAAgG;AAChG,gGAAgG;AAChG,yEAAyE;AACzE,MAAM,UAAU,8BAA8B,CAAC,QAAiC,EAAE,UAAkB;IAClG,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,gBAAgB,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,mBAAmB,EAAE,CAAC;IACzE,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAC,CAAC;IACzE,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;QAC5D,6FAA6F;QAC7F,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,CAA8B,EAAE,OAAO,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED,qGAAqG;AACrG,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,OAAqB,EACrB,MAAc,EACd,QAAgB,EAChB,MAAoB;IAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,0BAA0B,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAqB,EACrB,IAAU,EACV,MAAoB;IAEpB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAC7C,OAAO,0BAA0B,CAAC,OAAO,EAAE,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS,EAAE,MAAM,CAAC,CAAC;AAC1G,CAAC;AAED,iGAAiG;AACjG,+FAA+F;AAC/F,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,OAAqB,EACrB,KAAiB,EACjB,QAAiB,EACjB,MAAoB;IAEpB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1G,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAC1D,oGAAoG;IACpG,qGAAqG;IACrG,kGAAkG;IAClG,mGAAmG;IACnG,gGAAgG;IAChG,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,OAAO,mBAAmB,CAAC,WAAW,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAqB,EACrB,GAAW,EACX,MAAoB;IAEpB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9C,+FAA+F;IAC/F,kGAAkG;IAClG,qGAAqG;IACrG,oGAAoG;IACpG,gGAAgG;IAChG,sCAAsC;IACtC,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,KAAK,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,GAAG,CAAC,CAAC;IAC9G,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;IACjD,OAAO,0BAA0B,CAC/B,OAAO,EACP,IAAI,UAAU,CAAC,WAAW,CAAC,EAC3B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,SAAS,EACjD,MAAM,CACP,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAqB,EACrB,OAAoC,EACpC,MAAoB;IAEpB,MAAM,QAAQ,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,mBAAmB,EAAE,CAAC;IACpD,OAAO,wBAAwB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,iGAAiG;AACjG,oFAAoF;AACpF,MAAM,UAAU,sBAAsB,CAAC,OAAoC;IACzE,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACjE,IAAI,gBAAgB,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/audio",
|
|
3
|
-
"version": "0.3.0-next.
|
|
3
|
+
"version": "0.3.0-next.1220.6982c21",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@flighthq/types": "0.3.0-next.
|
|
39
|
+
"@flighthq/types": "0.3.0-next.1220.6982c21"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"typescript": "^5.3.0"
|
|
@@ -14,6 +14,17 @@ const mockContext = {
|
|
|
14
14
|
decodeAudioData: vi.fn().mockResolvedValue(decodedBuffer),
|
|
15
15
|
} as unknown as AudioContext;
|
|
16
16
|
|
|
17
|
+
// A context whose decode is held open, so a test can land an abort while the decode is still in
|
|
18
|
+
// flight — the window the pre-abort fast path cannot see. `finishDecode` then completes it, modelling
|
|
19
|
+
// the real decodeAudioData, which has no cancellation and so always runs to completion.
|
|
20
|
+
function createPendingDecodeContext(): { context: AudioContext; finishDecode: () => void } {
|
|
21
|
+
let release: (buffer: AudioBuffer) => void = () => {};
|
|
22
|
+
const context = {
|
|
23
|
+
decodeAudioData: vi.fn(() => new Promise<AudioBuffer>((resolve) => (release = resolve))),
|
|
24
|
+
} as unknown as AudioContext;
|
|
25
|
+
return { context, finishDecode: () => release(decodedBuffer) };
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
// jsdom lacks the AudioBuffer constructor; this minimal stand-in honours the { length,
|
|
18
29
|
// numberOfChannels, sampleRate } constructor plus copyToChannel/getChannelData used by
|
|
19
30
|
// createAudioResourceFromSamples.
|
|
@@ -78,6 +89,15 @@ describe('loadAudioResourceFromBase64', () => {
|
|
|
78
89
|
expect(resource.buffer).toBe(decodedBuffer);
|
|
79
90
|
expect(mockContext.decodeAudioData).toHaveBeenCalledOnce();
|
|
80
91
|
});
|
|
92
|
+
|
|
93
|
+
it('rejects when the signal aborts while the decode is in flight', async () => {
|
|
94
|
+
const { context, finishDecode } = createPendingDecodeContext();
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const promise = loadAudioResourceFromBase64(context, btoa('abc'), 'audio/mpeg', controller.signal);
|
|
97
|
+
controller.abort(new Error('cancelled'));
|
|
98
|
+
finishDecode();
|
|
99
|
+
await expect(promise).rejects.toThrow('cancelled');
|
|
100
|
+
});
|
|
81
101
|
});
|
|
82
102
|
|
|
83
103
|
describe('loadAudioResourceFromBlob', () => {
|
|
@@ -91,6 +111,20 @@ describe('loadAudioResourceFromBlob', () => {
|
|
|
91
111
|
expect(resource.buffer).toBe(decodedBuffer);
|
|
92
112
|
expect(mockContext.decodeAudioData).toHaveBeenCalledOnce();
|
|
93
113
|
});
|
|
114
|
+
|
|
115
|
+
it('rejects when the signal aborts while the decode is in flight', async () => {
|
|
116
|
+
const { context, finishDecode } = createPendingDecodeContext();
|
|
117
|
+
const blob = {
|
|
118
|
+
arrayBuffer: () => Promise.resolve(new Uint8Array([1, 2, 3, 4]).buffer),
|
|
119
|
+
type: 'audio/wav',
|
|
120
|
+
} as unknown as Blob;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const promise = loadAudioResourceFromBlob(context, blob, controller.signal);
|
|
123
|
+
await Promise.resolve();
|
|
124
|
+
controller.abort(new Error('cancelled'));
|
|
125
|
+
finishDecode();
|
|
126
|
+
await expect(promise).rejects.toThrow('cancelled');
|
|
127
|
+
});
|
|
94
128
|
});
|
|
95
129
|
|
|
96
130
|
describe('loadAudioResourceFromBytes', () => {
|
|
@@ -112,6 +146,31 @@ describe('loadAudioResourceFromBytes', () => {
|
|
|
112
146
|
loadAudioResourceFromBytes(mockContext, new Uint8Array([1, 2, 3, 4]), undefined, controller.signal),
|
|
113
147
|
).rejects.toThrow('cancelled');
|
|
114
148
|
});
|
|
149
|
+
|
|
150
|
+
// Every loader in this family funnels through here, and each is covered separately, because the
|
|
151
|
+
// guarantee is per entry point: a barrier that only holds for direct callers still lets the wrappers
|
|
152
|
+
// resolve past an abort.
|
|
153
|
+
it('rejects when the signal aborts while the decode is in flight', async () => {
|
|
154
|
+
const { context, finishDecode } = createPendingDecodeContext();
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const promise = loadAudioResourceFromBytes(context, new Uint8Array([1, 2, 3, 4]), undefined, controller.signal);
|
|
157
|
+
controller.abort(new Error('cancelled'));
|
|
158
|
+
finishDecode();
|
|
159
|
+
await expect(promise).rejects.toThrow('cancelled');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does not resolve with a decoded buffer after an abort', async () => {
|
|
163
|
+
const { context, finishDecode } = createPendingDecodeContext();
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const promise = loadAudioResourceFromBytes(context, new Uint8Array([1, 2, 3, 4]), undefined, controller.signal);
|
|
166
|
+
controller.abort(new Error('cancelled'));
|
|
167
|
+
finishDecode();
|
|
168
|
+
const settled = await promise.then(
|
|
169
|
+
(resource) => `resolved:${resource.buffer !== null}`,
|
|
170
|
+
() => 'rejected',
|
|
171
|
+
);
|
|
172
|
+
expect(settled).toBe('rejected');
|
|
173
|
+
});
|
|
115
174
|
});
|
|
116
175
|
|
|
117
176
|
describe('loadAudioResourceFromUrl', () => {
|
|
@@ -119,6 +178,7 @@ describe('loadAudioResourceFromUrl', () => {
|
|
|
119
178
|
vi.stubGlobal(
|
|
120
179
|
'fetch',
|
|
121
180
|
vi.fn().mockResolvedValue({
|
|
181
|
+
ok: true,
|
|
122
182
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
|
123
183
|
headers: new Headers({ 'content-type': 'audio/mpeg' }),
|
|
124
184
|
}),
|
|
@@ -126,6 +186,44 @@ describe('loadAudioResourceFromUrl', () => {
|
|
|
126
186
|
const resource = await loadAudioResourceFromUrl(mockContext, 'sound.mp3');
|
|
127
187
|
expect(resource.buffer).toBe(decodedBuffer);
|
|
128
188
|
});
|
|
189
|
+
|
|
190
|
+
// fetch resolves for 404/500, so without a status check the error page reaches the decoder and the
|
|
191
|
+
// caller is told the codec failed. The assertion pins the status, not merely that it rejected.
|
|
192
|
+
it('rejects with the HTTP status rather than decoding an error response', async () => {
|
|
193
|
+
const decodeAudioData = vi.fn().mockResolvedValue(decodedBuffer);
|
|
194
|
+
vi.stubGlobal(
|
|
195
|
+
'fetch',
|
|
196
|
+
vi.fn().mockResolvedValue({
|
|
197
|
+
ok: false,
|
|
198
|
+
status: 404,
|
|
199
|
+
statusText: 'Not Found',
|
|
200
|
+
arrayBuffer: () => Promise.resolve(new TextEncoder().encode('<html>404</html>').buffer),
|
|
201
|
+
headers: new Headers({ 'content-type': 'text/html' }),
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
await expect(
|
|
205
|
+
loadAudioResourceFromUrl({ decodeAudioData } as unknown as AudioContext, 'missing.mp3'),
|
|
206
|
+
).rejects.toThrow('Failed to load audio: missing.mp3 (404 Not Found)');
|
|
207
|
+
expect(decodeAudioData).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('rejects when the signal aborts while the decode is in flight', async () => {
|
|
211
|
+
const { context, finishDecode } = createPendingDecodeContext();
|
|
212
|
+
vi.stubGlobal(
|
|
213
|
+
'fetch',
|
|
214
|
+
vi.fn().mockResolvedValue({
|
|
215
|
+
ok: true,
|
|
216
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
|
217
|
+
headers: new Headers({ 'content-type': 'audio/mpeg' }),
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
const promise = loadAudioResourceFromUrl(context, 'sound.mp3', controller.signal);
|
|
222
|
+
await Promise.resolve();
|
|
223
|
+
controller.abort(new Error('cancelled'));
|
|
224
|
+
finishDecode();
|
|
225
|
+
await expect(promise).rejects.toThrow('cancelled');
|
|
226
|
+
});
|
|
129
227
|
});
|
|
130
228
|
|
|
131
229
|
describe('loadAudioResourceFromUrls', () => {
|
|
@@ -139,6 +237,7 @@ describe('loadAudioResourceFromUrls', () => {
|
|
|
139
237
|
type === 'audio/ogg' ? 'probably' : '',
|
|
140
238
|
);
|
|
141
239
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
240
|
+
ok: true,
|
|
142
241
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
|
143
242
|
headers: new Headers(),
|
|
144
243
|
});
|