audio-channel-queue 1.9.0 → 1.10.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>;
@@ -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);
@@ -88,7 +282,7 @@ const queueAudio = (audioUrl_1, ...args_1) => __awaiter(void 0, [audioUrl_1, ...
88
282
  // Use setTimeout to ensure the queue change event is emitted first
89
283
  setTimeout(() => {
90
284
  (0, exports.playAudioQueue)(channelNumber).catch((error) => {
91
- (0, errors_1.handleAudioError)(audio, channelNumber, audioUrl, error);
285
+ (0, errors_1.handleAudioError)(audio, channelNumber, validatedUrl, error);
92
286
  });
93
287
  }, 0);
94
288
  }
@@ -167,16 +361,9 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
167
361
  remainingInQueue: channel.queue.length - 1,
168
362
  src: currentAudio.src
169
363
  }, 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
364
  // Handle looping vs non-looping audio
178
365
  if (currentAudio.loop) {
179
- // For looping audio, reset current time and continue playing
366
+ // For looping audio, keep in queue and try to restart playback
180
367
  currentAudio.currentTime = 0;
181
368
  try {
182
369
  yield currentAudio.play();
@@ -188,9 +375,14 @@ const playAudioQueue = (channelNumber) => __awaiter(void 0, void 0, void 0, func
188
375
  }
189
376
  else {
190
377
  // For non-looping audio, remove from queue and play next
378
+ currentAudio.pause();
379
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
191
380
  channel.queue.shift();
192
- // Emit queue change after completion
193
- setTimeout(() => (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels), 10);
381
+ channel.isPaused = false; // Reset pause state
382
+ // Restore volume levels AFTER removing audio from queue
383
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
384
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
385
+ // Play next audio immediately if there's more in queue
194
386
  yield (0, exports.playAudioQueue)(channelNumber);
195
387
  resolve();
196
388
  }
@@ -230,16 +422,18 @@ const stopCurrentAudioInChannel = (...args_1) => __awaiter(void 0, [...args_1],
230
422
  remainingInQueue: channel.queue.length - 1,
231
423
  src: currentAudio.src
232
424
  }, info_1.audioChannels);
233
- // Restore volume levels when stopping
234
- yield (0, volume_1.restoreVolumeLevels)(channelNumber);
235
425
  currentAudio.pause();
236
426
  (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
237
427
  channel.queue.shift();
238
428
  channel.isPaused = false; // Reset pause state
429
+ // Restore volume levels AFTER removing from queue (so queue.length check works correctly)
430
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
239
431
  (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);
432
+ // Start next audio immediately if there's more in queue
433
+ if (channel.queue.length > 0) {
434
+ // eslint-disable-next-line no-console
435
+ (0, exports.playAudioQueue)(channelNumber).catch(console.error);
436
+ }
243
437
  }
244
438
  });
245
439
  exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
