audio-channel-queue 1.9.0 → 1.11.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.
package/dist/core.d.ts CHANGED
@@ -1,19 +1,58 @@
1
1
  /**
2
2
  * @fileoverview Core queue management functions for the audio-channel-queue package
3
3
  */
4
- import { AudioQueueOptions } from './types';
4
+ import { AudioQueueOptions, QueueConfig } from './types';
5
+ /**
6
+ * Sets the global queue configuration
7
+ * @param config - Queue configuration options
8
+ * @example
9
+ * ```typescript
10
+ * setQueueConfig({
11
+ * defaultMaxQueueSize: 50,
12
+ * dropOldestWhenFull: true,
13
+ * showQueueWarnings: true
14
+ * });
15
+ * ```
16
+ */
17
+ export declare const setQueueConfig: (config: Partial<QueueConfig>) => void;
18
+ /**
19
+ * Gets the current global queue configuration
20
+ * @returns Current queue configuration
21
+ * @example
22
+ * ```typescript
23
+ * const config = getQueueConfig();
24
+ * console.log(`Default max queue size: ${config.defaultMaxQueueSize}`);
25
+ * ```
26
+ */
27
+ export declare const getQueueConfig: () => QueueConfig;
28
+ /**
29
+ * Sets the maximum queue size for a specific channel
30
+ * @param channelNumber - The channel number to configure
31
+ * @param maxSize - Maximum queue size (undefined for unlimited)
32
+ * @throws Error if the channel number exceeds the maximum allowed channels
33
+ * @example
34
+ * ```typescript
35
+ * setChannelQueueLimit(0, 25); // Limit channel 0 to 25 items
36
+ * setChannelQueueLimit(1, undefined); // Remove limit for channel 1
37
+ * ```
38
+ */
39
+ export declare const setChannelQueueLimit: (channelNumber: number, maxSize?: number) => void;
5
40
  /**
6
41
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
7
42
  * @param audioUrl - The URL of the audio file to queue
8
43
  * @param channelNumber - The channel number to queue the audio to (defaults to 0)
9
44
  * @param options - Optional configuration for the audio file
10
45
  * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
46
+ * @throws Error if the audio URL is invalid or potentially malicious
47
+ * @throws Error if the channel number exceeds the maximum allowed channels
48
+ * @throws Error if the queue size limit would be exceeded
11
49
  * @example
12
50
  * ```typescript
13
51
  * await queueAudio('https://example.com/song.mp3', 0);
14
52
  * await queueAudio('./sounds/notification.wav'); // Uses default channel 0
15
53
  * await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
16
54
  * await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
55
+ * await queueAudio('./limited.mp3', 0, { maxQueueSize: 10 }); // Limit this queue to 10 items
17
56
  * ```
18
57
  */
19
58
  export declare const queueAudio: (audioUrl: string, channelNumber?: number, options?: AudioQueueOptions) => Promise<void>;
