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 +14 -2
- package/dist/index.d.mts +42 -7
- package/dist/index.d.ts +42 -7
- package/dist/index.js +282 -9
- package/dist/index.mjs +281 -9
- package/package.json +1 -1
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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,
|
|
767
|
+
/** Get the stored player name, auto-generating one if none exists. */
|
|
510
768
|
getPlayerName() {
|
|
511
|
-
return this.identity.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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,
|
|
734
|
+
/** Get the stored player name, auto-generating one if none exists. */
|
|
478
735
|
getPlayerName() {
|
|
479
|
-
return this.identity.
|
|
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
|
-
|
|
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
|
|
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
|
};
|