keeperboard 1.0.3 → 2.0.0

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