@@ -34,10 +73,10 @@ export declare const queueAudioPriority: (audioUrl: string, channelNumber?: numb
34
73
  /**
35
74
  * Plays the audio queue for a specific channel
36
75
  * @param channelNumber - The channel number to play
37
- * @returns Promise that resolves when the current audio finishes playing
76
+ * @returns Promise that resolves when the audio starts playing (setup complete)
38
77
  * @example
39
78
  * ```typescript
40
- * await playAudioQueue(0); // Play queue for channel 0
79
+ * await playAudioQueue(0); // Start playing queue for channel 0
41
80
  * ```
42
81
  */
43
82
  export declare const playAudioQueue: (channelNumber: number) => Promise<void>;
@@ -69,3 +108,22 @@ export declare const stopAllAudioInChannel: (channelNumber?: number) => Promise<
69
108
  * ```
70
109
  */
71
110
  export declare const stopAllAudio: () => Promise<void>;
111
+ /**
112
+ * Completely destroys a channel and cleans up all associated resources
113
+ * This stops all audio, cancels transitions, clears callbacks, and removes the channel
114
+ * @param channelNumber - The channel number to destroy (defaults to 0)
115
+ * @example
116
+ * ```typescript
117
+ * await destroyChannel(1); // Completely removes channel 1 and cleans up resources
118
+ * ```
119
+ */
120
+ export declare const destroyChannel: (channelNumber?: number) => Promise<void>;
121
+ /**
122
+ * Destroys all channels and cleans up all resources
123
+ * This is useful for complete cleanup when the audio system is no longer needed
124
+ * @example
125
+ * ```typescript
126
+ * await destroyAllChannels(); // Complete cleanup - removes all channels
127
+ * ```
128
+ */
129
+ export declare const destroyAllChannels: () => Promise<void>;
package/dist/core.js CHANGED
@@ -12,27 +12,213 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.playAudioQueue = exports.queueAudioPriority = exports.queueAudio = void 0;
15
+ exports.destroyAllChannels = exports.destroyChannel = exports.stopAllAudio = exports.stopAllAudioInChannel = exports.stopCurrentAudioInChannel = exports.playAudioQueue = exports.queueAudioPriority = exports.queueAudio = exports.setChannelQueueLimit = exports.getQueueConfig = exports.setQueueConfig = void 0;
16
+ const types_1 = require("./types");
16
17
  const info_1 = require("./info");
17
18
  const utils_1 = require("./utils");
18
19
  const events_1 = require("./events");
19
20
  const volume_1 = require("./volume");
20
21
  const errors_1 = require("./errors");
22
+ /**
23
+ * Global queue configuration
24
+ */
25
+ let globalQueueConfig = {
26
+ defaultMaxQueueSize: undefined, // unlimited by default
27
+ dropOldestWhenFull: false,
28
+ showQueueWarnings: true
29
+ };
30
+ /**
31
+ * Operation lock timeout in milliseconds
32
+ */
33
+ const OPERATION_LOCK_TIMEOUT = 100;
34
+ /**
35
+ * Acquires an operation lock for a channel to prevent race conditions
36
+ * @param channelNumber - The channel number to lock
37
+ * @param operationName - Name of the operation for debugging
38
+ * @returns Promise that resolves when lock is acquired
39
+ * @internal
40
+ */
41
+ const acquireChannelLock = (channelNumber, operationName) => __awaiter(void 0, void 0, void 0, function* () {
42
+ const channel = info_1.audioChannels[channelNumber];
43
+ if (!channel)
44
+ return;
45
+ const startTime = Date.now();
46
+ // Wait for any existing lock to be released
47
+ while (channel.isLocked) {
48
+ // Prevent infinite waiting with timeout
49
+ if (Date.now() - startTime > OPERATION_LOCK_TIMEOUT) {
50
+ // eslint-disable-next-line no-console
51
+ console.warn(`Operation lock timeout for channel ${channelNumber} during ${operationName}. ` +
52
+ `Forcibly acquiring lock.`);
53
+ break;
54
+ }
55
+ // Small delay to prevent tight polling
56
+ yield new Promise((resolve) => setTimeout(resolve, 10));
57
+ }
58
+ channel.isLocked = true;
59
+ });
60
+ /**
61
+ * Releases an operation lock for a channel
62
+ * @param channelNumber - The channel number to unlock
63
+ * @internal
64
+ */
65
+ const releaseChannelLock = (channelNumber) => {
66
+ const channel = info_1.audioChannels[channelNumber];
67
+ if (channel) {
68
+ channel.isLocked = false;
69
+ }
70
+ };
71
+ /**
72
+ * Executes an operation with channel lock protection
73
+ * @param channelNumber - The channel number to operate on
74
+ * @param operationName - Name of the operation for debugging
75
+ * @param operation - The operation to execute
76
+ * @returns Promise that resolves with the operation result
77
+ * @internal
78
+ */
79
+ const withChannelLock = (channelNumber, operationName, operation) => __awaiter(void 0, void 0, void 0, function* () {
80
+ try {
81
+ yield acquireChannelLock(channelNumber, operationName);
82
+ return yield operation();
83
+ }
84
+ finally {
85
+ releaseChannelLock(channelNumber);
86
+ }
87
+ });
88
+ /**
89
+ * Sets the global queue configuration
90
+ * @param config - Queue configuration options
91
+ * @example
92
+ * ```typescript
93
+ * setQueueConfig({
94
+ * defaultMaxQueueSize: 50,
95
+ * dropOldestWhenFull: true,
96
+ * showQueueWarnings: true
97
+ * });
98
+ * ```
99
+ */
100
+ const setQueueConfig = (config) => {
101
+ globalQueueConfig = Object.assign(Object.assign({}, globalQueueConfig), config);
102
+ };
103
+ exports.setQueueConfig = setQueueConfig;
104
+ /**
105
+ * Gets the current global queue configuration
106
+ * @returns Current queue configuration
107
+ * @example
108
+ * ```typescript
109
+ * const config = getQueueConfig();
110
+ * console.log(`Default max queue size: ${config.defaultMaxQueueSize}`);
111
+ * ```
112
+ */
113
+ const getQueueConfig = () => {
114
+ return Object.assign({}, globalQueueConfig);
115
+ };
116
+ exports.getQueueConfig = getQueueConfig;
117
+ /**
118
+ * Sets the maximum queue size for a specific channel
119
+ * @param channelNumber - The channel number to configure
120
+ * @param maxSize - Maximum queue size (undefined for unlimited)
121
+ * @throws Error if the channel number exceeds the maximum allowed channels
122
+ * @example
123
+ * ```typescript
124
+ * setChannelQueueLimit(0, 25); // Limit channel 0 to 25 items
125
+ * setChannelQueueLimit(1, undefined); // Remove limit for channel 1
126
+ * ```
127
+ */
128
+ const setChannelQueueLimit = (channelNumber, maxSize) => {
129
+ // Validate channel number limits BEFORE creating any channels
130
+ if (channelNumber < 0) {
131
+ throw new Error('Channel number must be non-negative');
132
+ }
133
+ if (channelNumber >= types_1.MAX_CHANNELS) {
134
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
135
+ }
136
+ // Ensure channel exists (now safe because we validated the limit above)
137
+ while (info_1.audioChannels.length <= channelNumber) {
138
+ info_1.audioChannels.push({
139
+ audioCompleteCallbacks: new Set(),
140
+ audioErrorCallbacks: new Set(),
141
+ audioPauseCallbacks: new Set(),
142
+ audioResumeCallbacks: new Set(),
143
+ audioStartCallbacks: new Set(),
144
+ isPaused: false,
145
+ progressCallbacks: new Map(),
146
+ queue: [],
147
+ queueChangeCallbacks: new Set(),
148
+ volume: 1.0
149
+ });
150
+ }
151
+ const channel = info_1.audioChannels[channelNumber];
152
+ channel.maxQueueSize = maxSize;
153
+ };
154
+ exports.setChannelQueueLimit = setChannelQueueLimit;
155
+ /**
156
+ * Checks if adding an item to the queue would exceed limits and handles the situation
157
+ * @param channel - The channel to check
158
+ * @param channelNumber - The channel number for logging
159
+ * @param maxQueueSize - Override max queue size from options
160
+ * @returns true if the item can be added, false otherwise
161
+ * @internal
162
+ */
163
+ const checkQueueLimit = (channel, channelNumber, maxQueueSize) => {
164
+ var _a;
165
+ // Determine the effective queue limit
166
+ const effectiveLimit = (_a = maxQueueSize !== null && maxQueueSize !== void 0 ? maxQueueSize : channel.maxQueueSize) !== null && _a !== void 0 ? _a : globalQueueConfig.defaultMaxQueueSize;
167
+ if (effectiveLimit === undefined) {
168
+ return true; // No limit set
169
+ }
170
+ if (channel.queue.length < effectiveLimit) {
171
+ return true; // Within limits
172
+ }
173
+ // Queue is at or over the limit
174
+ if (globalQueueConfig.showQueueWarnings) {
175
+ // eslint-disable-next-line no-console
176
+ console.warn(`Queue limit reached for channel ${channelNumber}. ` +
177
+ `Current size: ${channel.queue.length}, Limit: ${effectiveLimit}`);
178
+ }
179
+ if (globalQueueConfig.dropOldestWhenFull) {
180
+ // Remove oldest item (but not currently playing)
181
+ if (channel.queue.length > 1) {
182
+ const removedAudio = channel.queue.splice(1, 1)[0];
183
+ (0, events_1.cleanupProgressTracking)(removedAudio, channelNumber, info_1.audioChannels);
184
+ if (globalQueueConfig.showQueueWarnings) {
185
+ // eslint-disable-next-line no-console
186
+ console.info(`Dropped oldest queued item to make room for new audio`);
187
+ }
188
+ return true;
189
+ }
190
+ }
191
+ // Cannot add - queue is full and not dropping oldest
192
+ return false;
193
+ };
21
194
  /**
22
195
  * Queues an audio file to a specific channel and starts playing if it's the first in queue
23
196
  * @param audioUrl - The URL of the audio file to queue
24
197
  * @param channelNumber - The channel number to queue the audio to (defaults to 0)
25
198
  * @param options - Optional configuration for the audio file
26
199
  * @returns Promise that resolves when the audio is queued and starts playing (if first in queue)
200
+ * @throws Error if the audio URL is invalid or potentially malicious
201
+ * @throws Error if the channel number exceeds the maximum allowed channels
202
+ * @throws Error if the queue size limit would be exceeded
27
203
  * @example
28
204
  * ```typescript
29
205
  * await queueAudio('https://example.com/song.mp3', 0);
30
206
  * await queueAudio('./sounds/notification.wav'); // Uses default channel 0
31
207
  * await queueAudio('./music/loop.mp3', 1, { loop: true }); // Loop the audio
32
208
  * await queueAudio('./urgent.wav', 0, { addToFront: true }); // Add to front of queue
209
+ * await queueAudio('./limited.mp3', 0, { maxQueueSize: 10 }); // Limit this queue to 10 items
33
210
  * ```
34
211
  */
35
212
  const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...args_1], void 0, function* (audioUrl, channelNumber = 0, options) {
213
+ // Validate the URL for security
214
+ const validatedUrl = (0, utils_1.validateAudioUrl)(audioUrl);
215
+ // Check channel number limits
216
+ if (channelNumber < 0) {
217
+ throw new Error('Channel number must be non-negative');
218
+ }
219
+ if (channelNumber >= types_1.MAX_CHANNELS) {
220
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
221
+ }
36
222
  // Ensure the channel exists
37
223
  while (info_1.audioChannels.length <= channelNumber) {
38
224
  info_1.audioChannels.push({
@@ -49,10 +235,14 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
49
235
  });
50
236
  }
51
237
  const channel = info_1.audioChannels[channelNumber];
52
- const audio = new Audio(audioUrl);
238
+ // Check queue size limits before creating audio element
239
+ if (!checkQueueLimit(channel, channelNumber, options === null || options === void 0 ? void 0 : options.maxQueueSize)) {
240
+ throw new Error(`Queue size limit exceeded for channel ${channelNumber}`);
241
+ }
242
+ const audio = new Audio(validatedUrl);
53
243
  // Set up comprehensive error handling
54
- (0, errors_1.setupAudioErrorHandling)(audio, channelNumber, audioUrl, (error) => __awaiter(void 0, void 0, void 0, function* () {
55
- yield (0, errors_1.handleAudioError)(audio, channelNumber, audioUrl, error);
244
+ (0, errors_1.setupAudioErrorHandling)(audio, channelNumber, validatedUrl, (error) => __awaiter(void 0, void 0, void 0, function* () {
245
+ yield (0, errors_1.handleAudioError)(audio, channelNumber, validatedUrl, error);
56
246
  }));
57
247
  // Apply options if provided
58
248
  if (options) {
@@ -65,6 +255,10 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
65
255
  // Set channel volume to match the audio volume
66
256
  channel.volume = clampedVolume;
67
257
  }
258
+ // Set channel-specific queue limit if provided
259
+ if (typeof options.maxQueueSize === 'number') {
260
+ channel.maxQueueSize = options.maxQueueSize;
261
+ }
68
262
  }
69
263
  // Handle priority option (same as addToFront for backward compatibility)
70
264
  const shouldAddToFront = (options === null || options === void 0 ? void 0 : options.addToFront) || (options === null || options === void 0 ? void 0 : options.priority);
@@ -85,12 +279,8 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
85
279
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
86
280
  // Start playing if this is the first item and channel isn't paused
87
281
  if (channel.queue.length === 1 && !channel.isPaused) {
88
- // Use setTimeout to ensure the queue change event is emitted first
89
- setTimeout(() => {
90
- (0, exports.playAudioQueue)(channelNumber).catch((error) => {
91
- (0, errors_1.handleAudioError)(audio, channelNumber, audioUrl, error);
92
- });
93
- }, 0);
282
+ // Await the audio setup to complete before resolving queueAudio
283
+ yield (0, exports.playAudioQueue)(channelNumber);
94
284
  }
95
285
  });
96
286
  exports.queueAudio = queueAudio;
@@ -115,10 +305,10 @@ exports.queueAudioPriority = queueAudioPriority;
115
305
  /**
116
306
  * Plays the audio queue for a specific channel
117
307
  * @param channelNumber - The channel number to play
118
- * @returns Promise that resolves when the current audio finishes playing
308
+ * @returns Promise that resolves when the audio starts playing (setup complete)
119
309
  * @example
120
310
  * ```typescript
121
- * await playAudioQueue(0); // Play queue for channel 0
311
+ * await playAudioQueue(0); // Start playing queue for channel 0
122
312
  * ```
123
313
  */
124
314
  const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, function* () {
@@ -137,6 +327,7 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
137
327
  let hasStarted = false;
138
328
  let metadataLoaded = false;
139
329
  let playStarted = false;
330
+ let setupComplete = false;
140
331
  // Check if we should fire onAudioStart (both conditions met)
141
332
  const tryFireAudioStart = () => {
142
333
  if (!hasStarted && metadataLoaded && playStarted) {
@@ -147,6 +338,11 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
147
338
  fileName: (0, utils_1.extractFileName)(currentAudio.src),
148
339
  src: currentAudio.src
149
340
  }, info_1.audioChannels);
341
+ // Resolve setup promise when audio start event is fired
342
+ if (!setupComplete) {
343
+ setupComplete = true;
344
+ resolve();
345
+ }
150
346
  }
151
347
  };
152
348
  // Event handler for when metadata loads (duration becomes available)
@@ -167,16 +363,9 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
167
363
  remainingInQueue: channel.queue.length - 1,
168
364
  src: currentAudio.src
169
365
  }, info_1.audioChannels);
170
- // Restore volume levels when priority channel stops
171
- yield (0, volume_1.restoreVolumeLevels)(channelNumber);
172
- // Clean up event listeners
173
- currentAudio.removeEventListener('loadedmetadata', handleLoadedMetadata);
174
- currentAudio.removeEventListener('play', handlePlay);
175
- currentAudio.removeEventListener('ended', handleEnded);
176
- (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
177
366
  // Handle looping vs non-looping audio
178
367
  if (currentAudio.loop) {
179
- // For looping audio, reset current time and continue playing
368
+ // For looping audio, keep in queue and try to restart playback
180
369
  currentAudio.currentTime = 0;
181
370
  try {
182
371
  yield currentAudio.play();
@@ -184,15 +373,18 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
184
373
  catch (error) {
185
374
  yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
186
375
  }
187
- resolve();
188
376
  }
189
377
  else {
190
378
  // For non-looping audio, remove from queue and play next
379
+ currentAudio.pause();
380
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
191
381
  channel.queue.shift();
192
- // Emit queue change after completion
193
- setTimeout(() => (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels), 10);
382
+ channel.isPaused = false; // Reset pause state
383
+ // Restore volume levels AFTER removing audio from queue
384
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
385
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
386
+ // Play next audio immediately if there's more in queue
194
387
  yield (0, exports.playAudioQueue)(channelNumber);
195
- resolve();
196
388
  }
197
389
  });
198
390
  // Add event listeners
@@ -206,7 +398,10 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
206
398
  // Enhanced play with error handling
207
399
  currentAudio.play().catch((error) => __awaiter(void 0, void 0, void 0, function* () {
208
400
  yield (0, errors_1.handleAudioError)(currentAudio, channelNumber, currentAudio.src, error);
209
- resolve(); // Resolve to prevent hanging
401
+ if (!setupComplete) {
402
+ setupComplete = true;
403
+ resolve(); // Resolve gracefully instead of rejecting
404
+ }
210
405
  }));
211
406
  });
212
407
  });
@@ -230,16 +425,18 @@ const stopCurrentAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1],
230
425
  remainingInQueue: channel.queue.length - 1,
231
426
  src: currentAudio.src
232
427
  }, info_1.audioChannels);
233
- // Restore volume levels when stopping
234
- yield (0, volume_1.restoreVolumeLevels)(channelNumber);
235
428
  currentAudio.pause();
236
429
  (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
237
430
  channel.queue.shift();
238
431
  channel.isPaused = false; // Reset pause state
432
+ // Restore volume levels AFTER removing from queue (so queue.length check works correctly)
433
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
239
434
  (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
240
- // Start next audio without waiting for it to complete
241
- // eslint-disable-next-line no-console
242
- (0, exports.playAudioQueue)(channelNumber).catch(console.error);
435
+ // Start next audio immediately if there's more in queue
436
+ if (channel.queue.length > 0) {
437
+ // eslint-disable-next-line no-console
438
+ (0, exports.playAudioQueue)(channelNumber).catch(console.error);
439
+ }
243
440
  }
244
441
  });
245
442
  exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
@@ -253,27 +450,29 @@ exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
253
450
  * ```
254
451
  */
255
452
  const stopAllAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
256
- const channel = info_1.audioChannels[channelNumber];
257
- if (channel) {
258
- if (channel.queue.length > 0) {
259
- const currentAudio = channel.queue[0];
260
- (0, events_1.emitAudioComplete)(channelNumber, {
261
- channelNumber,
262
- fileName: (0, utils_1.extractFileName)(currentAudio.src),
263
- remainingInQueue: 0, // Will be 0 since we're clearing the queue
264
- src: currentAudio.src
265
- }, info_1.audioChannels);
266
- // Restore volume levels when stopping
267
- yield (0, volume_1.restoreVolumeLevels)(channelNumber);
268
- currentAudio.pause();
269
- (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
453
+ return withChannelLock(channelNumber, 'stopAllAudioInChannel', () => __awaiter(void 0, void 0, void 0, function* () {
454
+ const channel = info_1.audioChannels[channelNumber];
455
+ if (channel) {
456
+ if (channel.queue.length > 0) {
457
+ const currentAudio = channel.queue[0];
458
+ (0, events_1.emitAudioComplete)(channelNumber, {
459
+ channelNumber,
460
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
461
+ remainingInQueue: 0, // Will be 0 since we're clearing the queue
462
+ src: currentAudio.src
463
+ }, info_1.audioChannels);
464
+ // Restore volume levels when stopping
465
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
466
+ currentAudio.pause();
467
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
468
+ }
469
+ // Clean up all progress tracking for this channel
470
+ channel.queue.forEach((audio) => (0, events_1.cleanupProgressTracking)(audio, channelNumber, info_1.audioChannels));
471
+ channel.queue = [];
472
+ channel.isPaused = false; // Reset pause state
473
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
270
474
  }
271
- // Clean up all progress tracking for this channel
272
- channel.queue.forEach((audio) => (0, events_1.cleanupProgressTracking)(audio, channelNumber, info_1.audioChannels));
273
- channel.queue = [];
274
- channel.isPaused = false; // Reset pause state
275
- (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
276
- }
475
+ }));
277
476
  });
278
477
  exports.stopAllAudioInChannel = stopAllAudioInChannel;
279
478
  /**
@@ -291,3 +490,98 @@ const stopAllAudio = () => __awaiter(void 0, void 0, void 0, function* () {
291
490
  yield Promise.all(stopPromises);
292
491
  });
293
492
  exports.stopAllAudio = stopAllAudio;
493
+ /**
494
+ * Completely destroys a channel and cleans up all associated resources
495
+ * This stops all audio, cancels transitions, clears callbacks, and removes the channel
496
+ * @param channelNumber - The channel number to destroy (defaults to 0)
497
+ * @example
498
+ * ```typescript
499
+ * await destroyChannel(1); // Completely removes channel 1 and cleans up resources
500
+ * ```
501
+ */
502
+ const destroyChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
503
+ const channel = info_1.audioChannels[channelNumber];
504
+ if (!channel)
505
+ return;
506
+ // Comprehensive cleanup of all audio elements in the queue
507
+ if (channel.queue && channel.queue.length > 0) {
508
+ channel.queue.forEach((audio) => {
509
+ // Properly clean up each audio element
510
+ const cleanAudio = audio;
511
+ cleanAudio.pause();
512
+ cleanAudio.currentTime = 0;
513
+ // Remove all event listeners if possible
514
+ if (cleanAudio.parentNode) {
515
+ cleanAudio.parentNode.removeChild(cleanAudio);
516
+ }
517
+ // Clean up audio attributes
518
+ cleanAudio.removeAttribute('src');
519
+ // Reset audio element state
520
+ if (cleanAudio.src) {
521
+ // Copy essential properties
522
+ cleanAudio.src = '';
523
+ try {
524
+ cleanAudio.load();
525
+ }
526
+ catch (_a) {
527
+ // Ignore load errors in tests (jsdom limitation)
528
+ }
529
+ }
530
+ });
531
+ }
532
+ // Stop all audio in the channel (this handles additional cleanup)
533
+ yield (0, exports.stopAllAudioInChannel)(channelNumber);
534
+ // Cancel any active volume transitions
535
+ (0, volume_1.cancelVolumeTransition)(channelNumber);
536
+ // Clear all callback sets completely
537
+ const callbackProperties = [
538
+ 'audioCompleteCallbacks',
539
+ 'audioErrorCallbacks',
540
+ 'audioPauseCallbacks',
541
+ 'audioResumeCallbacks',
542
+ 'audioStartCallbacks',
543
+ 'queueChangeCallbacks',
544
+ 'progressCallbacks'
545
+ ];
546
+ callbackProperties.forEach((prop) => {
547
+ if (channel[prop]) {
548
+ channel[prop].clear();
549
+ }
550
+ });
551
+ // Remove optional channel configuration
552
+ delete channel.fadeState;
553
+ delete channel.retryConfig;
554
+ // Reset required properties to clean state
555
+ channel.isPaused = false;
556
+ channel.volume = 1.0;
557
+ channel.queue = [];
558
+ // Remove the channel completely
559
+ delete info_1.audioChannels[channelNumber];
560
+ });
561
+ exports.destroyChannel = destroyChannel;
562
+ /**
563
+ * Destroys all channels and cleans up all resources
564
+ * This is useful for complete cleanup when the audio system is no longer needed
565
+ * @example
566
+ * ```typescript
567
+ * await destroyAllChannels(); // Complete cleanup - removes all channels
568
+ * ```
569
+ */
570
+ const destroyAllChannels = () => __awaiter(void 0, void 0, void 0, function* () {
571
+ const destroyPromises = [];
572
+ // Collect indices of existing channels
573
+ const channelIndices = [];
574
+ info_1.audioChannels.forEach((_channel, index) => {
575
+ if (info_1.audioChannels[index]) {
576
+ channelIndices.push(index);
577
+ }
578
+ });
579
+ // Destroy all channels in parallel
580
+ channelIndices.forEach((index) => {
581
+ destroyPromises.push((0, exports.destroyChannel)(index));
582
+ });
583
+ yield Promise.all(destroyPromises);
584
+ // Clear the entire array
585
+ info_1.audioChannels.length = 0;
586
+ });
587
+ exports.destroyAllChannels = destroyAllChannels;
package/dist/errors.d.ts CHANGED
@@ -6,6 +6,7 @@ import { AudioErrorInfo, AudioErrorCallback, RetryConfig, ErrorRecoveryOptions,
6
6
  * Subscribes to audio error events for a specific channel
7
7
  * @param channelNumber - The channel number to listen to (defaults to 0)
8
8
  * @param callback - Function to call when an audio error occurs
9
+ * @throws Error if the channel number exceeds the maximum allowed channels
9
10
  * @example
10
11
  * ```typescript
11
12
  * onAudioError(0, (errorInfo) => {
package/dist/errors.js CHANGED
@@ -36,6 +36,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.createProtectedAudioElement = exports.handleAudioError = exports.setupAudioErrorHandling = exports.categorizeError = exports.emitAudioError = exports.retryFailedAudio = exports.getErrorRecovery = exports.setErrorRecovery = exports.getRetryConfig = exports.setRetryConfig = exports.offAudioError = exports.onAudioError = void 0;
39
+ const types_1 = require("./types");
39
40
  const info_1 = require("./info");
40
41
  const utils_1 = require("./utils");
41
42
  let globalRetryConfig = {
@@ -59,6 +60,7 @@ const loadTimeouts = new WeakMap();
59
60
  * Subscribes to audio error events for a specific channel
60
61
  * @param channelNumber - The channel number to listen to (defaults to 0)
61
62
  * @param callback - Function to call when an audio error occurs
63
+ * @throws Error if the channel number exceeds the maximum allowed channels
62
64
  * @example
63
65
  * ```typescript
64
66
  * onAudioError(0, (errorInfo) => {
@@ -68,7 +70,14 @@ const loadTimeouts = new WeakMap();
68
70
  * ```
69
71
  */
70
72
  const onAudioError = (channelNumber = 0, callback) => {
71
- // Ensure channel exists
73
+ // Validate channel number limits BEFORE creating any channels
74
+ if (channelNumber < 0) {
75
+ throw new Error('Channel number must be non-negative');
76
+ }
77
+ if (channelNumber >= types_1.MAX_CHANNELS) {
78
+ throw new Error(`Channel number ${channelNumber} exceeds maximum allowed channels (${types_1.MAX_CHANNELS})`);
79
+ }
80
+ // Ensure channel exists (now safe because we validated the limit above)
72
81
  while (info_1.audioChannels.length <= channelNumber) {
73
82
  info_1.audioChannels.push({
74
83
  audioCompleteCallbacks: new Set(),