keeperboard 2.0.0 → 2.0.1

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/index.d.mts CHANGED
@@ -306,6 +306,7 @@ declare class KeeperBoardSession {
306
306
  private readonly defaultPlayerName;
307
307
  private readonly cache;
308
308
  private readonly retryQueue;
309
+ private cachedLimit;
309
310
  private isSubmitting;
310
311
  constructor(config: SessionConfig);
311
312
  /** Get or create a persistent player GUID. */
@@ -328,7 +329,8 @@ declare class KeeperBoardSession {
328
329
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
329
330
  * plus the current player's rank if they're outside the top N.
330
331
  *
331
- * Uses cache if enabled and fresh.
332
+ * Uses cache if enabled and fresh. If a larger limit is requested than
333
+ * what's cached, the cache is invalidated and fresh data is fetched.
332
334
  */
333
335
  getSnapshot(options?: {
334
336
  limit?: number;
@@ -427,11 +429,17 @@ declare function validateName(input: string, options?: NameValidationOptions): s
427
429
 
428
430
  /**
429
431
  * Generic TTL cache with in-flight deduplication and background refresh.
432
+ *
433
+ * Features:
434
+ * - TTL-based expiration
435
+ * - In-flight request deduplication
436
+ * - Background refresh scheduling (handles concurrent refresh requests)
430
437
  */
431
438
  declare class Cache<T> {
432
439
  private data;
433
440
  private fetchedAt;
434
441
  private inflight;
442
+ private pendingRefresh;
435
443
  private readonly ttlMs;
436
444
  constructor(ttlMs: number);
437
445
  /**
@@ -441,9 +449,11 @@ declare class Cache<T> {
441
449
  getOrFetch(fetchFn: () => Promise<T>): Promise<T>;
442
450
  /**
443
451
  * Trigger a background refresh without awaiting the result.
444
- * Returns immediately. If a fetch is already in flight, does nothing.
452
+ * Returns immediately. If a fetch is already in flight, schedules
453
+ * the refresh to run after the current one completes.
445
454
  */
446
455
  refreshInBackground(fetchFn: () => Promise<T>): void;
456
+ private startBackgroundFetch;
447
457
  /** Invalidate the cache, forcing the next getOrFetch to re-fetch. */
448
458
  invalidate(): void;
449
459
  /** Get the cached value without fetching. Returns undefined if empty or stale. */
package/dist/index.d.ts CHANGED
@@ -306,6 +306,7 @@ declare class KeeperBoardSession {
306
306
  private readonly defaultPlayerName;
307
307
  private readonly cache;
308
308
  private readonly retryQueue;
309
+ private cachedLimit;
309
310
  private isSubmitting;
310
311
  constructor(config: SessionConfig);
311
312
  /** Get or create a persistent player GUID. */
@@ -328,7 +329,8 @@ declare class KeeperBoardSession {
328
329
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
329
330
  * plus the current player's rank if they're outside the top N.
330
331
  *
331
- * Uses cache if enabled and fresh.
332
+ * Uses cache if enabled and fresh. If a larger limit is requested than
333
+ * what's cached, the cache is invalidated and fresh data is fetched.
332
334
  */
333
335
  getSnapshot(options?: {
334
336
  limit?: number;
@@ -427,11 +429,17 @@ declare function validateName(input: string, options?: NameValidationOptions): s
427
429
 
428
430
  /**
429
431
  * Generic TTL cache with in-flight deduplication and background refresh.
432
+ *
433
+ * Features:
434
+ * - TTL-based expiration
435
+ * - In-flight request deduplication
436
+ * - Background refresh scheduling (handles concurrent refresh requests)
430
437
  */
431
438
  declare class Cache<T> {
432
439
  private data;
433
440
  private fetchedAt;
434
441
  private inflight;
442
+ private pendingRefresh;
435
443
  private readonly ttlMs;
436
444
  constructor(ttlMs: number);
437
445
  /**
@@ -441,9 +449,11 @@ declare class Cache<T> {
441
449
  getOrFetch(fetchFn: () => Promise<T>): Promise<T>;
442
450
  /**
443
451
  * Trigger a background refresh without awaiting the result.
444
- * Returns immediately. If a fetch is already in flight, does nothing.
452
+ * Returns immediately. If a fetch is already in flight, schedules
453
+ * the refresh to run after the current one completes.
445
454
  */
446
455
  refreshInBackground(fetchFn: () => Promise<T>): void;
456
+ private startBackgroundFetch;
447
457
  /** Invalidate the cache, forcing the next getOrFetch to re-fetch. */
448
458
  invalidate(): void;
449
459
  /** Get the cached value without fetching. Returns undefined if empty or stale. */
package/dist/index.js CHANGED
@@ -354,6 +354,7 @@ var Cache = class {
354
354
  constructor(ttlMs) {
355
355
  this.fetchedAt = 0;
356
356
  this.inflight = null;
357
+ this.pendingRefresh = null;
357
358
  this.ttlMs = ttlMs;
358
359
  }
359
360
  /**
@@ -380,17 +381,30 @@ var Cache = class {
380
381
  }
381
382
  /**
382
383
  * Trigger a background refresh without awaiting the result.
383
- * Returns immediately. If a fetch is already in flight, does nothing.
384
+ * Returns immediately. If a fetch is already in flight, schedules
385
+ * the refresh to run after the current one completes.
384
386
  */
385
387
  refreshInBackground(fetchFn) {
386
- if (this.inflight) return;
388
+ if (this.inflight) {
389
+ this.pendingRefresh = fetchFn;
390
+ return;
391
+ }
392
+ this.startBackgroundFetch(fetchFn);
393
+ }
394
+ startBackgroundFetch(fetchFn) {
387
395
  this.inflight = fetchFn().then((result) => {
388
396
  this.data = result;
389
397
  this.fetchedAt = Date.now();
390
398
  this.inflight = null;
399
+ if (this.pendingRefresh) {
400
+ const pending = this.pendingRefresh;
401
+ this.pendingRefresh = null;
402
+ this.startBackgroundFetch(pending);
403
+ }
391
404
  return result;
392
405
  }).catch((err) => {
393
406
  this.inflight = null;
407
+ this.pendingRefresh = null;
394
408
  throw err;
395
409
  });
396
410
  this.inflight.catch(() => {
@@ -473,7 +487,8 @@ function validateName(input, options) {
473
487
  if (opts.uppercase) {
474
488
  name = name.toUpperCase();
475
489
  }
476
- name = name.replace(opts.allowedPattern, "");
490
+ const pattern = options?.allowedPattern ?? (opts.uppercase ? /[^A-Z0-9_]/g : /[^A-Za-z0-9_]/g);
491
+ name = name.replace(pattern, "");
477
492
  name = name.substring(0, opts.maxLength);
478
493
  if (name.length < opts.minLength) {
479
494
  return null;
@@ -484,6 +499,8 @@ function validateName(input, options) {
484
499
  // src/KeeperBoardSession.ts
485
500
  var KeeperBoardSession = class {
486
501
  constructor(config) {
502
+ this.cachedLimit = 0;
503
+ // Track the limit used for cached data
487
504
  this.isSubmitting = false;
488
505
  this.client = new KeeperBoardClient({
489
506
  apiKey: config.apiKey,
@@ -543,6 +560,7 @@ var KeeperBoardSession = class {
543
560
  this.retryQueue?.clear();
544
561
  if (this.cache) {
545
562
  this.cache.invalidate();
563
+ this.cachedLimit = 0;
546
564
  this.cache.refreshInBackground(() => this.fetchSnapshot());
547
565
  }
548
566
  return {
@@ -564,12 +582,18 @@ var KeeperBoardSession = class {
564
582
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
565
583
  * plus the current player's rank if they're outside the top N.
566
584
  *
567
- * Uses cache if enabled and fresh.
585
+ * Uses cache if enabled and fresh. If a larger limit is requested than
586
+ * what's cached, the cache is invalidated and fresh data is fetched.
568
587
  */
569
588
  async getSnapshot(options) {
570
589
  const limit = options?.limit ?? 10;
571
590
  if (this.cache) {
572
- return this.cache.getOrFetch(() => this.fetchSnapshot(limit));
591
+ if (limit > this.cachedLimit) {
592
+ this.cache.invalidate();
593
+ }
594
+ const result = await this.cache.getOrFetch(() => this.fetchSnapshot(limit));
595
+ this.cachedLimit = limit;
596
+ return result;
573
597
  }
574
598
  return this.fetchSnapshot(limit);
575
599
  }
@@ -584,7 +608,10 @@ var KeeperBoardSession = class {
584
608
  newName
585
609
  });
586
610
  this.identity.setPlayerName(newName);
587
- this.cache?.invalidate();
611
+ if (this.cache) {
612
+ this.cache.invalidate();
613
+ this.cachedLimit = 0;
614
+ }
588
615
  return true;
589
616
  } catch {
590
617
  return false;
package/dist/index.mjs CHANGED
@@ -322,6 +322,7 @@ var Cache = class {
322
322
  constructor(ttlMs) {
323
323
  this.fetchedAt = 0;
324
324
  this.inflight = null;
325
+ this.pendingRefresh = null;
325
326
  this.ttlMs = ttlMs;
326
327
  }
327
328
  /**
@@ -348,17 +349,30 @@ var Cache = class {
348
349
  }
349
350
  /**
350
351
  * Trigger a background refresh without awaiting the result.
351
- * Returns immediately. If a fetch is already in flight, does nothing.
352
+ * Returns immediately. If a fetch is already in flight, schedules
353
+ * the refresh to run after the current one completes.
352
354
  */
353
355
  refreshInBackground(fetchFn) {
354
- if (this.inflight) return;
356
+ if (this.inflight) {
357
+ this.pendingRefresh = fetchFn;
358
+ return;
359
+ }
360
+ this.startBackgroundFetch(fetchFn);
361
+ }
362
+ startBackgroundFetch(fetchFn) {
355
363
  this.inflight = fetchFn().then((result) => {
356
364
  this.data = result;
357
365
  this.fetchedAt = Date.now();
358
366
  this.inflight = null;
367
+ if (this.pendingRefresh) {
368
+ const pending = this.pendingRefresh;
369
+ this.pendingRefresh = null;
370
+ this.startBackgroundFetch(pending);
371
+ }
359
372
  return result;
360
373
  }).catch((err) => {
361
374
  this.inflight = null;
375
+ this.pendingRefresh = null;
362
376
  throw err;
363
377
  });
364
378
  this.inflight.catch(() => {
@@ -441,7 +455,8 @@ function validateName(input, options) {
441
455
  if (opts.uppercase) {
442
456
  name = name.toUpperCase();
443
457
  }
444
- name = name.replace(opts.allowedPattern, "");
458
+ const pattern = options?.allowedPattern ?? (opts.uppercase ? /[^A-Z0-9_]/g : /[^A-Za-z0-9_]/g);
459
+ name = name.replace(pattern, "");
445
460
  name = name.substring(0, opts.maxLength);
446
461
  if (name.length < opts.minLength) {
447
462
  return null;
@@ -452,6 +467,8 @@ function validateName(input, options) {
452
467
  // src/KeeperBoardSession.ts
453
468
  var KeeperBoardSession = class {
454
469
  constructor(config) {
470
+ this.cachedLimit = 0;
471
+ // Track the limit used for cached data
455
472
  this.isSubmitting = false;
456
473
  this.client = new KeeperBoardClient({
457
474
  apiKey: config.apiKey,
@@ -511,6 +528,7 @@ var KeeperBoardSession = class {
511
528
  this.retryQueue?.clear();
512
529
  if (this.cache) {
513
530
  this.cache.invalidate();
531
+ this.cachedLimit = 0;
514
532
  this.cache.refreshInBackground(() => this.fetchSnapshot());
515
533
  }
516
534
  return {
@@ -532,12 +550,18 @@ var KeeperBoardSession = class {
532
550
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
533
551
  * plus the current player's rank if they're outside the top N.
534
552
  *
535
- * Uses cache if enabled and fresh.
553
+ * Uses cache if enabled and fresh. If a larger limit is requested than
554
+ * what's cached, the cache is invalidated and fresh data is fetched.
536
555
  */
537
556
  async getSnapshot(options) {
538
557
  const limit = options?.limit ?? 10;
539
558
  if (this.cache) {
540
- return this.cache.getOrFetch(() => this.fetchSnapshot(limit));
559
+ if (limit > this.cachedLimit) {
560
+ this.cache.invalidate();
561
+ }
562
+ const result = await this.cache.getOrFetch(() => this.fetchSnapshot(limit));
563
+ this.cachedLimit = limit;
564
+ return result;
541
565
  }
542
566
  return this.fetchSnapshot(limit);
543
567
  }
@@ -552,7 +576,10 @@ var KeeperBoardSession = class {
552
576
  newName
553
577
  });
554
578
  this.identity.setPlayerName(newName);
555
- this.cache?.invalidate();
579
+ if (this.cache) {
580
+ this.cache.invalidate();
581
+ this.cachedLimit = 0;
582
+ }
556
583
  return true;
557
584
  } catch {
558
585
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keeperboard",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "TypeScript client SDK for KeeperBoard leaderboard-as-a-service",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",