ziplayer 0.3.12 → 0.3.13

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.
@@ -91,6 +91,7 @@ interface ManagerCacheEntry<T> {
91
91
  export class PlayerManager extends EventEmitter {
92
92
  private static instance: PlayerManager | null = null;
93
93
  private players: Map<string, Player> = new Map();
94
+ private pendingPlayers: Map<string, Promise<Player>> = new Map();
94
95
  private searchCache: Map<string, ManagerCacheEntry<SearchResult>>;
95
96
  private readonly SEARCH_CACHE_TTL = 60 * 1000; // 1 minute
96
97
  private readonly MAX_CACHE_SIZE = 100;
@@ -317,80 +318,95 @@ export class PlayerManager extends EventEmitter {
317
318
  return this.players.get(guildId)!;
318
319
  }
319
320
 
320
- this.debug(`Creating player for guildId: ${guildId}`);
321
- const player = new Player(guildId, options, this);
322
-
323
- // Add all registered plugins
324
- this.plugins.forEach((plugin) => player.addPlugin(plugin));
325
-
326
- // Activate extensions
327
- let extsToActivate: any[] = [];
328
- const optExts = (options as any)?.extensions as any[] | string[] | undefined;
329
-
330
- if (Array.isArray(optExts)) {
331
- if (optExts.length === 0) {
332
- extsToActivate = [];
333
- } else if (typeof optExts[0] === "string") {
334
- const wanted = new Set(optExts as string[]);
335
- extsToActivate = this.extensions.filter((ext) => {
336
- const name = typeof ext === "function" ? ext.name : ext?.name;
337
- return !!name && wanted.has(name);
338
- });
339
- } else {
340
- extsToActivate = optExts;
341
- }
342
- } else {
343
- // Use all extensions by default
344
- extsToActivate = this.extensions;
321
+ // Check if a player is already being created for this guild
322
+ if (this.pendingPlayers.has(guildId)) {
323
+ this.debug(`Player creation already in progress for guildId: ${guildId}, awaiting...`);
324
+ return this.pendingPlayers.get(guildId)!;
345
325
  }
346
326
 
347
- for (const ext of extsToActivate) {
348
- let instance = ext;
349
- if (typeof ext === "function") {
350
- try {
351
- instance = new ext(player);
352
- } catch (e) {
353
- this.debug(`Extension constructor error for ${ext.name}:`, e);
354
- continue;
327
+ const creationPromise = (async () => {
328
+ try {
329
+ this.debug(`Creating player for guildId: ${guildId}`);
330
+ const player = new Player(guildId, options, this);
331
+
332
+ // Add all registered plugins
333
+ this.plugins.forEach((plugin) => player.addPlugin(plugin));
334
+
335
+ // Activate extensions
336
+ let extsToActivate: any[] = [];
337
+ const optExts = (options as any)?.extensions as any[] | string[] | undefined;
338
+
339
+ if (Array.isArray(optExts)) {
340
+ if (optExts.length === 0) {
341
+ extsToActivate = [];
342
+ } else if (typeof optExts[0] === "string") {
343
+ const wanted = new Set(optExts as string[]);
344
+ extsToActivate = this.extensions.filter((ext) => {
345
+ const name = typeof ext === "function" ? ext.name : ext?.name;
346
+ return !!name && wanted.has(name);
347
+ });
348
+ } else {
349
+ extsToActivate = optExts;
350
+ }
351
+ } else {
352
+ // Use all extensions by default
353
+ extsToActivate = this.extensions;
355
354
  }
356
- }
357
355
 
358
- if (instance && typeof instance === "object") {
359
- const extInstance = instance as BaseExtension;
360
- if ("player" in extInstance && !extInstance.player) extInstance.player = player;
361
- player.attachExtension(extInstance);
362
-
363
- if (typeof extInstance.active === "function") {
364
- let activated: boolean | void = true;
365
- try {
366
- activated = await withTimeout(
367
- Promise.resolve(extInstance.active({ manager: this, player })),
368
- player.options.extractorTimeout ?? 15000,
369
- `Extension ${extInstance?.name} activation timed out`,
370
- );
371
- this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
372
- } catch (e) {
373
- activated = false;
374
- this.debug(`Extension activation error for ${extInstance?.name}:`, e);
356
+ for (const ext of extsToActivate) {
357
+ let instance = ext;
358
+ if (typeof ext === "function") {
359
+ try {
360
+ instance = new ext(player);
361
+ } catch (e) {
362
+ this.debug(`Extension constructor error for ${ext.name}:`, e);
363
+ continue;
364
+ }
375
365
  }
376
366
 
377
- if (activated === false) {
378
- player.detachExtension(extInstance);
379
- continue;
367
+ if (instance && typeof instance === "object") {
368
+ const extInstance = instance as BaseExtension;
369
+ if ("player" in extInstance && !extInstance.player) extInstance.player = player;
370
+ player.attachExtension(extInstance);
371
+
372
+ if (typeof extInstance.active === "function") {
373
+ let activated: boolean | void = true;
374
+ try {
375
+ activated = await withTimeout(
376
+ Promise.resolve(extInstance.active({ manager: this, player })),
377
+ player.options.extractorTimeout ?? 15000,
378
+ `Extension ${extInstance?.name} activation timed out`,
379
+ );
380
+ this.debug(`Extension ${extInstance?.name} active check returned: ${activated}`);
381
+ } catch (e) {
382
+ activated = false;
383
+ this.debug(`Extension activation error for ${extInstance?.name}:`, e);
384
+ }
385
+
386
+ if (activated === false) {
387
+ player.detachExtension(extInstance);
388
+ continue;
389
+ }
390
+ }
380
391
  }
381
392
  }
382
- }
383
- }
384
393
 
385
- // Forward all player events to manager
386
- this.setupEventForwarding(player, guildId);
394
+ // Forward all player events to manager
395
+ this.setupEventForwarding(player, guildId);
387
396
 
388
- // Mark last activity
389
- (player as any)._lastActivity = Date.now();
397
+ // Mark last activity
398
+ (player as any)._lastActivity = Date.now();
390
399
 
391
- this.players.set(guildId, player);
392
- this.debug(`Player created for guildId: ${guildId}`);
393
- return player;
400
+ this.players.set(guildId, player);
401
+ this.debug(`Player created for guildId: ${guildId}`);
402
+ return player;
403
+ } finally {
404
+ this.pendingPlayers.delete(guildId);
405
+ }
406
+ })();
407
+
408
+ this.pendingPlayers.set(guildId, creationPromise);
409
+ return creationPromise;
394
410
  }
395
411
 
396
412
  private setupEventForwarding(player: Player, guildId: string): void {
@@ -281,7 +281,7 @@ export class PreloadManager {
281
281
  signal.removeEventListener("abort", handler);
282
282
  reject(new Error("PRELOAD_CANCELLED"));
283
283
  };
284
- signal.addEventListener("abort", handler);
284
+ signal.addEventListener("abort", handler, { once: true });
285
285
  });
286
286
 
287
287
  const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
@@ -231,9 +231,9 @@ export class Queue {
231
231
  this.current = this.tracks.shift() || null;
232
232
  }
233
233
 
234
- // Skip bypassed track loop but no other track exists → restore current from history
234
+ // Skip bypassed track loop but no other track exists → trigger queue end
235
235
  if (!this.current && this._loop === "track" && ignoreLoop && this.history.length > 0) {
236
- this.current = this.history.pop() || null;
236
+ return null;
237
237
  }
238
238
 
239
239
  return this.current;
@@ -6,5 +6,12 @@
6
6
  * @returns Promise that rejects if timeout is reached
7
7
  */
8
8
  export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
9
- return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeoutMs))]);
9
+ let timeoutId: NodeJS.Timeout;
10
+ const timeoutPromise = new Promise<never>((_, reject) => {
11
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
12
+ });
13
+
14
+ return Promise.race([promise, timeoutPromise]).finally(() => {
15
+ if (timeoutId) clearTimeout(timeoutId);
16
+ });
10
17
  }