@@ -253,27 +447,29 @@ exports.stopCurrentAudioInChannel = stopCurrentAudioInChannel;
253
447
  * ```
254
448
  */
255
449
  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);
450
+ return withChannelLock(channelNumber, 'stopAllAudioInChannel', () => __awaiter(void 0, void 0, void 0, function* () {
451
+ const channel = info_1.audioChannels[channelNumber];
452
+ if (channel) {
453
+ if (channel.queue.length > 0) {
454
+ const currentAudio = channel.queue[0];
455
+ (0, events_1.emitAudioComplete)(channelNumber, {
456
+ channelNumber,
457
+ fileName: (0, utils_1.extractFileName)(currentAudio.src),
458
+ remainingInQueue: 0, // Will be 0 since we're clearing the queue
459
+ src: currentAudio.src
460
+ }, info_1.audioChannels);
461
+ // Restore volume levels when stopping
462
+ yield (0, volume_1.restoreVolumeLevels)(channelNumber);
463
+ currentAudio.pause();
464
+ (0, events_1.cleanupProgressTracking)(currentAudio, channelNumber, info_1.audioChannels);
465
+ }
466
+ // Clean up all progress tracking for this channel
467
+ channel.queue.forEach((audio) => (0, events_1.cleanupProgressTracking)(audio, channelNumber, info_1.audioChannels));
468
+ channel.queue = [];
469
+ channel.isPaused = false; // Reset pause state
470
+ (0, events_1.emitQueueChange)(channelNumber, info_1.audioChannels);
270
471
  }
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
- }
472
+ }));
277
473
  });
278
474
  exports.stopAllAudioInChannel = stopAllAudioInChannel;
279
475
  /**
@@ -291,3 +487,98 @@ const stopAllAudio = () => __awaiter(void 0, void 0, void 0, function* () {
291
487
  yield Promise.all(stopPromises);
292
488
  });
293
489
  exports.stopAllAudio = stopAllAudio;
490
+ /**
491
+ * Completely destroys a channel and cleans up all associated resources
492
+ * This stops all audio, cancels transitions, clears callbacks, and removes the channel
493
+ * @param channelNumber - The channel number to destroy (defaults to 0)
494
+ * @example
495
+ * ```typescript
496
+ * await destroyChannel(1); // Completely removes channel 1 and cleans up resources
497
+ * ```
498
+ */
499
+ const destroyChannel = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (channelNumber = 0) {
500
+ const channel = info_1.audioChannels[channelNumber];
501
+ if (!channel)
502
+ return;
503
+ // Comprehensive cleanup of all audio elements in the queue
504
+ if (channel.queue && channel.queue.length > 0) {
505
+ channel.queue.forEach((audio) => {
506
+ // Properly clean up each audio element
507
+ const cleanAudio = audio;
508
+ cleanAudio.pause();
509
+ cleanAudio.currentTime = 0;
510
+ // Remove all event listeners if possible
511
+ if (cleanAudio.parentNode) {
512
+ cleanAudio.parentNode.removeChild(cleanAudio);
513
+ }
514
+ // Clean up audio attributes
515
+ cleanAudio.removeAttribute('src');
516
+ // Reset audio element state
517
+ if (cleanAudio.src) {
518
+ // Copy essential properties
519
+ cleanAudio.src = '';
520
+ try {
521
+ cleanAudio.load();
522
+ }
523
+ catch (_a) {
524
+ // Ignore load errors in tests (jsdom limitation)
525
+ }
526
+ }
527
+ });
528
+ }
529
+ // Stop all audio in the channel (this handles additional cleanup)
530
+ yield (0, exports.stopAllAudioInChannel)(channelNumber);
531
+ // Cancel any active volume transitions
532
+ (0, volume_1.cancelVolumeTransition)(channelNumber);
533
+ // Clear all callback sets completely
534
+ const callbackProperties = [
535
+ 'audioCompleteCallbacks',
536
+ 'audioErrorCallbacks',
537
+ 'audioPauseCallbacks',
538
+ 'audioResumeCallbacks',
539
+ 'audioStartCallbacks',
540
+ 'queueChangeCallbacks',
541
+ 'progressCallbacks'
542
+ ];
543
+ callbackProperties.forEach((prop) => {
544
+ if (channel[prop]) {
545
+ channel[prop].clear();
546
+ }
547
+ });
548
+ // Remove optional channel configuration
549
+ delete channel.fadeState;
550
+ delete channel.retryConfig;
551
+ // Reset required properties to clean state
552
+ channel.isPaused = false;
553
+ channel.volume = 1.0;
554
+ channel.queue = [];
555
+ // Remove the channel completely
556
+ delete info_1.audioChannels[channelNumber];
557
+ });
558
+ exports.destroyChannel = destroyChannel;
559
+ /**
560
+ * Destroys all channels and cleans up all resources
561
+ * This is useful for complete cleanup when the audio system is no longer needed
562
+ * @example
563
+ * ```typescript
564
+ * await destroyAllChannels(); // Complete cleanup - removes all channels
565
+ * ```
566
+ */
567
+ const destroyAllChannels = () => __awaiter(void 0, void 0, void 0, function* () {
568
+ const destroyPromises = [];
569
+ // Collect indices of existing channels
570
+ const channelIndices = [];
571
+ info_1.audioChannels.forEach((_channel, index) => {
572
+ if (info_1.audioChannels[index]) {
573
+ channelIndices.push(index);
574
+ }
575
+ });
576
+ // Destroy all channels in parallel
577
+ channelIndices.forEach((index) => {
578
+ destroyPromises.push((0, exports.destroyChannel)(index));
579
+ });
580
+ yield Promise.all(destroyPromises);
581
+ // Clear the entire array
582
+ info_1.audioChannels.length = 0;
583
+ });
584
+ 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(),
package/dist/index.d.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * Exports all public functions and types for audio queue management, pause/resume controls,
4
4
  * volume management with ducking, progress tracking, and comprehensive event system
5
5
  */
6
- export { queueAudio, queueAudioPriority, stopCurrentAudioInChannel, stopAllAudioInChannel, stopAllAudio, playAudioQueue } from './core';
6
+ export { queueAudio, queueAudioPriority, stopCurrentAudioInChannel, stopAllAudioInChannel, stopAllAudio, playAudioQueue, destroyChannel, destroyAllChannels, setQueueConfig, getQueueConfig, setChannelQueueLimit } from './core';
7
+ export { clearQueueAfterCurrent, getQueueItemInfo, getQueueLength, removeQueuedItem, reorderQueue, swapQueueItems } from './queue-manipulation';
7
8
  export { getErrorRecovery, getRetryConfig, offAudioError, onAudioError, retryFailedAudio, setErrorRecovery, setRetryConfig } from './errors';
8
9
  export { getAllChannelsPauseState, isChannelPaused, pauseAllChannels, pauseAllWithFade, pauseChannel, pauseWithFade, resumeAllChannels, resumeAllWithFade, resumeChannel, resumeWithFade, togglePauseAllChannels, togglePauseAllWithFade, togglePauseChannel, togglePauseWithFade } from './pause';
9
- export { clearVolumeDucking, fadeVolume, getAllChannelsVolume, getChannelVolume, getFadeConfig, setAllChannelsVolume, setChannelVolume, setVolumeDucking, transitionVolume } from './volume';
10
+ export { clearVolumeDucking, fadeVolume, getAllChannelsVolume, getChannelVolume, getFadeConfig, setAllChannelsVolume, setChannelVolume, setVolumeDucking, transitionVolume, cancelVolumeTransition, cancelAllVolumeTransitions } from './volume';
10
11
  export { getAllChannelsInfo, getCurrentAudioInfo, getQueueSnapshot, offAudioPause, offAudioProgress, offAudioResume, offQueueChange, onAudioComplete, onAudioPause, onAudioProgress, onAudioResume, onAudioStart, onQueueChange } from './info';
11
12
  export { audioChannels } from './info';
12
- export { cleanWebpackFilename, createQueueSnapshot, extractFileName, getAudioInfoFromElement } from './utils';
13
- export type { AudioCompleteCallback, AudioCompleteInfo, AudioErrorCallback, AudioErrorInfo, AudioInfo, AudioPauseCallback, AudioQueueOptions, AudioResumeCallback, AudioStartCallback, AudioStartInfo, ChannelFadeState, ErrorRecoveryOptions, ExtendedAudioQueueChannel, FadeConfig, ProgressCallback, QueueChangeCallback, QueueItem, QueueSnapshot, RetryConfig, VolumeConfig } from './types';
14
- export { EasingType, FadeType, GLOBAL_PROGRESS_KEY } from './types';
13
+ export { cleanWebpackFilename, createQueueSnapshot, extractFileName, getAudioInfoFromElement, sanitizeForDisplay, validateAudioUrl } from './utils';
14
+ export type { AudioCompleteCallback, AudioCompleteInfo, AudioErrorCallback, AudioErrorInfo, AudioInfo, AudioPauseCallback, AudioQueueOptions, AudioResumeCallback, AudioStartCallback, AudioStartInfo, ChannelFadeState, ErrorRecoveryOptions, ExtendedAudioQueueChannel, FadeConfig, ProgressCallback, QueueChangeCallback, QueueItem, QueueManipulationResult, QueueSnapshot, RetryConfig, VolumeConfig, QueueConfig } from './types';
15
+ export { EasingType, FadeType, MAX_CHANNELS, TimerType, GLOBAL_PROGRESS_KEY } from './types';