keeperboard 1.0.4 → 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/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # KeeperBoard SDK
2
2
 
3
- TypeScript client SDK for [KeeperBoard](https://github.com/clauderoy790/keeperboard) — a free, open-source leaderboard-as-a-service for indie game developers.
4
-
5
- Works with Phaser.js, vanilla JavaScript, and any TypeScript/JavaScript game running in the browser.
3
+ TypeScript client for [KeeperBoard](https://keeperboard.vercel.app) leaderboard-as-a-service. Works in browsers and Node.js.
6
4
 
7
5
  ## Installation
8
6
 
@@ -10,287 +8,222 @@ Works with Phaser.js, vanilla JavaScript, and any TypeScript/JavaScript game run
10
8
  npm install keeperboard
11
9
  ```
12
10
 
13
- ### Alternative: Copy source directly
14
-
15
- If you prefer not to use npm, copy the `src/` folder into your project:
16
-
17
- ```typescript
18
- import { KeeperBoardClient, PlayerIdentity } from './keeperboard/index';
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ### 1. Get your API key
24
-
25
- 1. Sign up at your KeeperBoard dashboard
26
- 2. Create a game
27
- 3. Create an environment (dev/prod)
28
- 4. Generate an API key for that environment
29
-
30
- ### 2. Initialize the client
11
+ ## Quick Start (15 lines)
31
12
 
32
13
  ```typescript
33
- import { KeeperBoardClient, PlayerIdentity } from 'keeperboard';
14
+ import { KeeperBoardSession } from 'keeperboard';
34
15
 
35
- // Create the API client
36
- const client = new KeeperBoardClient({
37
- apiKey: 'kb_prod_your_api_key_here',
16
+ const session = new KeeperBoardSession({
17
+ apiKey: 'kb_dev_your_api_key',
18
+ leaderboard: 'main',
19
+ cache: { ttlMs: 30000 }, // Optional: 30s cache
20
+ retry: { maxAgeMs: 86400000 }, // Optional: 24h retry queue
38
21
  });
39
22
 
40
- // Helper for persistent player identity
41
- const identity = new PlayerIdentity();
42
- ```
43
-
44
- ### 3. Submit a score
45
-
46
- ```typescript
47
- const playerGuid = identity.getOrCreatePlayerGuid();
48
-
49
- // Submit to default leaderboard
50
- const result = await client.submitScore(playerGuid, 'PlayerName', 1500);
51
-
52
- console.log(`Rank: #${result.rank}`);
53
- console.log(`New high score: ${result.is_new_high_score}`);
54
- ```
55
-
56
- ### 4. Display the leaderboard
57
-
58
- ```typescript
59
- // Get top 10
60
- const leaderboard = await client.getLeaderboard();
61
-
62
- leaderboard.entries.forEach((entry) => {
63
- console.log(`#${entry.rank} ${entry.player_name}: ${entry.score}`);
64
- });
65
- ```
66
-
67
- ### 5. Show player's rank (even if not in top 10)
68
-
69
- ```typescript
70
- const player = await client.getPlayerRank(playerGuid);
71
-
72
- if (player && player.rank > 10) {
73
- console.log(`You are ranked #${player.rank} with ${player.score} points`);
23
+ // Submit a score
24
+ const result = await session.submitScore(1500);
25
+ if (result.success) {
26
+ console.log(`Rank #${result.rank}, New high: ${result.isNewHighScore}`);
74
27
  }
75
- ```
76
28
 
77
- ## API Reference
78
-
79
- ### KeeperBoardClient
80
-
81
- #### Constructor
82
-
83
- ```typescript
84
- const client = new KeeperBoardClient({
85
- apiKey: string, // API key from dashboard
29
+ // Get leaderboard with player's rank
30
+ const snapshot = await session.getSnapshot({ limit: 10 });
31
+ snapshot.entries.forEach(e => {
32
+ console.log(`#${e.rank} ${e.playerName}: ${e.score}`, e.isCurrentPlayer ? '(you)' : '');
86
33
  });
87
34
  ```
88
35
 
89
- > **Note:** The API key determines which game and environment you're accessing. You don't need to pass environment or game IDs — they're implicit in the key.
90
-
91
- ---
92
-
93
- ### Score Submission
94
-
95
- #### `submitScore(playerGuid, playerName, score)`
36
+ ## Two API Layers
96
37
 
97
- Submit a score to the default leaderboard. Only updates if higher than existing score.
38
+ | Layer | Use case | Identity | Cache | Retry |
39
+ |-------|----------|----------|-------|-------|
40
+ | **KeeperBoardSession** | Browser games | Auto-managed | Built-in | Built-in |
41
+ | **KeeperBoardClient** | Server-side, advanced | Manual | No | No |
98
42
 
99
- ```typescript
100
- const result = await client.submitScore('player-uuid', 'PlayerName', 2500);
101
- // Returns: { id, player_guid, player_name, score, rank, is_new_high_score }
102
- ```
103
-
104
- #### `submitScore(playerGuid, playerName, score, leaderboard)`
43
+ Most browser games should use `KeeperBoardSession`. Use `KeeperBoardClient` for server-side code or when you need full control.
105
44
 
106
- Submit to a specific leaderboard by name.
107
-
108
- ```typescript
109
- await client.submitScore('player-uuid', 'PlayerName', 2500, 'Weekly Best');
110
- ```
45
+ ---
111
46
 
112
- #### `submitScore(playerGuid, playerName, score, leaderboard, metadata)`
47
+ ## KeeperBoardSession API
113
48
 
114
- Submit with optional metadata.
49
+ ### Constructor
115
50
 
116
51
  ```typescript
117
- await client.submitScore('player-uuid', 'PlayerName', 2500, 'Weekly Best', {
118
- level: 10,
119
- character: 'warrior',
52
+ const session = new KeeperBoardSession({
53
+ apiKey: 'kb_dev_xxx', // Required
54
+ leaderboard: 'main', // Required - session is bound to one board
55
+ defaultPlayerName: 'ANON', // Optional (default: 'ANON')
56
+ identity: { keyPrefix: 'app_' }, // Optional localStorage prefix
57
+ cache: { ttlMs: 30000 }, // Optional TTL cache for getSnapshot()
58
+ retry: { maxAgeMs: 86400000 }, // Optional retry queue for failed submissions
120
59
  });
121
60
  ```
122
61
 
123
- ---
124
-
125
- ### Leaderboard
126
-
127
- #### `getLeaderboard()`
128
-
129
- Get the default leaderboard (top 10 entries).
62
+ ### Identity (auto-managed)
130
63
 
131
64
  ```typescript
132
- const lb = await client.getLeaderboard();
133
- // Returns: { entries, total_count, reset_schedule }
134
- ```
135
-
136
- #### `getLeaderboard(name)`
65
+ session.getPlayerGuid(); // Get or create persistent GUID
66
+ session.getPlayerName(); // Get stored name or default
67
+ session.setPlayerName(name); // Store name locally (doesn't update server)
137
68
 
138
- Get a specific leaderboard by name.
139
-
140
- ```typescript
141
- const lb = await client.getLeaderboard('Weekly Best');
69
+ // Validate a name (pure function)
70
+ const validated = session.validateName(' Ace Pilot! ');
71
+ // Returns 'ACEPILOT' or null if invalid
142
72
  ```
143
73
 
144
- #### `getLeaderboard(name, limit)`
145
-
146
- Get with a custom limit (max 100).
74
+ ### Core Methods
147
75
 
148
76
  ```typescript
149
- const lb = await client.getLeaderboard('Weekly Best', 50);
77
+ // Submit score (identity auto-injected)
78
+ const result = await session.submitScore(1500, { level: 5 });
79
+ // Returns: { success: true, rank: 3, isNewHighScore: true }
80
+ // or: { success: false, error: 'Network error' }
81
+
82
+ // Get snapshot (leaderboard + player rank combined)
83
+ const snapshot = await session.getSnapshot({ limit: 10 });
84
+ // Returns: {
85
+ // entries: [{ rank, playerGuid, playerName, score, isCurrentPlayer }],
86
+ // totalCount: 150,
87
+ // playerRank: { rank: 42, score: 1200, ... } | null // Only if outside top N
88
+ // }
89
+
90
+ // Update player name on server
91
+ const success = await session.updatePlayerName('MAVERICK');
150
92
  ```
151
93
 
152
- #### `getLeaderboard(name, limit, offset)`
153
-
154
- Get with pagination.
94
+ ### Retry Queue
155
95
 
156
96
  ```typescript
157
- // Page 2 (entries 11-20)
158
- const lb = await client.getLeaderboard('Weekly Best', 10, 10);
97
+ // Check for pending scores from previous failed submissions
98
+ if (session.hasPendingScore()) {
99
+ await session.retryPendingScore();
100
+ }
159
101
  ```
160
102
 
161
- ---
162
-
163
- ### Leaderboard Versions (Time-Based)
164
-
165
- For leaderboards with reset schedules (daily/weekly/monthly), you can query historical versions.
166
-
167
- #### `getLeaderboardVersion(name, version)`
168
-
169
- Get a specific version of a time-based leaderboard.
103
+ ### Cache
170
104
 
171
105
  ```typescript
172
- // Get last week's scores (version 3)
173
- const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
174
- ```
106
+ // Pre-fetch in background (e.g., on menu load)
107
+ session.prefetch();
175
108
 
176
- #### `getLeaderboardVersion(name, version, limit, offset)`
177
-
178
- Get historical version with pagination.
179
-
180
- ```typescript
181
- const lb = await client.getLeaderboardVersion('Weekly Best', 3, 25, 0);
109
+ // getSnapshot() automatically uses cache when fresh
182
110
  ```
183
111
 
184
- ---
185
-
186
- ### Player
187
-
188
- #### `getPlayerRank(playerGuid)`
189
-
190
- Get a player's rank and score. Returns `null` if player has no score.
112
+ ### Escape Hatch
191
113
 
192
114
  ```typescript
193
- const player = await client.getPlayerRank('player-uuid');
194
-
195
- if (player) {
196
- console.log(`Rank: #${player.rank}, Score: ${player.score}`);
197
- }
115
+ // Access underlying client for advanced operations
116
+ const client = session.getClient();
117
+ await client.claimScore({ playerGuid: '...', playerName: '...' });
198
118
  ```
199
119
 
200
- #### `getPlayerRank(playerGuid, leaderboard)`
201
-
202
- Get player's rank on a specific leaderboard.
120
+ ---
203
121
 
204
- ```typescript
205
- const player = await client.getPlayerRank('player-uuid', 'Weekly Best');
206
- ```
122
+ ## KeeperBoardClient API
207
123
 
208
- #### `updatePlayerName(playerGuid, newName)`
124
+ Low-level client with options-object methods and camelCase responses.
209
125
 
210
- Update a player's display name.
126
+ ### Constructor
211
127
 
212
128
  ```typescript
213
- await client.updatePlayerName('player-uuid', 'NewPlayerName');
129
+ const client = new KeeperBoardClient({
130
+ apiKey: 'kb_dev_xxx',
131
+ defaultLeaderboard: 'main', // Optional - used when leaderboard not specified
132
+ });
214
133
  ```
215
134
 
216
- ---
217
-
218
- ### Claim (for migrated scores)
219
-
220
- #### `claimScore(playerGuid, playerName)`
221
-
222
- Claim a migrated score by matching player name. Used when scores were imported without player GUIDs.
135
+ ### Methods
223
136
 
224
137
  ```typescript
225
- const result = await client.claimScore('new-player-guid', 'ImportedPlayerName');
226
- console.log(`Claimed score: ${result.score}, Rank: #${result.rank}`);
227
- ```
228
-
229
- ---
138
+ // Submit score
139
+ const result = await client.submitScore({
140
+ playerGuid: 'abc-123',
141
+ playerName: 'ACE',
142
+ score: 1500,
143
+ metadata: { level: 5 }, // Optional
144
+ leaderboard: 'weekly', // Optional - overrides defaultLeaderboard
145
+ });
146
+ // Returns: ScoreResult { id, playerGuid, playerName, score, rank, isNewHighScore }
147
+
148
+ // Get leaderboard
149
+ const lb = await client.getLeaderboard({
150
+ leaderboard: 'main', // Optional
151
+ limit: 25, // Optional (default 10, max 100)
152
+ offset: 0, // Optional pagination
153
+ version: 3, // Optional - for time-based boards
154
+ });
155
+ // Returns: LeaderboardResult { entries, totalCount, resetSchedule, version?, ... }
230
156
 
231
- ### Health Check
157
+ // Get player rank
158
+ const player = await client.getPlayerRank({
159
+ playerGuid: 'abc-123',
160
+ leaderboard: 'main', // Optional
161
+ });
162
+ // Returns: PlayerResult | null
232
163
 
233
- #### `healthCheck()`
164
+ // Update player name
165
+ const updated = await client.updatePlayerName({
166
+ playerGuid: 'abc-123',
167
+ newName: 'MAVERICK',
168
+ leaderboard: 'main', // Optional
169
+ });
234
170
 
235
- Check if the API is healthy. Does not require an API key.
171
+ // Claim migrated score (for imported data without GUIDs)
172
+ const claim = await client.claimScore({
173
+ playerGuid: 'abc-123',
174
+ playerName: 'OldPlayer',
175
+ leaderboard: 'main', // Optional
176
+ });
236
177
 
237
- ```typescript
178
+ // Health check (no auth required)
238
179
  const health = await client.healthCheck();
239
- console.log(`API Version: ${health.version}`);
240
180
  ```
241
181
 
242
182
  ---
243
183
 
244
- ### PlayerIdentity
184
+ ## Name Validation
245
185
 
246
- Helper for managing persistent player identity in localStorage.
186
+ Standalone function for validating player names:
247
187
 
248
188
  ```typescript
249
- const identity = new PlayerIdentity({
250
- keyPrefix: 'mygame_', // optional, default: 'keeperboard_'
189
+ import { validateName } from 'keeperboard';
190
+
191
+ validateName(' Ace Pilot! '); // 'ACEPILOT'
192
+ validateName('x'); // null (too short)
193
+ validateName('verylongname123456'); // 'VERYLONGNAME' (truncated to 12)
194
+
195
+ // Custom options
196
+ validateName('hello', {
197
+ minLength: 3,
198
+ maxLength: 8,
199
+ uppercase: false,
200
+ allowedPattern: /[^a-z]/g,
251
201
  });
252
-
253
- // Get or create a persistent player GUID
254
- const guid = identity.getOrCreatePlayerGuid();
255
-
256
- // Store/retrieve player name
257
- identity.setPlayerName('PlayerName');
258
- const name = identity.getPlayerName();
259
-
260
- // Check if identity exists
261
- if (identity.hasIdentity()) {
262
- // returning player
263
- }
264
-
265
- // Clear identity (e.g., for "Sign Out")
266
- identity.clear();
267
202
  ```
268
203
 
269
204
  ---
270
205
 
271
- ### Error Handling
272
-
273
- All methods throw `KeeperBoardError` on failure:
206
+ ## Error Handling
274
207
 
275
208
  ```typescript
276
209
  import { KeeperBoardError } from 'keeperboard';
277
210
 
278
211
  try {
279
- await client.submitScore(playerGuid, name, score);
212
+ await client.submitScore({ ... });
280
213
  } catch (error) {
281
214
  if (error instanceof KeeperBoardError) {
282
- console.error(`Error [${error.code}]: ${error.message}`);
283
-
284
215
  switch (error.code) {
285
216
  case 'INVALID_API_KEY':
286
- // Check your API key
217
+ console.error('Check your API key');
287
218
  break;
288
219
  case 'NOT_FOUND':
289
- // Player or leaderboard not found
220
+ console.error('Leaderboard not found');
290
221
  break;
291
222
  case 'INVALID_REQUEST':
292
- // Check request parameters
223
+ console.error('Bad request:', error.message);
293
224
  break;
225
+ default:
226
+ console.error('API error:', error.message);
294
227
  }
295
228
  }
296
229
  }
@@ -298,129 +231,100 @@ try {
298
231
 
299
232
  ---
300
233
 
301
- ## Multiple Leaderboards
302
-
303
- If your game has multiple leaderboards (e.g., per-level or per-mode):
304
-
305
- ```typescript
306
- // Submit to different leaderboards
307
- await client.submitScore(guid, name, score, 'Level 1');
308
- await client.submitScore(guid, name, score, 'Endless Mode');
309
- await client.submitScore(guid, name, score, 'Weekly Challenge');
310
-
311
- // Get specific leaderboards
312
- const level1 = await client.getLeaderboard('Level 1');
313
- const endless = await client.getLeaderboard('Endless Mode', 50);
314
- ```
315
-
316
- ---
317
-
318
- ## Phaser.js Integration Example
234
+ ## Phaser.js Integration
319
235
 
320
236
  ```typescript
321
- import { KeeperBoardClient, PlayerIdentity } from 'keeperboard';
322
-
323
- const client = new KeeperBoardClient({
324
- apiKey: 'kb_prod_your_api_key',
237
+ import { KeeperBoardSession } from 'keeperboard';
238
+
239
+ // Initialize once at game start
240
+ const leaderboard = new KeeperBoardSession({
241
+ apiKey: import.meta.env.VITE_KEEPERBOARD_API_KEY,
242
+ leaderboard: 'main',
243
+ cache: { ttlMs: 30000 },
244
+ retry: { maxAgeMs: 86400000 },
325
245
  });
326
246
 
327
- const identity = new PlayerIdentity();
328
-
329
- class GameOverScene extends Phaser.Scene {
330
- private score: number = 0;
331
-
332
- init(data: { score: number }) {
333
- this.score = data.score;
247
+ // In BootScene - prefetch and retry
248
+ class BootScene extends Phaser.Scene {
249
+ async create() {
250
+ leaderboard.prefetch();
251
+ await leaderboard.retryPendingScore();
252
+ this.scene.start('MenuScene');
334
253
  }
254
+ }
335
255
 
256
+ // In GameOverScene
257
+ class GameOverScene extends Phaser.Scene {
336
258
  async create() {
337
- const playerGuid = identity.getOrCreatePlayerGuid();
338
- const playerName = identity.getPlayerName() ?? 'Anonymous';
339
-
340
- // Submit score
341
- const result = await client.submitScore(playerGuid, playerName, this.score);
342
-
343
- // Display rank
344
- this.add
345
- .text(400, 200, `Your Rank: #${result.rank}`, { fontSize: '32px' })
346
- .setOrigin(0.5);
347
-
348
- if (result.is_new_high_score) {
349
- this.add
350
- .text(400, 250, 'NEW HIGH SCORE!', {
351
- fontSize: '24px',
352
- color: '#ffff00',
353
- })
354
- .setOrigin(0.5);
259
+ const result = await leaderboard.submitScore(this.score);
260
+ if (result.success) {
261
+ this.showRank(result.rank, result.isNewHighScore);
355
262
  }
356
263
 
357
- // Display top 10
358
- const leaderboard = await client.getLeaderboard();
359
-
360
- leaderboard.entries.forEach((entry, index) => {
361
- const isMe = entry.player_guid === playerGuid;
362
- const color = isMe ? '#00ff00' : '#ffffff';
363
-
364
- this.add
365
- .text(
366
- 400,
367
- 350 + index * 30,
368
- `#${entry.rank} ${entry.player_name}: ${entry.score}`,
369
- { fontSize: '18px', color },
370
- )
371
- .setOrigin(0.5);
372
- });
373
-
374
- // Show player's rank if not in top 10
375
- const player = await client.getPlayerRank(playerGuid);
376
- if (player && player.rank > 10) {
377
- this.add
378
- .text(400, 660, `... #${player.rank} ${playerName}: ${player.score}`, {
379
- fontSize: '18px',
380
- color: '#00ff00',
381
- })
382
- .setOrigin(0.5);
383
- }
264
+ const snapshot = await leaderboard.getSnapshot({ limit: 10 });
265
+ this.displayLeaderboard(snapshot.entries);
384
266
  }
385
267
  }
386
268
  ```
387
269
 
388
270
  ---
389
271
 
390
- ## Development
272
+ ## Utilities
391
273
 
392
- ```bash
393
- # Install dependencies
394
- npm install
274
+ ### PlayerIdentity
395
275
 
396
- # Type check
397
- npm run typecheck
276
+ Standalone helper for localStorage identity management:
398
277
 
399
- # Build
400
- npm run build
278
+ ```typescript
279
+ import { PlayerIdentity } from 'keeperboard';
401
280
 
402
- # Run tests (requires .env with Supabase credentials)
403
- npm test
281
+ const identity = new PlayerIdentity({ keyPrefix: 'myapp_' });
282
+ const guid = identity.getOrCreatePlayerGuid();
283
+ identity.setPlayerName('ACE');
284
+ ```
285
+
286
+ ### Cache
287
+
288
+ Generic TTL cache with deduplication:
404
289
 
405
- # Clean
406
- npm run clean
290
+ ```typescript
291
+ import { Cache } from 'keeperboard';
292
+
293
+ const cache = new Cache<Data>(30000); // 30s TTL
294
+ const data = await cache.getOrFetch(() => fetchData());
407
295
  ```
408
296
 
409
- ### Running Tests
297
+ ### RetryQueue
410
298
 
411
- Copy `.env.example` to `.env` and fill in your Supabase credentials:
299
+ localStorage-based retry for failed operations:
412
300
 
413
- ```bash
414
- cp .env.example .env
301
+ ```typescript
302
+ import { RetryQueue } from 'keeperboard';
303
+
304
+ const queue = new RetryQueue('myapp_retry', 86400000); // 24h max age
305
+ queue.save(1500, { level: 5 });
306
+ const pending = queue.get(); // { score: 1500, metadata: {...} } or null
415
307
  ```
416
308
 
417
- Then run:
309
+ ---
310
+
311
+ ## Development
418
312
 
419
313
  ```bash
314
+ # Install dependencies
315
+ npm install
316
+
317
+ # Run tests (requires local KeeperBoard server + Supabase)
420
318
  npm test
319
+
320
+ # Type check
321
+ npm run typecheck
322
+
323
+ # Build
324
+ npm run build
421
325
  ```
422
326
 
423
- ---
327
+ See [MIGRATION.md](./MIGRATION.md) for upgrading from v1.x.
424
328
 
425
329
  ## License
426
330