@kokimoki/app 1.16.2 → 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/dist/llms.txt ADDED
@@ -0,0 +1,649 @@
1
+ ---
2
+ description: Instructions for the Kokimoki SDK
3
+ applyTo: '**/*.tsx,**/*.ts'
4
+ ---
5
+
6
+ # Kokimoki SDK
7
+
8
+ The Kokimoki SDK is a comprehensive development toolkit for building real-time collaborative game applications
9
+
10
+ ## General guidelines
11
+
12
+ - **IMPORTANT** `kmClient` is the main entry point for Kokimoki SDK
13
+ - Use `kmClient.id` as a unique identifier for each client (player)
14
+ - Use `kmClient.store` for global stores and `kmClient.localStore` for local stores
15
+ - Use `kmClient.transact` for atomic state updates across single store or multiple stores
16
+ - Use `kmClient.storage.upload` and related API methods to handle file uploads (media, JSON, etc.) in application
17
+ - Use `kmClient.serverTimestamp()` for time-related matters as this will be synced among players
18
+ - Use `useSnapshot` hook from `valtio` to get reactive state inside React components
19
+ - Use AI integration API methods: `kmClient.ai.chat`, `kmClient.ai.generateJson`, and `kmClient.ai.modifyImage` to add AI capabilities to application
20
+ - Use leaderboard API methods: `kmClient.leaderboard.insertEntry`, `kmClient.leaderboard.upsertEntry`, `kmClient.leaderboard.listEntries`, and `kmClient.leaderboard.getBestEntry` to add leaderboard capabilities to application
21
+
22
+ ## Kokimoki Client
23
+
24
+ - The `kmClient` instance is created in [km-client.ts](../../src/services/km-client.ts)
25
+ - The `kmClient` provides the following key functionalities:
26
+ - `kmClient.store` and `kmClient.localStore` for creating stores
27
+ - `kmClient.transact` for atomic state updates
28
+ - `kmClient.serverTimestamp()` for synchronized timestamps
29
+ - `kmClient.id` for unique player identification
30
+
31
+ ## Client ID
32
+
33
+ - Each client (player) has a unique `kmClient.id` (aka `clientId`), stable identifier that represents a player across multiple connections
34
+ (client sessions)
35
+ - The `kmClient.id` is persistent across client connections
36
+ - The `kmClient.id` remains consistent after user reconnect or page reload
37
+ - Use `kmClient.id` to identify players in global stores
38
+ - Each client (player) can have multiple connections, but all connections share the same `kmClient.id`
39
+
40
+ ## Kokimoki Store
41
+
42
+ Kokimoki Store powered by `valtio` and `valtio-yjs` for real-time state management in Kokimoki game applications
43
+
44
+ ### Store initialization
45
+
46
+ - Stores should be defined in `src/state/stores/`
47
+ - Store can be created in two ways:
48
+ - `kmClient.localStore` is used for data stored on the player device (local)
49
+ - `kmClient.store` is used for data shared among all players in a game (global)
50
+
51
+ **Example: Creating a Store**
52
+
53
+ ```typescript
54
+ import { kmClient } from '@services/km-client';
55
+
56
+ interface State {
57
+ title: string;
58
+ count: number;
59
+ }
60
+
61
+ const initialState: State = {
62
+ title: 'Store',
63
+ count: 0
64
+ };
65
+
66
+ // Initialize global store with initial state
67
+ export const store = kmClient.store<PlayerState>('store-name', initialState);
68
+ ```
69
+
70
+ ### State management
71
+
72
+ - State actions are functions that modify the store state by performing transactions
73
+ - Actions should be defined in `/src/state/actions/`
74
+ - Use async/await for all state transactions by `kmClient.transact` function for global stores
75
+ - Transactions are atomic and ensure state consistency
76
+ - ALWAYS update store state inside `kmClient.transact()` within action function
77
+ - Prefer using records, not arrays: store collections as `Record<string, T>` with timestamp keys for automatic sorting and better sync performance
78
+
79
+ **Example: Updating State**
80
+
81
+ ```typescript
82
+ import { store } from '../store';
83
+
84
+ // Update state
85
+ await kmClient.transact([store], ([state]) => {
86
+ state.title = 'New store';
87
+ state.count += 1;
88
+ });
89
+ ```
90
+
91
+ ### Combining Stores
92
+
93
+ - Multiple stores can be updated in a single transaction
94
+ - Prefer to update stores in a single transaction to ensure state consistency
95
+
96
+ **Example: Multiple Stores**
97
+
98
+ ```typescript
99
+ // Update multiple stores in a single transaction
100
+ await kmClient.transact([store1, store2], ([state1, state2]) => {
101
+ state1.name = 'My Store1';
102
+ state2.name = 'My Store2';
103
+ });
104
+ ```
105
+
106
+ ### Reactive State in Components
107
+
108
+ - Use `useSnapshot` hook from `valtio` to get reactive state inside React components
109
+ - The component will re-render when the store state changes
110
+
111
+ **Example: Using State in Components**
112
+
113
+ ```tsx
114
+ import { useSnapshot } from 'valtio';
115
+ import { store } from '../store';
116
+
117
+ const Component = () => {
118
+ // Get reactive snapshot of the store state
119
+ const { title, count } = useSnapshot(store.proxy);
120
+
121
+ return (
122
+ <div>
123
+ <h1>Title: {title}</h1>
124
+ <p>Count: {count}</p>
125
+ </div>
126
+ );
127
+ };
128
+ ```
129
+
130
+ ## Server Time Synchronization
131
+
132
+ Kokimoki SDK implements a time synchronization system to ensure consistent timestamps across all connected clients, regardless of their local clock differences
133
+
134
+ - Use `kmClient.serverTimestamp()` to get the current server-synchronized timestamp
135
+ - The timestamp is a Epoch Unix Timestamp
136
+ - Use server timestamps for time-related matters like event scheduling, timeouts, timers, etc.
137
+
138
+ ## Storage
139
+
140
+ Kokimoki SDK provides a storage service for uploading and managing files. Commonly used for media files (images, videos, audio), but also supports other file types like JSON or text files that aren't suitable for real-time stores. No setup required.
141
+
142
+ Access storage API via `kmClient.storage`.
143
+
144
+ ### API Methods
145
+
146
+ #### storage.upload(name, blob, tags?): Promise<Upload>
147
+
148
+ Uploads a file to storage.
149
+
150
+ **Parameters:**
151
+
152
+ - **name**: `string` filename
153
+ - **blob**: `Blob` object to upload
154
+ - **tags**: `string[]` (optional, default: [])
155
+
156
+ **Example:**
157
+
158
+ ```typescript
159
+ const upload: Upload = await kmClient.storage.upload('filename.jpg', fileBlob, [
160
+ 'tag1',
161
+ 'tag2'
162
+ ]);
163
+ // Use upload.url to access the media file
164
+ ```
165
+
166
+ #### storage.listUploads(filter?, skip?, limit?): Promise<Paginated<Upload>>
167
+
168
+ Query uploaded files by filter and pagination
169
+
170
+ **Parameters:**
171
+
172
+ - **filter.clientId**: `string` Specific client (optional)
173
+ - **filter.mimeTypes**: `string[]` E.g., ['image/jpeg', 'image/png'] (optional)
174
+ - **filter.tags**: `string[]` All tags must match (optional)
175
+ - **skip**: `number` Pagination offset (default: 0)
176
+ - **limit**: `number` Max results (default: 100)
177
+
178
+ **Example:**
179
+
180
+ ```typescript
181
+ // Query uploads by tag and uploaded by this client
182
+ const { items, total } = await kmClient.storage.listUploads(
183
+ { clientId: kmClient.id, tags: ['tag1'] },
184
+ skip,
185
+ limit
186
+ );
187
+ ```
188
+
189
+ #### storage.updateUpload(id, update): Promise<Upload>
190
+
191
+ Replace uploaded file tags with new tags
192
+
193
+ **Parameters:**
194
+
195
+ - **id**: `string` Upload id
196
+ - **update.tags**: `string[]` (optional)
197
+
198
+ **Example:**
199
+
200
+ ```typescript
201
+ const updatedUpload: Upload = await kmClient.storage.updateUpload(upload.id, {
202
+ tags: ['new']
203
+ });
204
+ ```
205
+
206
+ #### storage.deleteUpload(id): Promise<{ acknowledged: boolean; deletedCount: number }>
207
+
208
+ Permanently delete uploaded file
209
+
210
+ **Parameters:**
211
+
212
+ - **id**: `string` Upload id
213
+
214
+ **Example:**
215
+
216
+ ```typescript
217
+ await kmClient.storage.deleteUpload(upload.id);
218
+ ```
219
+
220
+ ### Types
221
+
222
+ ```typescript
223
+ interface Upload {
224
+ id: string; // unique id
225
+ url: string; // file url (CDN)
226
+ name: string; // original filename
227
+ size: number; // in bytes
228
+ mimeType: string;
229
+ clientId: string; // who uploaded
230
+ tags: string[]; // metadata for filtering and organization
231
+ completed: boolean; // upload status
232
+ createdAt: Date;
233
+ appId: string;
234
+ }
235
+
236
+ interface Paginated<T> {
237
+ items: T[];
238
+ total: number;
239
+ }
240
+ ```
241
+
242
+ ### Common Patterns
243
+
244
+ #### Example: User-specific uploads
245
+
246
+ ```typescript
247
+ // Get uploaded files by clientId
248
+ const clientUploads = await kmClient.storage.listUploads({ clientId: kmClient.id });
249
+ ```
250
+
251
+ #### Example: Filter by file type
252
+
253
+ ```typescript
254
+ // Get only uploaded images
255
+ const images = await kmClient.storage.listUploads({
256
+ mimeTypes: ['image/jpeg', 'image/png']
257
+ });
258
+ ```
259
+
260
+ #### Example: Tag-based uploads
261
+
262
+ ```typescript
263
+ // Upload file with tag
264
+ await kmClient.storage.upload('avatar.jpg', blob, ['profile']);
265
+
266
+ // Query uploads by tag
267
+ const profileUploads = await kmClient.storage.listUploads({ tags: ['profile'] });
268
+ ```
269
+
270
+ #### Example: Usage in Kokimoki Store
271
+
272
+ ```typescript
273
+ // Upload image from Blob
274
+ const upload = await kmClient.storage.upload('file.jpg', blob);
275
+
276
+ await kmClient.transact([store], (state) => {
277
+ // Add image to images array in the store
278
+ state.playerImages[upload.id] = { url: upload.url };
279
+ });
280
+ ```
281
+
282
+ ### Key Points
283
+
284
+ - **Use Cases**: Commonly used for media files (images, videos, audio), but also supports JSON, text files, or any data not suitable for real-time stores
285
+ - **CDN**: File `upload.url` is public and can be used directly
286
+ - **Tags**: Use tag system to organize uploads
287
+ - **Pagination**: Use skip/limit to paginate results
288
+ - **Filtering**: Combine clientId, mimeTypes, and tags to query uploads
289
+
290
+ ## AI Integration
291
+
292
+ Built-in methods for AI text generation and image transformation. No API keys required.
293
+
294
+ Access AI API via `kmClient.ai`.
295
+
296
+ ### API Methods
297
+
298
+ #### ai.chat(req): Promise<{ content: string }>
299
+
300
+ Used to generate text response with AI
301
+
302
+ **Parameters:**
303
+
304
+ - **req.model**: `string` AI model to use (optional). Available models:
305
+ - `gpt-4o`: OpenAI GPT-4 Optimized
306
+ - `gpt-4o-mini`: Smaller, faster GPT-4 variant
307
+ - `gpt-5`: OpenAI GPT-5 (latest)
308
+ - `gpt-5-mini`: Smaller GPT-5 variant
309
+ - `gpt-5-nano`: Smallest GPT-5 variant for lightweight tasks
310
+ - `gemini-2.5-flash-lite`: Google Gemini lite variant
311
+ - `gemini-2.5-flash`: Google Gemini fast variant
312
+ - **req.systemPrompt**: `string` AI role/behavior (optional)
313
+ - **req.userPrompt**: `string` User message (optional)
314
+ - **req.temperature**: `number` Creativity level from 0.0 = factual to 2.0 = creative (optional)
315
+ - **req.maxTokens**: `number` Response length limit (optional)
316
+
317
+ **Examples:**
318
+
319
+ ```typescript
320
+ // Generate text response
321
+ const { content } = await kmClient.ai.chat({
322
+ model: 'gemini-2.5-flash-lite',
323
+ systemPrompt: 'You are a sarcastic assistant',
324
+ userPrompt: 'Write a story about dragons',
325
+ temperature: 0.7, // moderate creativity
326
+ maxTokens: 500 // limit to 500 tokens
327
+ });
328
+ ```
329
+
330
+ #### ai.generateJson<T>(req): Promise<T>
331
+
332
+ Used to generate structured JSON output with AI. Automatically ensures the response is valid JSON and parses it for you.
333
+
334
+ **Parameters:**
335
+
336
+ - **req.model**: `string` AI model to use (optional). Available models:
337
+ - `gpt-4o`: OpenAI GPT-4 Optimized
338
+ - `gpt-4o-mini`: Smaller, faster GPT-4 variant
339
+ - `gpt-5`: OpenAI GPT-5 (latest)
340
+ - `gpt-5-mini`: Smaller GPT-5 variant
341
+ - `gpt-5-nano`: Smallest GPT-5 variant for lightweight tasks
342
+ - `gemini-2.5-flash-lite`: Google Gemini lite variant
343
+ - `gemini-2.5-flash`: Google Gemini fast variant
344
+ - **req.systemPrompt**: `string` AI role/behavior (optional)
345
+ - **req.userPrompt**: `string` User message (optional)
346
+ - **req.temperature**: `number` Creativity level from 0.0 = factual to 2.0 = creative (optional)
347
+ - **req.maxTokens**: `number` Response length limit (optional)
348
+
349
+ **Returns:** Promise resolving to parsed JSON object of type T
350
+
351
+ **Examples:**
352
+
353
+ ```typescript
354
+ // Generate quiz questions
355
+ interface Question {
356
+ question: string;
357
+ options: string[];
358
+ correctAnswer: number;
359
+ }
360
+
361
+ const questions = await kmClient.ai.generateJson<Question[]>({
362
+ systemPrompt: 'Generate quiz questions as JSON array',
363
+ userPrompt: 'Create 5 history quiz questions with 4 options each',
364
+ temperature: 0.7
365
+ });
366
+
367
+ // questions is already parsed and typed
368
+ questions.forEach(q => console.log(q.question));
369
+ ```
370
+
371
+ ```typescript
372
+ // Generate game character data
373
+ interface Character {
374
+ name: string;
375
+ strength: number;
376
+ agility: number;
377
+ backstory: string;
378
+ }
379
+
380
+ const character = await kmClient.ai.generateJson<Character>({
381
+ userPrompt: 'Create a fantasy warrior character with stats',
382
+ temperature: 0.8
383
+ });
384
+
385
+ console.log(character.name, character.strength);
386
+ ```
387
+
388
+ #### ai.modifyImage(baseImageUrl, prompt, tags?): Promise<Upload>
389
+
390
+ Used to modify/transform image with AI. The result is stored as [`Upload`](#storage) object
391
+
392
+ **Parameters:**
393
+
394
+ - **baseImageUrl**: `string` Source image URL
395
+ - **prompt**: `string` Modification/transformation description
396
+ - **tags**: `string[]` Tags for the result in `Upload` format (optional, default: [])
397
+
398
+ **Example:**
399
+
400
+ ```typescript
401
+ // Modify image from url
402
+ const upload: Upload = await kmClient.ai.modifyImage(
403
+ 'https://static.kokimoki.com/game/image.jpg',
404
+ 'Make it look like a painting',
405
+ ['art', 'ai-generated']
406
+ );
407
+ ```
408
+
409
+ ## Store Connections
410
+
411
+ Each Kokimoki store has a `connections` property that provides real-time presence information of all clients connected to that store.
412
+
413
+ ### Accessing Connections
414
+
415
+ - Use `store.connections` to access the connections proxy for any Kokimoki store
416
+ - Use `store.connections.clientIds` to get a `Set` of online client IDs
417
+ - Use `useSnapshot` to get reactive updates when connections change
418
+ - **ALWAYS** use `useSnapshot(store.connections)` to get reactive updates when connections change
419
+
420
+ ### Example: Track Online Players
421
+
422
+ ```tsx
423
+ import { useSnapshot } from 'valtio';
424
+ import { globalStore } from '@/state/stores/global-store';
425
+
426
+ const Component = () => {
427
+ // Get online client IDs from store connections
428
+ const onlineClientIds = useSnapshot(globalStore.connections).clientIds;
429
+
430
+ // Check if specific player is online
431
+ const isPlayerOnline = onlineClientIds.has(playerId);
432
+
433
+ // Get count of online players
434
+ const onlineCount = onlineClientIds.size;
435
+
436
+ return <div>Online players: {onlineCount}</div>;
437
+ };
438
+ ```
439
+
440
+ ### Example: Display Player List with Online Status
441
+
442
+ ```tsx
443
+ import { useSnapshot } from 'valtio';
444
+ import { globalStore } from '@/state/stores/global-store';
445
+
446
+ const PlayerList = () => {
447
+ const players = useSnapshot(globalStore.proxy).players;
448
+ const onlineClientIds = useSnapshot(globalStore.connections).clientIds;
449
+
450
+ const playersList = Object.entries(players).map(([clientId, player]) => ({
451
+ clientId,
452
+ name: player.name,
453
+
454
+ isOnline: onlineClientIds.has(clientId)
455
+ }));
456
+
457
+ return (
458
+ <ul>
459
+ {playersList.map((player) => (
460
+ <li key={player.clientId}>
461
+ {player.name} - {player.isOnline ? 'Online' : 'Offline'}
462
+ </li>
463
+ ))}
464
+ </ul>
465
+ );
466
+ };
467
+ ```
468
+
469
+ ### Key Points
470
+
471
+ - Each store has its own `connections` property
472
+ - `connections.clientIds` is a `Set<string>` containing connected client IDs
473
+ - Use `useSnapshot(store.connections)` to get reactive updates
474
+ - Players can have multiple browser tabs open, but all share the same `clientId`
475
+ - A player is considered online if their `clientId` is in the `clientIds` set
476
+
477
+ ## Leaderboard
478
+
479
+ Kokimoki SDK provides a leaderboard system to track and display player rankings. No setup required.
480
+
481
+ Access leaderboard API via `kmClient.leaderboard`.
482
+
483
+ ### When to Use Leaderboard API vs Global Store
484
+
485
+ **Use the Leaderboard API when:**
486
+ - You have a large number of entries (hundreds to thousands of players)
487
+ - You need efficient ranking and sorting with database indexes
488
+ - You want pagination and optimized queries for top scores
489
+ - Memory and network efficiency are important
490
+
491
+ **Use a Global Store when:**
492
+ - You have a small number of players (typically under 100)
493
+ - You need real-time updates and live leaderboard changes
494
+ - You want to combine player scores with other game state
495
+ - The leaderboard is temporary (session-based or reset frequently)
496
+
497
+ The leaderboard API is optimized for scalability with database indexes and efficient queries, making it the better choice for games with many players. Global stores are ideal for smaller, real-time collaborative scenarios where you want immediate synchronization.
498
+
499
+ ### API Methods
500
+
501
+ #### leaderboard.insertEntry<MetadataT, PrivateMetadataT>(leaderboardName, sortDir, score, metadata, privateMetadata): Promise<{ rank: number }>
502
+
503
+ Add a new entry to a leaderboard. Creates a new entry each time it's called.
504
+
505
+ **Parameters:**
506
+
507
+ - **leaderboardName**: `string` Name of the leaderboard
508
+ - **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
509
+ - **score**: `number` The score value
510
+ - **metadata**: `MetadataT` Public metadata visible to all players
511
+ - **privateMetadata**: `PrivateMetadataT` Private metadata only accessible via API
512
+
513
+ **Returns:** Promise resolving to an object with the entry's rank
514
+
515
+ **Example:**
516
+
517
+ ```typescript
518
+ const { rank } = await kmClient.leaderboard.insertEntry(
519
+ 'high-scores',
520
+ 'desc',
521
+ 1500,
522
+ { playerName: 'Alice', level: 10 },
523
+ { sessionId: 'abc123' }
524
+ );
525
+ console.log(`New rank: ${rank}`);
526
+ ```
527
+
528
+ #### leaderboard.upsertEntry<MetadataT, PrivateMetadataT>(leaderboardName, sortDir, score, metadata, privateMetadata): Promise<{ rank: number }>
529
+
530
+ Add or update the latest entry for the current client in a leaderboard. Replaces the previous entry if one exists.
531
+
532
+ **Parameters:**
533
+
534
+ - **leaderboardName**: `string` Name of the leaderboard
535
+ - **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
536
+ - **score**: `number` The score value
537
+ - **metadata**: `MetadataT` Public metadata visible to all players
538
+ - **privateMetadata**: `PrivateMetadataT` Private metadata only accessible via API
539
+
540
+ **Returns:** Promise resolving to an object with the entry's rank
541
+
542
+ **Example:**
543
+
544
+ ```typescript
545
+ const { rank } = await kmClient.leaderboard.upsertEntry(
546
+ 'daily-scores',
547
+ 'desc',
548
+ 2000,
549
+ { playerName: 'Bob', completionTime: 120 },
550
+ { deviceId: 'xyz789' }
551
+ );
552
+ console.log(`Updated rank: ${rank}`);
553
+ ```
554
+
555
+ #### leaderboard.listEntries<MetadataT>(leaderboardName, sortDir, skip?, limit?): Promise<Paginated<{ rank: number; score: number; metadata: MetadataT }>>
556
+
557
+ List entries in a leaderboard with pagination.
558
+
559
+ **Parameters:**
560
+
561
+ - **leaderboardName**: `string` Name of the leaderboard
562
+ - **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
563
+ - **skip**: `number` Number of entries to skip for pagination (default: 0)
564
+ - **limit**: `number` Maximum number of entries to return (default: 100)
565
+
566
+ **Returns:** Promise resolving to a paginated list of entries
567
+
568
+ **Example:**
569
+
570
+ ```typescript
571
+ const { items, total } = await kmClient.leaderboard.listEntries(
572
+ 'weekly-scores',
573
+ 'desc',
574
+ 0, // skip
575
+ 10 // limit - get top 10
576
+ );
577
+
578
+ items.forEach(entry => {
579
+ console.log(`Rank ${entry.rank}: ${entry.metadata.playerName} - ${entry.score}`);
580
+ });
581
+ ```
582
+
583
+ #### leaderboard.getBestEntry<MetadataT>(leaderboardName, sortDir, clientId?): Promise<{ rank: number; score: number; metadata: MetadataT }>
584
+
585
+ Get the best entry for a specific client in a leaderboard.
586
+
587
+ **Parameters:**
588
+
589
+ - **leaderboardName**: `string` Name of the leaderboard
590
+ - **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
591
+ - **clientId**: `string` (optional) Client ID to get entry for. Defaults to current client if not provided.
592
+
593
+ **Returns:** Promise resolving to the best entry for the client
594
+
595
+ **Example:**
596
+
597
+ ```typescript
598
+ // Get current client's best entry
599
+ const myBest = await kmClient.leaderboard.getBestEntry('all-time-high', 'desc');
600
+ console.log(`My best: Rank ${myBest.rank}, Score ${myBest.score}`);
601
+
602
+ // Get another player's best entry
603
+ const otherBest = await kmClient.leaderboard.getBestEntry(
604
+ 'all-time-high',
605
+ 'desc',
606
+ 'other-client-id'
607
+ );
608
+ ```
609
+
610
+ ### Common Patterns
611
+
612
+ #### Example: Track High Scores
613
+
614
+ ```typescript
615
+ // Submit a new high score
616
+ await kmClient.leaderboard.upsertEntry(
617
+ 'high-scores',
618
+ 'desc',
619
+ score,
620
+ { playerName: player.name },
621
+ { timestamp: Date.now() }
622
+ );
623
+
624
+ // Display top 10
625
+ const { items } = await kmClient.leaderboard.listEntries('high-scores', 'desc', 0, 10);
626
+ ```
627
+
628
+ #### Example: Track Speed Run Times
629
+
630
+ ```typescript
631
+ // Submit completion time (lower is better)
632
+ await kmClient.leaderboard.upsertEntry(
633
+ 'speed-run',
634
+ 'asc',
635
+ completionTimeInSeconds,
636
+ { playerName: player.name, difficulty: 'hard' },
637
+ {}
638
+ );
639
+
640
+ // Get personal best
641
+ const myBest = await kmClient.leaderboard.getBestEntry('speed-run', 'asc');
642
+ ```
643
+
644
+ ### Key Points
645
+
646
+ - **Sort Direction**: Use `asc` when lower scores are better (e.g., completion time), `desc` when higher scores are better (e.g., points)
647
+ - **Insert vs Upsert**: Use `insertEntry` to keep all attempts, `upsertEntry` to keep only the latest/best
648
+ - **Metadata**: Public metadata is visible to all, private metadata is only accessible via API calls
649
+ - **Pagination**: Use skip/limit to implement leaderboard pages or "load more" functionality
@@ -0,0 +1,8 @@
1
+ import { SyncedStore } from "./synced-store";
2
+ export declare class MessageQueue<T> extends SyncedStore<{
3
+ messages: T[];
4
+ }> {
5
+ name: string;
6
+ constructor(name: string);
7
+ push(message: T): Promise<Uint8Array>;
8
+ }
@@ -0,0 +1,19 @@
1
+ import EventEmitter from "events";
2
+ import { SyncedStore } from "./synced-store";
3
+ import Y from "yjs";
4
+ export class MessageQueue extends SyncedStore {
5
+ name;
6
+ constructor(name) {
7
+ super({ messages: [] });
8
+ this.name = name;
9
+ }
10
+ async push(message) {
11
+ // Construct Y update to push the message to the queue
12
+ const id = crypto.randomUUID();
13
+ const ydoc = new Y.Doc();
14
+ const map = ydoc.getMap("messages");
15
+ map.set(id, message);
16
+ const update = Y.encodeStateAsUpdate(ydoc);
17
+ return update;
18
+ }
19
+ }