keeperboard 2.0.0 → 2.0.2

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/README.md CHANGED
@@ -52,7 +52,6 @@ Most browser games should use `KeeperBoardSession`. Use `KeeperBoardClient` for
52
52
  const session = new KeeperBoardSession({
53
53
  apiKey: 'kb_dev_xxx', // Required
54
54
  leaderboard: 'main', // Required - session is bound to one board
55
- defaultPlayerName: 'ANON', // Optional (default: 'ANON')
56
55
  identity: { keyPrefix: 'app_' }, // Optional localStorage prefix
57
56
  cache: { ttlMs: 30000 }, // Optional TTL cache for getSnapshot()
58
57
  retry: { maxAgeMs: 86400000 }, // Optional retry queue for failed submissions
@@ -61,10 +60,13 @@ const session = new KeeperBoardSession({
61
60
 
62
61
  ### Identity (auto-managed)
63
62
 
63
+ Player names are auto-generated on first access (e.g., `BOLDFALCON`, `SWIFTPANDA`). Players can override with `setPlayerName()`.
64
+
64
65
  ```typescript
65
66
  session.getPlayerGuid(); // Get or create persistent GUID
66
- session.getPlayerName(); // Get stored name or default
67
+ session.getPlayerName(); // Get stored name (auto-generated if first time)
67
68
  session.setPlayerName(name); // Store name locally (doesn't update server)
69
+ session.hasExplicitPlayerName(); // true if player chose their name
68
70
 
69
71
  // Validate a name (pure function)
70
72
  const validated = session.validateName(' Ace Pilot! ');
@@ -271,6 +273,16 @@ class GameOverScene extends Phaser.Scene {
271
273
 
272
274
  ## Utilities
273
275
 
276
+ ### generatePlayerName
277
+
278
+ Generate random AdjectiveNoun player names:
279
+
280
+ ```typescript
281
+ import { generatePlayerName } from 'keeperboard';
282
+
283
+ const name = generatePlayerName(); // 'BOLDFALCON', 'SWIFTPANDA', etc.
284
+ ```
285
+
274
286
  ### PlayerIdentity
275
287
 
276
288
  Standalone helper for localStorage identity management:
package/dist/index.d.mts CHANGED
@@ -102,8 +102,6 @@ interface SessionConfig {
102
102
  identity?: {
103
103
  keyPrefix?: string;
104
104
  };
105
- /** Default player name when none has been set (default: "ANON") */
106
- defaultPlayerName?: string;
107
105
  /** TTL cache configuration for getSnapshot() */
108
106
  cache?: {
109
107
  ttlMs: number;
@@ -303,17 +301,19 @@ declare class KeeperBoardSession {
303
301
  private readonly client;
304
302
  private readonly identity;
305
303
  private readonly leaderboard;
306
- private readonly defaultPlayerName;
307
304
  private readonly cache;
308
305
  private readonly retryQueue;
306
+ private cachedLimit;
309
307
  private isSubmitting;
310
308
  constructor(config: SessionConfig);
311
309
  /** Get or create a persistent player GUID. */
312
310
  getPlayerGuid(): string;
313
- /** Get the stored player name, falling back to defaultPlayerName. */
311
+ /** Get the stored player name, auto-generating one if none exists. */
314
312
  getPlayerName(): string;
315
313
  /** Store a player name locally. Does NOT update the server — call updatePlayerName() for that. */
316
314
  setPlayerName(name: string): void;
315
+ /** Check if the player has explicitly set a name (vs auto-generated). */
316
+ hasExplicitPlayerName(): boolean;
317
317
  /** Validate a name using configurable rules. Returns sanitized string or null. */
318
318
  validateName(input: string, options?: NameValidationOptions): string | null;
319
319
  /**
@@ -328,7 +328,8 @@ declare class KeeperBoardSession {
328
328
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
329
329
  * plus the current player's rank if they're outside the top N.
330
330
  *
331
- * Uses cache if enabled and fresh.
331
+ * Uses cache if enabled and fresh. If a larger limit is requested than
332
+ * what's cached, the cache is invalidated and fresh data is fetched.
332
333
  */
333
334
  getSnapshot(options?: {
334
335
  limit?: number;
@@ -367,6 +368,7 @@ declare class PlayerIdentity {
367
368
  private readonly keyPrefix;
368
369
  private readonly guidKey;
369
370
  private readonly nameKey;
371
+ private readonly nameAutoKey;
370
372
  constructor(config?: PlayerIdentityConfig);
371
373
  /**
372
374
  * Get the stored player GUID, or null if none exists.
@@ -389,6 +391,15 @@ declare class PlayerIdentity {
389
391
  * Set the player name in localStorage.
390
392
  */
391
393
  setPlayerName(name: string): void;
394
+ /**
395
+ * Get the stored player name, creating an auto-generated one if it doesn't exist.
396
+ * Uses AdjectiveNounNumber pattern (e.g., ArcaneBlob99).
397
+ */
398
+ getOrCreatePlayerName(): string;
399
+ /**
400
+ * Check if the current player name was auto-generated (vs explicitly set by user).
401
+ */
402
+ isAutoGeneratedName(): boolean;
392
403
  /**
393
404
  * Clear all stored player identity data.
394
405
  */
@@ -425,13 +436,35 @@ declare class PlayerIdentity {
425
436
  */
426
437
  declare function validateName(input: string, options?: NameValidationOptions): string | null;
427
438
 
439
+ /**
440
+ * Auto-generated player name system.
441
+ * Generates random AdjectiveNounNumber names (e.g., ArcaneBlob99).
442
+ * Names are PascalCase words plus a numeric suffix, and fit within 4-12 characters.
443
+ */
444
+ /**
445
+ * Generate a random player name in the format AdjectiveNoun1-99.
446
+ * Returns a PascalCase string with length 4-12 characters (fits validateName rules).
447
+ * ~990,000 unique combinations possible.
448
+ *
449
+ * @example
450
+ * generatePlayerName() // 'ArcaneBlob99'
451
+ * generatePlayerName() // 'CosmicViper42'
452
+ */
453
+ declare function generatePlayerName(): string;
454
+
428
455
  /**
429
456
  * Generic TTL cache with in-flight deduplication and background refresh.
457
+ *
458
+ * Features:
459
+ * - TTL-based expiration
460
+ * - In-flight request deduplication
461
+ * - Background refresh scheduling (handles concurrent refresh requests)
430
462
  */
431
463
  declare class Cache<T> {
432
464
  private data;
433
465
  private fetchedAt;
434
466
  private inflight;
467
+ private pendingRefresh;
435
468
  private readonly ttlMs;
436
469
  constructor(ttlMs: number);
437
470
  /**
@@ -441,9 +474,11 @@ declare class Cache<T> {
441
474
  getOrFetch(fetchFn: () => Promise<T>): Promise<T>;
442
475
  /**
443
476
  * Trigger a background refresh without awaiting the result.
444
- * Returns immediately. If a fetch is already in flight, does nothing.
477
+ * Returns immediately. If a fetch is already in flight, schedules
478
+ * the refresh to run after the current one completes.
445
479
  */
446
480
  refreshInBackground(fetchFn: () => Promise<T>): void;
481
+ private startBackgroundFetch;
447
482
  /** Invalidate the cache, forcing the next getOrFetch to re-fetch. */
448
483
  invalidate(): void;
449
484
  /** Get the cached value without fetching. Returns undefined if empty or stale. */
@@ -478,4 +513,4 @@ declare class RetryQueue {
478
513
  clear(): void;
479
514
  }
480
515
 
481
- export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdatePlayerNameOptions, validateName };
516
+ export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdatePlayerNameOptions, generatePlayerName, validateName };
package/dist/index.d.ts CHANGED
@@ -102,8 +102,6 @@ interface SessionConfig {
102
102
  identity?: {
103
103
  keyPrefix?: string;
104
104
  };
105
- /** Default player name when none has been set (default: "ANON") */
106
- defaultPlayerName?: string;
107
105
  /** TTL cache configuration for getSnapshot() */
108
106
  cache?: {
109
107
  ttlMs: number;
@@ -303,17 +301,19 @@ declare class KeeperBoardSession {
303
301
  private readonly client;
304
302
  private readonly identity;
305
303
  private readonly leaderboard;
306
- private readonly defaultPlayerName;
307
304
  private readonly cache;
308
305
  private readonly retryQueue;
306
+ private cachedLimit;
309
307
  private isSubmitting;
310
308
  constructor(config: SessionConfig);
311
309
  /** Get or create a persistent player GUID. */
312
310
  getPlayerGuid(): string;
313
- /** Get the stored player name, falling back to defaultPlayerName. */
311
+ /** Get the stored player name, auto-generating one if none exists. */
314
312
  getPlayerName(): string;
315
313
  /** Store a player name locally. Does NOT update the server — call updatePlayerName() for that. */
316
314
  setPlayerName(name: string): void;
315
+ /** Check if the player has explicitly set a name (vs auto-generated). */
316
+ hasExplicitPlayerName(): boolean;
317
317
  /** Validate a name using configurable rules. Returns sanitized string or null. */
318
318
  validateName(input: string, options?: NameValidationOptions): string | null;
319
319
  /**
@@ -328,7 +328,8 @@ declare class KeeperBoardSession {
328
328
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
329
329
  * plus the current player's rank if they're outside the top N.
330
330
  *
331
- * Uses cache if enabled and fresh.
331
+ * Uses cache if enabled and fresh. If a larger limit is requested than
332
+ * what's cached, the cache is invalidated and fresh data is fetched.
332
333
  */
333
334
  getSnapshot(options?: {
334
335
  limit?: number;
@@ -367,6 +368,7 @@ declare class PlayerIdentity {
367
368
  private readonly keyPrefix;
368
369
  private readonly guidKey;
369
370
  private readonly nameKey;
371
+ private readonly nameAutoKey;
370
372
  constructor(config?: PlayerIdentityConfig);
371
373
  /**
372
374
  * Get the stored player GUID, or null if none exists.
@@ -389,6 +391,15 @@ declare class PlayerIdentity {
389
391
  * Set the player name in localStorage.
390
392
  */
391
393
  setPlayerName(name: string): void;
394
+ /**
395
+ * Get the stored player name, creating an auto-generated one if it doesn't exist.
396
+ * Uses AdjectiveNounNumber pattern (e.g., ArcaneBlob99).
397
+ */
398
+ getOrCreatePlayerName(): string;
399
+ /**
400
+ * Check if the current player name was auto-generated (vs explicitly set by user).
401
+ */
402
+ isAutoGeneratedName(): boolean;
392
403
  /**
393
404
  * Clear all stored player identity data.
394
405
  */
@@ -425,13 +436,35 @@ declare class PlayerIdentity {
425
436
  */
426
437
  declare function validateName(input: string, options?: NameValidationOptions): string | null;
427
438
 
439
+ /**
440
+ * Auto-generated player name system.
441
+ * Generates random AdjectiveNounNumber names (e.g., ArcaneBlob99).
442
+ * Names are PascalCase words plus a numeric suffix, and fit within 4-12 characters.
443
+ */
444
+ /**
445
+ * Generate a random player name in the format AdjectiveNoun1-99.
446
+ * Returns a PascalCase string with length 4-12 characters (fits validateName rules).
447
+ * ~990,000 unique combinations possible.
448
+ *
449
+ * @example
450
+ * generatePlayerName() // 'ArcaneBlob99'
451
+ * generatePlayerName() // 'CosmicViper42'
452
+ */
453
+ declare function generatePlayerName(): string;
454
+
428
455
  /**
429
456
  * Generic TTL cache with in-flight deduplication and background refresh.
457
+ *
458
+ * Features:
459
+ * - TTL-based expiration
460
+ * - In-flight request deduplication
461
+ * - Background refresh scheduling (handles concurrent refresh requests)
430
462
  */
431
463
  declare class Cache<T> {
432
464
  private data;
433
465
  private fetchedAt;
434
466
  private inflight;
467
+ private pendingRefresh;
435
468
  private readonly ttlMs;
436
469
  constructor(ttlMs: number);
437
470
  /**
@@ -441,9 +474,11 @@ declare class Cache<T> {
441
474
  getOrFetch(fetchFn: () => Promise<T>): Promise<T>;
442
475
  /**
443
476
  * Trigger a background refresh without awaiting the result.
444
- * Returns immediately. If a fetch is already in flight, does nothing.
477
+ * Returns immediately. If a fetch is already in flight, schedules
478
+ * the refresh to run after the current one completes.
445
479
  */
446
480
  refreshInBackground(fetchFn: () => Promise<T>): void;
481
+ private startBackgroundFetch;
447
482
  /** Invalidate the cache, forcing the next getOrFetch to re-fetch. */
448
483
  invalidate(): void;
449
484
  /** Get the cached value without fetching. Returns undefined if empty or stale. */
@@ -478,4 +513,4 @@ declare class RetryQueue {
478
513
  clear(): void;
479
514
  }
480
515
 
481
- export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdatePlayerNameOptions, validateName };
516
+ export { Cache, type ClaimResponse, type ClaimResult, type ClaimScoreOptions, type GetLeaderboardOptions, type GetPlayerRankOptions, type HealthResponse, type HealthResult, KeeperBoardClient, type KeeperBoardConfig, KeeperBoardError, KeeperBoardSession, type LeaderboardEntry, type LeaderboardResponse, type LeaderboardResult, type NameValidationOptions, PlayerIdentity, type PlayerIdentityConfig, type PlayerResponse, type PlayerResult, type ResetSchedule, RetryQueue, type ScoreResponse, type ScoreResult, type ScoreSubmission, type SessionConfig, type SessionScoreResult, type SnapshotEntry, type SnapshotResult, type SubmitScoreOptions, type UpdatePlayerNameOptions, generatePlayerName, validateName };
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  KeeperBoardSession: () => KeeperBoardSession,
27
27
  PlayerIdentity: () => PlayerIdentity,
28
28
  RetryQueue: () => RetryQueue,
29
+ generatePlayerName: () => generatePlayerName,
29
30
  validateName: () => validateName
30
31
  });
31
32
  module.exports = __toCommonJS(index_exports);
@@ -261,6 +262,220 @@ var _KeeperBoardClient = class _KeeperBoardClient {
261
262
  _KeeperBoardClient.DEFAULT_API_URL = "https://keeperboard.vercel.app";
262
263
  var KeeperBoardClient = _KeeperBoardClient;
263
264
 
265
+ // src/nameGenerator.ts
266
+ var MAX_BASE_LENGTH = 10;
267
+ var ADJECTIVES = [
268
+ "Arcane",
269
+ "Astro",
270
+ "Blazing",
271
+ "Bouncy",
272
+ "Brassy",
273
+ "Brisk",
274
+ "Bubbly",
275
+ "Chaotic",
276
+ "Cheeky",
277
+ "Chill",
278
+ "Chunky",
279
+ "Cloaked",
280
+ "Cosmic",
281
+ "Crimson",
282
+ "Crispy",
283
+ "Dashing",
284
+ "Dizzy",
285
+ "Dynamic",
286
+ "Electric",
287
+ "Epic",
288
+ "Feisty",
289
+ "Fiery",
290
+ "Flashy",
291
+ "Frosty",
292
+ "Funky",
293
+ "Furious",
294
+ "Galactic",
295
+ "Glitchy",
296
+ "Golden",
297
+ "Goofy",
298
+ "Gritty",
299
+ "Groovy",
300
+ "Hyper",
301
+ "Icy",
302
+ "Inky",
303
+ "Jazzy",
304
+ "Jolly",
305
+ "Jumpy",
306
+ "Laser",
307
+ "Legendary",
308
+ "Loud",
309
+ "Lucky",
310
+ "Lunar",
311
+ "Madcap",
312
+ "Magic",
313
+ "Majestic",
314
+ "Meteor",
315
+ "Mighty",
316
+ "Minty",
317
+ "Mystic",
318
+ "Neon",
319
+ "Nimble",
320
+ "Nova",
321
+ "Nuclear",
322
+ "Omega",
323
+ "Orbital",
324
+ "Peppy",
325
+ "Phantom",
326
+ "Pixel",
327
+ "Plasma",
328
+ "Polished",
329
+ "Primal",
330
+ "Quantum",
331
+ "Quick",
332
+ "Radiant",
333
+ "Rampaging",
334
+ "Razor",
335
+ "Rebel",
336
+ "Retro",
337
+ "Rogue",
338
+ "Rowdy",
339
+ "Savage",
340
+ "Shadow",
341
+ "Shiny",
342
+ "Silly",
343
+ "Sketchy",
344
+ "Skybound",
345
+ "Slick",
346
+ "Snappy",
347
+ "Solar",
348
+ "Sonic",
349
+ "Sparky",
350
+ "Speedy",
351
+ "Spiky",
352
+ "Starry",
353
+ "Stealthy",
354
+ "Stormy",
355
+ "Supreme",
356
+ "Swift",
357
+ "Thunder",
358
+ "Turbo",
359
+ "Twilight",
360
+ "Ultra",
361
+ "Vibrant",
362
+ "Warped",
363
+ "Wicked",
364
+ "Wild",
365
+ "Wizard",
366
+ "Zappy",
367
+ "Zesty"
368
+ ];
369
+ var NOUNS = [
370
+ "Aardvark",
371
+ "Asteroid",
372
+ "Badger",
373
+ "Bandit",
374
+ "Banshee",
375
+ "Beacon",
376
+ "Beetle",
377
+ "Blaster",
378
+ "Blob",
379
+ "Boomer",
380
+ "Bot",
381
+ "Brawler",
382
+ "Buccaneer",
383
+ "Buffalo",
384
+ "Cannon",
385
+ "Captain",
386
+ "Caribou",
387
+ "Charger",
388
+ "Cheetah",
389
+ "Chimera",
390
+ "Cobra",
391
+ "Comet",
392
+ "Cosmonaut",
393
+ "Cougar",
394
+ "Coyote",
395
+ "Cyborg",
396
+ "Dagger",
397
+ "Defender",
398
+ "Dino",
399
+ "Dragon",
400
+ "Drifter",
401
+ "Drone",
402
+ "Duck",
403
+ "Eagle",
404
+ "Eel",
405
+ "Falcon",
406
+ "Ferret",
407
+ "Fireball",
408
+ "Fox",
409
+ "Fury",
410
+ "Gazelle",
411
+ "Ghost",
412
+ "Gizmo",
413
+ "Gladiator",
414
+ "Goblin",
415
+ "Griffin",
416
+ "Hammer",
417
+ "Hawk",
418
+ "Hero",
419
+ "Hydra",
420
+ "Iguana",
421
+ "Jaguar",
422
+ "Jester",
423
+ "Jetpack",
424
+ "Jinx",
425
+ "Kangaroo",
426
+ "Katana",
427
+ "Kraken",
428
+ "Lancer",
429
+ "Laser",
430
+ "Legend",
431
+ "Lemur",
432
+ "Leopard",
433
+ "Lion",
434
+ "Luchador",
435
+ "Lynx",
436
+ "Maverick",
437
+ "Meteor",
438
+ "Monkey",
439
+ "Monsoon",
440
+ "Moose",
441
+ "Ninja",
442
+ "Nova",
443
+ "Octopus",
444
+ "Oracle",
445
+ "Otter",
446
+ "Panther",
447
+ "Phoenix",
448
+ "Pirate",
449
+ "Pixel",
450
+ "Puma",
451
+ "Quasar",
452
+ "Racer",
453
+ "Raptor",
454
+ "Raven",
455
+ "Reactor",
456
+ "Rocket",
457
+ "Ronin",
458
+ "Saber",
459
+ "Scorpion",
460
+ "Shark",
461
+ "Spartan",
462
+ "Sphinx",
463
+ "Sprinter",
464
+ "Stallion",
465
+ "Tiger",
466
+ "Titan",
467
+ "Viking",
468
+ "Viper",
469
+ "Wizard"
470
+ ];
471
+ function generatePlayerName() {
472
+ const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
473
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
474
+ const number = Math.floor(Math.random() * 99) + 1;
475
+ const base = (adjective + noun).slice(0, MAX_BASE_LENGTH);
476
+ return `${base}${number}`;
477
+ }
478
+
264
479
  // src/PlayerIdentity.ts
265
480
  var DEFAULT_KEY_PREFIX = "keeperboard_";
266
481
  var PlayerIdentity = class {
@@ -268,6 +483,7 @@ var PlayerIdentity = class {
268
483
  this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
269
484
  this.guidKey = `${this.keyPrefix}player_guid`;
270
485
  this.nameKey = `${this.keyPrefix}player_name`;
486
+ this.nameAutoKey = `${this.keyPrefix}player_name_auto`;
271
487
  }
272
488
  /**
273
489
  * Get the stored player GUID, or null if none exists.
@@ -316,6 +532,31 @@ var PlayerIdentity = class {
316
532
  return;
317
533
  }
318
534
  localStorage.setItem(this.nameKey, name);
535
+ localStorage.removeItem(this.nameAutoKey);
536
+ }
537
+ /**
538
+ * Get the stored player name, creating an auto-generated one if it doesn't exist.
539
+ * Uses AdjectiveNounNumber pattern (e.g., ArcaneBlob99).
540
+ */
541
+ getOrCreatePlayerName() {
542
+ let name = this.getPlayerName();
543
+ if (!name) {
544
+ name = generatePlayerName();
545
+ if (typeof window !== "undefined" && window.localStorage) {
546
+ localStorage.setItem(this.nameKey, name);
547
+ localStorage.setItem(this.nameAutoKey, "true");
548
+ }
549
+ }
550
+ return name;
551
+ }
552
+ /**
553
+ * Check if the current player name was auto-generated (vs explicitly set by user).
554
+ */
555
+ isAutoGeneratedName() {
556
+ if (typeof window === "undefined" || !window.localStorage) {
557
+ return false;
558
+ }
559
+ return localStorage.getItem(this.nameAutoKey) === "true";
319
560
  }
320
561
  /**
321
562
  * Clear all stored player identity data.
@@ -326,6 +567,7 @@ var PlayerIdentity = class {
326
567
  }
327
568
  localStorage.removeItem(this.guidKey);
328
569
  localStorage.removeItem(this.nameKey);
570
+ localStorage.removeItem(this.nameAutoKey);
329
571
  }
330
572
  /**
331
573
  * Check if player identity is stored.
@@ -354,6 +596,7 @@ var Cache = class {
354
596
  constructor(ttlMs) {
355
597
  this.fetchedAt = 0;
356
598
  this.inflight = null;
599
+ this.pendingRefresh = null;
357
600
  this.ttlMs = ttlMs;
358
601
  }
359
602
  /**
@@ -380,17 +623,30 @@ var Cache = class {
380
623
  }
381
624
  /**
382
625
  * Trigger a background refresh without awaiting the result.
383
- * Returns immediately. If a fetch is already in flight, does nothing.
626
+ * Returns immediately. If a fetch is already in flight, schedules
627
+ * the refresh to run after the current one completes.
384
628
  */
385
629
  refreshInBackground(fetchFn) {
386
- if (this.inflight) return;
630
+ if (this.inflight) {
631
+ this.pendingRefresh = fetchFn;
632
+ return;
633
+ }
634
+ this.startBackgroundFetch(fetchFn);
635
+ }
636
+ startBackgroundFetch(fetchFn) {
387
637
  this.inflight = fetchFn().then((result) => {
388
638
  this.data = result;
389
639
  this.fetchedAt = Date.now();
390
640
  this.inflight = null;
641
+ if (this.pendingRefresh) {
642
+ const pending = this.pendingRefresh;
643
+ this.pendingRefresh = null;
644
+ this.startBackgroundFetch(pending);
645
+ }
391
646
  return result;
392
647
  }).catch((err) => {
393
648
  this.inflight = null;
649
+ this.pendingRefresh = null;
394
650
  throw err;
395
651
  });
396
652
  this.inflight.catch(() => {
@@ -473,7 +729,8 @@ function validateName(input, options) {
473
729
  if (opts.uppercase) {
474
730
  name = name.toUpperCase();
475
731
  }
476
- name = name.replace(opts.allowedPattern, "");
732
+ const pattern = options?.allowedPattern ?? (opts.uppercase ? /[^A-Z0-9_]/g : /[^A-Za-z0-9_]/g);
733
+ name = name.replace(pattern, "");
477
734
  name = name.substring(0, opts.maxLength);
478
735
  if (name.length < opts.minLength) {
479
736
  return null;
@@ -484,6 +741,8 @@ function validateName(input, options) {
484
741
  // src/KeeperBoardSession.ts
485
742
  var KeeperBoardSession = class {
486
743
  constructor(config) {
744
+ this.cachedLimit = 0;
745
+ // Track the limit used for cached data
487
746
  this.isSubmitting = false;
488
747
  this.client = new KeeperBoardClient({
489
748
  apiKey: config.apiKey,
@@ -492,7 +751,6 @@ var KeeperBoardSession = class {
492
751
  });
493
752
  this.identity = new PlayerIdentity(config.identity);
494
753
  this.leaderboard = config.leaderboard;
495
- this.defaultPlayerName = config.defaultPlayerName ?? "ANON";
496
754
  this.cache = config.cache ? new Cache(config.cache.ttlMs) : null;
497
755
  this.retryQueue = config.retry ? new RetryQueue(
498
756
  `keeperboard_retry_${config.leaderboard}`,
@@ -506,14 +764,18 @@ var KeeperBoardSession = class {
506
764
  getPlayerGuid() {
507
765
  return this.identity.getOrCreatePlayerGuid();
508
766
  }
509
- /** Get the stored player name, falling back to defaultPlayerName. */
767
+ /** Get the stored player name, auto-generating one if none exists. */
510
768
  getPlayerName() {
511
- return this.identity.getPlayerName() ?? this.defaultPlayerName;
769
+ return this.identity.getOrCreatePlayerName();
512
770
  }
513
771
  /** Store a player name locally. Does NOT update the server — call updatePlayerName() for that. */
514
772
  setPlayerName(name) {
515
773
  this.identity.setPlayerName(name);
516
774
  }
775
+ /** Check if the player has explicitly set a name (vs auto-generated). */
776
+ hasExplicitPlayerName() {
777
+ return this.identity.getPlayerName() !== null && !this.identity.isAutoGeneratedName();
778
+ }
517
779
  /** Validate a name using configurable rules. Returns sanitized string or null. */
518
780
  validateName(input, options) {
519
781
  return validateName(input, options);
@@ -543,6 +805,7 @@ var KeeperBoardSession = class {
543
805
  this.retryQueue?.clear();
544
806
  if (this.cache) {
545
807
  this.cache.invalidate();
808
+ this.cachedLimit = 0;
546
809
  this.cache.refreshInBackground(() => this.fetchSnapshot());
547
810
  }
548
811
  return {
@@ -564,12 +827,18 @@ var KeeperBoardSession = class {
564
827
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
565
828
  * plus the current player's rank if they're outside the top N.
566
829
  *
567
- * Uses cache if enabled and fresh.
830
+ * Uses cache if enabled and fresh. If a larger limit is requested than
831
+ * what's cached, the cache is invalidated and fresh data is fetched.
568
832
  */
569
833
  async getSnapshot(options) {
570
834
  const limit = options?.limit ?? 10;
571
835
  if (this.cache) {
572
- return this.cache.getOrFetch(() => this.fetchSnapshot(limit));
836
+ if (limit > this.cachedLimit) {
837
+ this.cache.invalidate();
838
+ }
839
+ const result = await this.cache.getOrFetch(() => this.fetchSnapshot(limit));
840
+ this.cachedLimit = limit;
841
+ return result;
573
842
  }
574
843
  return this.fetchSnapshot(limit);
575
844
  }
@@ -584,7 +853,10 @@ var KeeperBoardSession = class {
584
853
  newName
585
854
  });
586
855
  this.identity.setPlayerName(newName);
587
- this.cache?.invalidate();
856
+ if (this.cache) {
857
+ this.cache.invalidate();
858
+ this.cachedLimit = 0;
859
+ }
588
860
  return true;
589
861
  } catch {
590
862
  return false;
@@ -653,5 +925,6 @@ var KeeperBoardSession = class {
653
925
  KeeperBoardSession,
654
926
  PlayerIdentity,
655
927
  RetryQueue,
928
+ generatePlayerName,
656
929
  validateName
657
930
  });
package/dist/index.mjs CHANGED
@@ -229,6 +229,220 @@ var _KeeperBoardClient = class _KeeperBoardClient {
229
229
  _KeeperBoardClient.DEFAULT_API_URL = "https://keeperboard.vercel.app";
230
230
  var KeeperBoardClient = _KeeperBoardClient;
231
231
 
232
+ // src/nameGenerator.ts
233
+ var MAX_BASE_LENGTH = 10;
234
+ var ADJECTIVES = [
235
+ "Arcane",
236
+ "Astro",
237
+ "Blazing",
238
+ "Bouncy",
239
+ "Brassy",
240
+ "Brisk",
241
+ "Bubbly",
242
+ "Chaotic",
243
+ "Cheeky",
244
+ "Chill",
245
+ "Chunky",
246
+ "Cloaked",
247
+ "Cosmic",
248
+ "Crimson",
249
+ "Crispy",
250
+ "Dashing",
251
+ "Dizzy",
252
+ "Dynamic",
253
+ "Electric",
254
+ "Epic",
255
+ "Feisty",
256
+ "Fiery",
257
+ "Flashy",
258
+ "Frosty",
259
+ "Funky",
260
+ "Furious",
261
+ "Galactic",
262
+ "Glitchy",
263
+ "Golden",
264
+ "Goofy",
265
+ "Gritty",
266
+ "Groovy",
267
+ "Hyper",
268
+ "Icy",
269
+ "Inky",
270
+ "Jazzy",
271
+ "Jolly",
272
+ "Jumpy",
273
+ "Laser",
274
+ "Legendary",
275
+ "Loud",
276
+ "Lucky",
277
+ "Lunar",
278
+ "Madcap",
279
+ "Magic",
280
+ "Majestic",
281
+ "Meteor",
282
+ "Mighty",
283
+ "Minty",
284
+ "Mystic",
285
+ "Neon",
286
+ "Nimble",
287
+ "Nova",
288
+ "Nuclear",
289
+ "Omega",
290
+ "Orbital",
291
+ "Peppy",
292
+ "Phantom",
293
+ "Pixel",
294
+ "Plasma",
295
+ "Polished",
296
+ "Primal",
297
+ "Quantum",
298
+ "Quick",
299
+ "Radiant",
300
+ "Rampaging",
301
+ "Razor",
302
+ "Rebel",
303
+ "Retro",
304
+ "Rogue",
305
+ "Rowdy",
306
+ "Savage",
307
+ "Shadow",
308
+ "Shiny",
309
+ "Silly",
310
+ "Sketchy",
311
+ "Skybound",
312
+ "Slick",
313
+ "Snappy",
314
+ "Solar",
315
+ "Sonic",
316
+ "Sparky",
317
+ "Speedy",
318
+ "Spiky",
319
+ "Starry",
320
+ "Stealthy",
321
+ "Stormy",
322
+ "Supreme",
323
+ "Swift",
324
+ "Thunder",
325
+ "Turbo",
326
+ "Twilight",
327
+ "Ultra",
328
+ "Vibrant",
329
+ "Warped",
330
+ "Wicked",
331
+ "Wild",
332
+ "Wizard",
333
+ "Zappy",
334
+ "Zesty"
335
+ ];
336
+ var NOUNS = [
337
+ "Aardvark",
338
+ "Asteroid",
339
+ "Badger",
340
+ "Bandit",
341
+ "Banshee",
342
+ "Beacon",
343
+ "Beetle",
344
+ "Blaster",
345
+ "Blob",
346
+ "Boomer",
347
+ "Bot",
348
+ "Brawler",
349
+ "Buccaneer",
350
+ "Buffalo",
351
+ "Cannon",
352
+ "Captain",
353
+ "Caribou",
354
+ "Charger",
355
+ "Cheetah",
356
+ "Chimera",
357
+ "Cobra",
358
+ "Comet",
359
+ "Cosmonaut",
360
+ "Cougar",
361
+ "Coyote",
362
+ "Cyborg",
363
+ "Dagger",
364
+ "Defender",
365
+ "Dino",
366
+ "Dragon",
367
+ "Drifter",
368
+ "Drone",
369
+ "Duck",
370
+ "Eagle",
371
+ "Eel",
372
+ "Falcon",
373
+ "Ferret",
374
+ "Fireball",
375
+ "Fox",
376
+ "Fury",
377
+ "Gazelle",
378
+ "Ghost",
379
+ "Gizmo",
380
+ "Gladiator",
381
+ "Goblin",
382
+ "Griffin",
383
+ "Hammer",
384
+ "Hawk",
385
+ "Hero",
386
+ "Hydra",
387
+ "Iguana",
388
+ "Jaguar",
389
+ "Jester",
390
+ "Jetpack",
391
+ "Jinx",
392
+ "Kangaroo",
393
+ "Katana",
394
+ "Kraken",
395
+ "Lancer",
396
+ "Laser",
397
+ "Legend",
398
+ "Lemur",
399
+ "Leopard",
400
+ "Lion",
401
+ "Luchador",
402
+ "Lynx",
403
+ "Maverick",
404
+ "Meteor",
405
+ "Monkey",
406
+ "Monsoon",
407
+ "Moose",
408
+ "Ninja",
409
+ "Nova",
410
+ "Octopus",
411
+ "Oracle",
412
+ "Otter",
413
+ "Panther",
414
+ "Phoenix",
415
+ "Pirate",
416
+ "Pixel",
417
+ "Puma",
418
+ "Quasar",
419
+ "Racer",
420
+ "Raptor",
421
+ "Raven",
422
+ "Reactor",
423
+ "Rocket",
424
+ "Ronin",
425
+ "Saber",
426
+ "Scorpion",
427
+ "Shark",
428
+ "Spartan",
429
+ "Sphinx",
430
+ "Sprinter",
431
+ "Stallion",
432
+ "Tiger",
433
+ "Titan",
434
+ "Viking",
435
+ "Viper",
436
+ "Wizard"
437
+ ];
438
+ function generatePlayerName() {
439
+ const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
440
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
441
+ const number = Math.floor(Math.random() * 99) + 1;
442
+ const base = (adjective + noun).slice(0, MAX_BASE_LENGTH);
443
+ return `${base}${number}`;
444
+ }
445
+
232
446
  // src/PlayerIdentity.ts
233
447
  var DEFAULT_KEY_PREFIX = "keeperboard_";
234
448
  var PlayerIdentity = class {
@@ -236,6 +450,7 @@ var PlayerIdentity = class {
236
450
  this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
237
451
  this.guidKey = `${this.keyPrefix}player_guid`;
238
452
  this.nameKey = `${this.keyPrefix}player_name`;
453
+ this.nameAutoKey = `${this.keyPrefix}player_name_auto`;
239
454
  }
240
455
  /**
241
456
  * Get the stored player GUID, or null if none exists.
@@ -284,6 +499,31 @@ var PlayerIdentity = class {
284
499
  return;
285
500
  }
286
501
  localStorage.setItem(this.nameKey, name);
502
+ localStorage.removeItem(this.nameAutoKey);
503
+ }
504
+ /**
505
+ * Get the stored player name, creating an auto-generated one if it doesn't exist.
506
+ * Uses AdjectiveNounNumber pattern (e.g., ArcaneBlob99).
507
+ */
508
+ getOrCreatePlayerName() {
509
+ let name = this.getPlayerName();
510
+ if (!name) {
511
+ name = generatePlayerName();
512
+ if (typeof window !== "undefined" && window.localStorage) {
513
+ localStorage.setItem(this.nameKey, name);
514
+ localStorage.setItem(this.nameAutoKey, "true");
515
+ }
516
+ }
517
+ return name;
518
+ }
519
+ /**
520
+ * Check if the current player name was auto-generated (vs explicitly set by user).
521
+ */
522
+ isAutoGeneratedName() {
523
+ if (typeof window === "undefined" || !window.localStorage) {
524
+ return false;
525
+ }
526
+ return localStorage.getItem(this.nameAutoKey) === "true";
287
527
  }
288
528
  /**
289
529
  * Clear all stored player identity data.
@@ -294,6 +534,7 @@ var PlayerIdentity = class {
294
534
  }
295
535
  localStorage.removeItem(this.guidKey);
296
536
  localStorage.removeItem(this.nameKey);
537
+ localStorage.removeItem(this.nameAutoKey);
297
538
  }
298
539
  /**
299
540
  * Check if player identity is stored.
@@ -322,6 +563,7 @@ var Cache = class {
322
563
  constructor(ttlMs) {
323
564
  this.fetchedAt = 0;
324
565
  this.inflight = null;
566
+ this.pendingRefresh = null;
325
567
  this.ttlMs = ttlMs;
326
568
  }
327
569
  /**
@@ -348,17 +590,30 @@ var Cache = class {
348
590
  }
349
591
  /**
350
592
  * Trigger a background refresh without awaiting the result.
351
- * Returns immediately. If a fetch is already in flight, does nothing.
593
+ * Returns immediately. If a fetch is already in flight, schedules
594
+ * the refresh to run after the current one completes.
352
595
  */
353
596
  refreshInBackground(fetchFn) {
354
- if (this.inflight) return;
597
+ if (this.inflight) {
598
+ this.pendingRefresh = fetchFn;
599
+ return;
600
+ }
601
+ this.startBackgroundFetch(fetchFn);
602
+ }
603
+ startBackgroundFetch(fetchFn) {
355
604
  this.inflight = fetchFn().then((result) => {
356
605
  this.data = result;
357
606
  this.fetchedAt = Date.now();
358
607
  this.inflight = null;
608
+ if (this.pendingRefresh) {
609
+ const pending = this.pendingRefresh;
610
+ this.pendingRefresh = null;
611
+ this.startBackgroundFetch(pending);
612
+ }
359
613
  return result;
360
614
  }).catch((err) => {
361
615
  this.inflight = null;
616
+ this.pendingRefresh = null;
362
617
  throw err;
363
618
  });
364
619
  this.inflight.catch(() => {
@@ -441,7 +696,8 @@ function validateName(input, options) {
441
696
  if (opts.uppercase) {
442
697
  name = name.toUpperCase();
443
698
  }
444
- name = name.replace(opts.allowedPattern, "");
699
+ const pattern = options?.allowedPattern ?? (opts.uppercase ? /[^A-Z0-9_]/g : /[^A-Za-z0-9_]/g);
700
+ name = name.replace(pattern, "");
445
701
  name = name.substring(0, opts.maxLength);
446
702
  if (name.length < opts.minLength) {
447
703
  return null;
@@ -452,6 +708,8 @@ function validateName(input, options) {
452
708
  // src/KeeperBoardSession.ts
453
709
  var KeeperBoardSession = class {
454
710
  constructor(config) {
711
+ this.cachedLimit = 0;
712
+ // Track the limit used for cached data
455
713
  this.isSubmitting = false;
456
714
  this.client = new KeeperBoardClient({
457
715
  apiKey: config.apiKey,
@@ -460,7 +718,6 @@ var KeeperBoardSession = class {
460
718
  });
461
719
  this.identity = new PlayerIdentity(config.identity);
462
720
  this.leaderboard = config.leaderboard;
463
- this.defaultPlayerName = config.defaultPlayerName ?? "ANON";
464
721
  this.cache = config.cache ? new Cache(config.cache.ttlMs) : null;
465
722
  this.retryQueue = config.retry ? new RetryQueue(
466
723
  `keeperboard_retry_${config.leaderboard}`,
@@ -474,14 +731,18 @@ var KeeperBoardSession = class {
474
731
  getPlayerGuid() {
475
732
  return this.identity.getOrCreatePlayerGuid();
476
733
  }
477
- /** Get the stored player name, falling back to defaultPlayerName. */
734
+ /** Get the stored player name, auto-generating one if none exists. */
478
735
  getPlayerName() {
479
- return this.identity.getPlayerName() ?? this.defaultPlayerName;
736
+ return this.identity.getOrCreatePlayerName();
480
737
  }
481
738
  /** Store a player name locally. Does NOT update the server — call updatePlayerName() for that. */
482
739
  setPlayerName(name) {
483
740
  this.identity.setPlayerName(name);
484
741
  }
742
+ /** Check if the player has explicitly set a name (vs auto-generated). */
743
+ hasExplicitPlayerName() {
744
+ return this.identity.getPlayerName() !== null && !this.identity.isAutoGeneratedName();
745
+ }
485
746
  /** Validate a name using configurable rules. Returns sanitized string or null. */
486
747
  validateName(input, options) {
487
748
  return validateName(input, options);
@@ -511,6 +772,7 @@ var KeeperBoardSession = class {
511
772
  this.retryQueue?.clear();
512
773
  if (this.cache) {
513
774
  this.cache.invalidate();
775
+ this.cachedLimit = 0;
514
776
  this.cache.refreshInBackground(() => this.fetchSnapshot());
515
777
  }
516
778
  return {
@@ -532,12 +794,18 @@ var KeeperBoardSession = class {
532
794
  * Get a combined snapshot: leaderboard entries (with `isCurrentPlayer` flag)
533
795
  * plus the current player's rank if they're outside the top N.
534
796
  *
535
- * Uses cache if enabled and fresh.
797
+ * Uses cache if enabled and fresh. If a larger limit is requested than
798
+ * what's cached, the cache is invalidated and fresh data is fetched.
536
799
  */
537
800
  async getSnapshot(options) {
538
801
  const limit = options?.limit ?? 10;
539
802
  if (this.cache) {
540
- return this.cache.getOrFetch(() => this.fetchSnapshot(limit));
803
+ if (limit > this.cachedLimit) {
804
+ this.cache.invalidate();
805
+ }
806
+ const result = await this.cache.getOrFetch(() => this.fetchSnapshot(limit));
807
+ this.cachedLimit = limit;
808
+ return result;
541
809
  }
542
810
  return this.fetchSnapshot(limit);
543
811
  }
@@ -552,7 +820,10 @@ var KeeperBoardSession = class {
552
820
  newName
553
821
  });
554
822
  this.identity.setPlayerName(newName);
555
- this.cache?.invalidate();
823
+ if (this.cache) {
824
+ this.cache.invalidate();
825
+ this.cachedLimit = 0;
826
+ }
556
827
  return true;
557
828
  } catch {
558
829
  return false;
@@ -620,5 +891,6 @@ export {
620
891
  KeeperBoardSession,
621
892
  PlayerIdentity,
622
893
  RetryQueue,
894
+ generatePlayerName,
623
895
  validateName
624
896
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keeperboard",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "TypeScript client SDK for KeeperBoard leaderboard-as-a-service",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",