@kokimoki/app 2.1.0 → 3.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 +72 -0
- package/dist/core/kokimoki-client.d.ts +75 -15
- package/dist/core/kokimoki-client.js +137 -22
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -0
- package/dist/kokimoki.min.d.ts +322 -2
- package/dist/kokimoki.min.js +1884 -72
- package/dist/kokimoki.min.js.map +1 -1
- package/dist/llms.txt +6 -0
- package/dist/protocol/ws-message/reader.d.ts +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/kokimoki-ai.d.ts +185 -122
- package/dist/services/kokimoki-ai.js +201 -109
- package/dist/services/kokimoki-i18n.d.ts +259 -0
- package/dist/services/kokimoki-i18n.js +325 -0
- package/dist/stores/kokimoki-local-store.d.ts +1 -1
- package/dist/types/common.d.ts +9 -0
- package/dist/types/env.d.ts +36 -0
- package/dist/types/env.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/kokimoki-client.d.ts +31 -0
- package/dist/utils/kokimoki-client.js +38 -0
- package/dist/utils/kokimoki-dev.d.ts +30 -0
- package/dist/utils/kokimoki-dev.js +75 -0
- package/dist/utils/kokimoki-env.d.ts +20 -0
- package/dist/utils/kokimoki-env.js +30 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/kokimoki-ai.instructions.md +316 -0
- package/docs/kokimoki-dynamic-stores.instructions.md +439 -0
- package/docs/kokimoki-i18n.instructions.md +285 -0
- package/docs/kokimoki-leaderboard.instructions.md +189 -0
- package/docs/kokimoki-sdk.instructions.md +221 -0
- package/docs/kokimoki-storage.instructions.md +162 -0
- package/llms.txt +43 -0
- package/package.json +9 -13
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Leaderboard and player rankings with Kokimoki SDK"
|
|
3
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Leaderboard
|
|
7
|
+
|
|
8
|
+
Kokimoki SDK provides a leaderboard system to track and display player rankings. No setup required.
|
|
9
|
+
|
|
10
|
+
For core SDK concepts, see [Kokimoki SDK](./kokimoki-sdk.instructions.md).
|
|
11
|
+
|
|
12
|
+
Access leaderboard API via `kmClient.leaderboard`.
|
|
13
|
+
|
|
14
|
+
## When to Use Leaderboard API vs Global Store
|
|
15
|
+
|
|
16
|
+
**Use the Leaderboard API when:**
|
|
17
|
+
|
|
18
|
+
- You have a large number of entries (hundreds to thousands of players)
|
|
19
|
+
- You need efficient ranking and sorting with database indexes
|
|
20
|
+
- You want pagination and optimized queries for top scores
|
|
21
|
+
- Memory and network efficiency are important
|
|
22
|
+
|
|
23
|
+
**Use a Global Store when:**
|
|
24
|
+
|
|
25
|
+
- You have a small number of players (typically under 100)
|
|
26
|
+
- You need real-time updates and live leaderboard changes
|
|
27
|
+
- You want to combine player scores with other game state
|
|
28
|
+
- The leaderboard is temporary (session-based or reset frequently)
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
## API Methods
|
|
33
|
+
|
|
34
|
+
### leaderboard.insertEntry<MetadataT, PrivateMetadataT>(leaderboardName, sortDir, score, metadata, privateMetadata): Promise<{ rank: number }>
|
|
35
|
+
|
|
36
|
+
Add a new entry to a leaderboard. Creates a new entry each time it's called.
|
|
37
|
+
|
|
38
|
+
**Parameters:**
|
|
39
|
+
|
|
40
|
+
- **leaderboardName**: `string` Name of the leaderboard
|
|
41
|
+
- **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
|
|
42
|
+
- **score**: `number` The score value
|
|
43
|
+
- **metadata**: `MetadataT` Public metadata visible to all players
|
|
44
|
+
- **privateMetadata**: `PrivateMetadataT` Private metadata only accessible via API
|
|
45
|
+
|
|
46
|
+
**Returns:** Promise resolving to an object with the entry's rank
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const { rank } = await kmClient.leaderboard.insertEntry(
|
|
52
|
+
"high-scores",
|
|
53
|
+
"desc",
|
|
54
|
+
1500,
|
|
55
|
+
{ playerName: "Alice", level: 10 },
|
|
56
|
+
{ sessionId: "abc123" }
|
|
57
|
+
);
|
|
58
|
+
console.log(`New rank: ${rank}`);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### leaderboard.upsertEntry<MetadataT, PrivateMetadataT>(leaderboardName, sortDir, score, metadata, privateMetadata): Promise<{ rank: number }>
|
|
62
|
+
|
|
63
|
+
Add or update the latest entry for the current client in a leaderboard. Replaces the previous entry if one exists.
|
|
64
|
+
|
|
65
|
+
**Parameters:**
|
|
66
|
+
|
|
67
|
+
- **leaderboardName**: `string` Name of the leaderboard
|
|
68
|
+
- **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
|
|
69
|
+
- **score**: `number` The score value
|
|
70
|
+
- **metadata**: `MetadataT` Public metadata visible to all players
|
|
71
|
+
- **privateMetadata**: `PrivateMetadataT` Private metadata only accessible via API
|
|
72
|
+
|
|
73
|
+
**Returns:** Promise resolving to an object with the entry's rank
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const { rank } = await kmClient.leaderboard.upsertEntry(
|
|
79
|
+
"daily-scores",
|
|
80
|
+
"desc",
|
|
81
|
+
2000,
|
|
82
|
+
{ playerName: "Bob", completionTime: 120 },
|
|
83
|
+
{ deviceId: "xyz789" }
|
|
84
|
+
);
|
|
85
|
+
console.log(`Updated rank: ${rank}`);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### leaderboard.listEntries<MetadataT>(leaderboardName, sortDir, skip?, limit?): Promise<Paginated<{ rank: number; score: number; metadata: MetadataT }>>
|
|
89
|
+
|
|
90
|
+
List entries in a leaderboard with pagination.
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
|
|
94
|
+
- **leaderboardName**: `string` Name of the leaderboard
|
|
95
|
+
- **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
|
|
96
|
+
- **skip**: `number` Number of entries to skip for pagination (default: 0)
|
|
97
|
+
- **limit**: `number` Maximum number of entries to return (default: 100)
|
|
98
|
+
|
|
99
|
+
**Returns:** Promise resolving to a paginated list of entries
|
|
100
|
+
|
|
101
|
+
**Example:**
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const { items, total } = await kmClient.leaderboard.listEntries(
|
|
105
|
+
"weekly-scores",
|
|
106
|
+
"desc",
|
|
107
|
+
0, // skip
|
|
108
|
+
10 // limit - get top 10
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
items.forEach((entry) => {
|
|
112
|
+
console.log(
|
|
113
|
+
`Rank ${entry.rank}: ${entry.metadata.playerName} - ${entry.score}`
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### leaderboard.getBestEntry<MetadataT>(leaderboardName, sortDir, clientId?): Promise<{ rank: number; score: number; metadata: MetadataT }>
|
|
119
|
+
|
|
120
|
+
Get the best entry for a specific client in a leaderboard.
|
|
121
|
+
|
|
122
|
+
**Parameters:**
|
|
123
|
+
|
|
124
|
+
- **leaderboardName**: `string` Name of the leaderboard
|
|
125
|
+
- **sortDir**: `"asc" | "desc"` Sort direction (asc = lowest is best, desc = highest is best)
|
|
126
|
+
- **clientId**: `string` (optional) Client ID to get entry for. Defaults to current client if not provided.
|
|
127
|
+
|
|
128
|
+
**Returns:** Promise resolving to the best entry for the client
|
|
129
|
+
|
|
130
|
+
**Example:**
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Get current client's best entry
|
|
134
|
+
const myBest = await kmClient.leaderboard.getBestEntry("all-time-high", "desc");
|
|
135
|
+
console.log(`My best: Rank ${myBest.rank}, Score ${myBest.score}`);
|
|
136
|
+
|
|
137
|
+
// Get another player's best entry
|
|
138
|
+
const otherBest = await kmClient.leaderboard.getBestEntry(
|
|
139
|
+
"all-time-high",
|
|
140
|
+
"desc",
|
|
141
|
+
"other-client-id"
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Common Patterns
|
|
146
|
+
|
|
147
|
+
### Example: Track High Scores
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Submit a new high score
|
|
151
|
+
await kmClient.leaderboard.upsertEntry(
|
|
152
|
+
"high-scores",
|
|
153
|
+
"desc",
|
|
154
|
+
score,
|
|
155
|
+
{ playerName: player.name },
|
|
156
|
+
{ timestamp: Date.now() }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Display top 10
|
|
160
|
+
const { items } = await kmClient.leaderboard.listEntries(
|
|
161
|
+
"high-scores",
|
|
162
|
+
"desc",
|
|
163
|
+
0,
|
|
164
|
+
10
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Example: Track Speed Run Times
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Submit completion time (lower is better)
|
|
172
|
+
await kmClient.leaderboard.upsertEntry(
|
|
173
|
+
"speed-run",
|
|
174
|
+
"asc",
|
|
175
|
+
completionTimeInSeconds,
|
|
176
|
+
{ playerName: player.name, difficulty: "hard" },
|
|
177
|
+
{}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Get personal best
|
|
181
|
+
const myBest = await kmClient.leaderboard.getBestEntry("speed-run", "asc");
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Key Points
|
|
185
|
+
|
|
186
|
+
- **Sort Direction**: Use `asc` when lower scores are better (e.g., completion time), `desc` when higher scores are better (e.g., points)
|
|
187
|
+
- **Insert vs Upsert**: Use `insertEntry` to keep all attempts, `upsertEntry` to keep only the latest/best
|
|
188
|
+
- **Metadata**: Public metadata is visible to all, private metadata is only accessible via API calls
|
|
189
|
+
- **Pagination**: Use skip/limit to implement leaderboard pages or "load more" functionality
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Core Kokimoki SDK for real-time collaborative game applications"
|
|
3
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
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** Use `getKmClient()` from `@kokimoki/app` to get the singleton client instance
|
|
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 dynamic stores with `kmClient.join()` and `kmClient.leave()` for room-based isolation (teams, chat rooms, breakout rooms)
|
|
16
|
+
- Use `kmClient.transact` for atomic state updates across single store or multiple stores
|
|
17
|
+
- Use `kmClient.storage.upload` and related API methods to handle file uploads (media, JSON, etc.) in application
|
|
18
|
+
- Use `kmClient.serverTimestamp()` for time-related matters as this will be synced among players
|
|
19
|
+
- Use `useSnapshot` hook from `valtio` to get reactive state inside React components
|
|
20
|
+
- Use AI integration API methods: `kmClient.ai.generateText`, `kmClient.ai.generateJson`, and `kmClient.ai.generateImage` with corresponding poll methods for AI capabilities
|
|
21
|
+
- Use i18n API methods: `kmClient.i18n.createI18n`, `kmClient.i18n.init`, and `kmClient.i18n.requestTranslation` for internationalization with AI-powered translations
|
|
22
|
+
- Use leaderboard API methods: `kmClient.leaderboard.insertEntry`, `kmClient.leaderboard.upsertEntry`, `kmClient.leaderboard.listEntries`, and `kmClient.leaderboard.getBestEntry` to add leaderboard capabilities to application
|
|
23
|
+
|
|
24
|
+
## Kokimoki Client
|
|
25
|
+
|
|
26
|
+
- Use `getKmClient()` to get the singleton Kokimoki client instance
|
|
27
|
+
- The `kmClient` provides the following key functionalities:
|
|
28
|
+
- `kmClient.store` and `kmClient.localStore` for creating stores
|
|
29
|
+
- `kmClient.join` and `kmClient.leave` for dynamic store lifecycle management
|
|
30
|
+
- `kmClient.transact` for atomic state updates
|
|
31
|
+
- `kmClient.serverTimestamp()` for synchronized timestamps
|
|
32
|
+
- `kmClient.id` for unique player identification
|
|
33
|
+
|
|
34
|
+
**Example: Getting the Client**
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { getKmClient } from "@kokimoki/app";
|
|
38
|
+
|
|
39
|
+
const kmClient = getKmClient();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Client ID
|
|
43
|
+
|
|
44
|
+
- Each client (player) has a unique `kmClient.id` (aka `clientId`), stable identifier that represents a player across multiple connections
|
|
45
|
+
(client sessions)
|
|
46
|
+
- The `kmClient.id` is persistent across client connections
|
|
47
|
+
- The `kmClient.id` remains consistent after user reconnect or page reload
|
|
48
|
+
- Use `kmClient.id` to identify players in global stores
|
|
49
|
+
- Each client (player) can have multiple connections, but all connections share the same `kmClient.id`
|
|
50
|
+
|
|
51
|
+
## Kokimoki Store
|
|
52
|
+
|
|
53
|
+
Kokimoki Store powered by `valtio` and `valtio-yjs` for real-time state management in Kokimoki game applications
|
|
54
|
+
|
|
55
|
+
### Store initialization
|
|
56
|
+
|
|
57
|
+
- Stores should be defined in `src/state/stores/`
|
|
58
|
+
- Store can be created in two ways:
|
|
59
|
+
- `kmClient.localStore` is used for data stored on the player device (local)
|
|
60
|
+
- `kmClient.store` is used for data shared among all players in a game (global)
|
|
61
|
+
|
|
62
|
+
**Example: Creating a Store**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { getKmClient } from "@kokimoki/app";
|
|
66
|
+
|
|
67
|
+
const kmClient = getKmClient();
|
|
68
|
+
|
|
69
|
+
interface State {
|
|
70
|
+
title: string;
|
|
71
|
+
count: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const initialState: State = {
|
|
75
|
+
title: "Store",
|
|
76
|
+
count: 0,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Initialize global store with initial state
|
|
80
|
+
export const store = kmClient.store<State>("store-name", initialState);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### State management
|
|
84
|
+
|
|
85
|
+
- State actions are functions that modify the store state by performing transactions
|
|
86
|
+
- Actions should be defined in `/src/state/actions/`
|
|
87
|
+
- Use async/await for all state transactions by `kmClient.transact` function for global stores
|
|
88
|
+
- Transactions are atomic and ensure state consistency
|
|
89
|
+
- ALWAYS update store state inside `kmClient.transact()` within action function
|
|
90
|
+
- Prefer using records, not arrays: store collections as `Record<string, T>` with timestamp keys for automatic sorting and better sync performance
|
|
91
|
+
|
|
92
|
+
**Example: Updating State**
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { store } from "../store";
|
|
96
|
+
|
|
97
|
+
// Update state
|
|
98
|
+
await kmClient.transact([store], ([state]) => {
|
|
99
|
+
state.title = "New store";
|
|
100
|
+
state.count += 1;
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Combining Stores
|
|
105
|
+
|
|
106
|
+
- Multiple stores can be updated in a single transaction
|
|
107
|
+
- Prefer to update stores in a single transaction to ensure state consistency
|
|
108
|
+
|
|
109
|
+
**Example: Multiple Stores**
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Update multiple stores in a single transaction
|
|
113
|
+
await kmClient.transact([store1, store2], ([state1, state2]) => {
|
|
114
|
+
state1.name = "My Store1";
|
|
115
|
+
state2.name = "My Store2";
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Reactive State in Components
|
|
120
|
+
|
|
121
|
+
- Use `useSnapshot` hook from `valtio` to get reactive state inside React components
|
|
122
|
+
- The component will re-render when the store state changes
|
|
123
|
+
|
|
124
|
+
**Example: Using State in Components**
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { useSnapshot } from "valtio";
|
|
128
|
+
import { store } from "../store";
|
|
129
|
+
|
|
130
|
+
const Component = () => {
|
|
131
|
+
// Get reactive snapshot of the store state
|
|
132
|
+
const { title, count } = useSnapshot(store.proxy);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div>
|
|
136
|
+
<h1>Title: {title}</h1>
|
|
137
|
+
<p>Count: {count}</p>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Dynamic Stores
|
|
144
|
+
|
|
145
|
+
For room-based state isolation (teams, chat rooms, breakout rooms), see [Dynamic Stores](./kokimoki-dynamic-stores.instructions.md).
|
|
146
|
+
|
|
147
|
+
## Server Time Synchronization
|
|
148
|
+
|
|
149
|
+
Kokimoki SDK implements a time synchronization system to ensure consistent timestamps across all connected clients, regardless of their local clock differences
|
|
150
|
+
|
|
151
|
+
- Use `kmClient.serverTimestamp()` to get the current server-synchronized timestamp
|
|
152
|
+
- The timestamp is a Epoch Unix Timestamp
|
|
153
|
+
- Use server timestamps for time-related matters like event scheduling, timeouts, timers, etc.
|
|
154
|
+
|
|
155
|
+
## Store Connections
|
|
156
|
+
|
|
157
|
+
Each Kokimoki store has a `connections` property that provides real-time presence information of all clients connected to that store.
|
|
158
|
+
|
|
159
|
+
### Accessing Connections
|
|
160
|
+
|
|
161
|
+
- Use `store.connections` to access the connections proxy for any Kokimoki store
|
|
162
|
+
- Use `store.connections.clientIds` to get a `Set` of online client IDs
|
|
163
|
+
- Use `useSnapshot` to get reactive updates when connections change
|
|
164
|
+
- **ALWAYS** use `useSnapshot(store.connections)` to get reactive updates when connections change
|
|
165
|
+
|
|
166
|
+
### Example: Track Online Players
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { useSnapshot } from "valtio";
|
|
170
|
+
import { globalStore } from "@/state/stores/global-store";
|
|
171
|
+
|
|
172
|
+
const Component = () => {
|
|
173
|
+
// Get online client IDs from store connections
|
|
174
|
+
const onlineClientIds = useSnapshot(globalStore.connections).clientIds;
|
|
175
|
+
|
|
176
|
+
// Check if specific player is online
|
|
177
|
+
const isPlayerOnline = onlineClientIds.has(playerId);
|
|
178
|
+
|
|
179
|
+
// Get count of online players
|
|
180
|
+
const onlineCount = onlineClientIds.size;
|
|
181
|
+
|
|
182
|
+
return <div>Online players: {onlineCount}</div>;
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Example: Display Player List with Online Status
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useSnapshot } from "valtio";
|
|
190
|
+
import { globalStore } from "@/state/stores/global-store";
|
|
191
|
+
|
|
192
|
+
const PlayerList = () => {
|
|
193
|
+
const players = useSnapshot(globalStore.proxy).players;
|
|
194
|
+
const onlineClientIds = useSnapshot(globalStore.connections).clientIds;
|
|
195
|
+
|
|
196
|
+
const playersList = Object.entries(players).map(([clientId, player]) => ({
|
|
197
|
+
clientId,
|
|
198
|
+
name: player.name,
|
|
199
|
+
|
|
200
|
+
isOnline: onlineClientIds.has(clientId),
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<ul>
|
|
205
|
+
{playersList.map((player) => (
|
|
206
|
+
<li key={player.clientId}>
|
|
207
|
+
{player.name} - {player.isOnline ? "Online" : "Offline"}
|
|
208
|
+
</li>
|
|
209
|
+
))}
|
|
210
|
+
</ul>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Key Points
|
|
216
|
+
|
|
217
|
+
- Each store has its own `connections` property
|
|
218
|
+
- `connections.clientIds` is a `Set<string>` containing connected client IDs
|
|
219
|
+
- Use `useSnapshot(store.connections)` to get reactive updates
|
|
220
|
+
- Players can have multiple browser tabs open, but all share the same `clientId`
|
|
221
|
+
- A player is considered online if their `clientId` is in the `clientIds` set
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "File storage and CDN uploads with Kokimoki SDK"
|
|
3
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Storage
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
For core SDK concepts, see [Kokimoki SDK](./kokimoki-sdk.instructions.md).
|
|
11
|
+
|
|
12
|
+
Access storage API via `kmClient.storage`.
|
|
13
|
+
|
|
14
|
+
## API Methods
|
|
15
|
+
|
|
16
|
+
### storage.upload(name, blob, tags?): Promise<Upload>
|
|
17
|
+
|
|
18
|
+
Uploads a file to storage.
|
|
19
|
+
|
|
20
|
+
**Parameters:**
|
|
21
|
+
|
|
22
|
+
- **name**: `string` filename
|
|
23
|
+
- **blob**: `Blob` object to upload
|
|
24
|
+
- **tags**: `string[]` (optional, default: [])
|
|
25
|
+
|
|
26
|
+
**Example:**
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const upload: Upload = await kmClient.storage.upload("filename.jpg", fileBlob, [
|
|
30
|
+
"tag1",
|
|
31
|
+
"tag2",
|
|
32
|
+
]);
|
|
33
|
+
// Use upload.url to access the media file
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### storage.listUploads(filter?, skip?, limit?): Promise<Paginated<Upload>>
|
|
37
|
+
|
|
38
|
+
Query uploaded files by filter and pagination
|
|
39
|
+
|
|
40
|
+
**Parameters:**
|
|
41
|
+
|
|
42
|
+
- **filter.clientId**: `string` Specific client (optional)
|
|
43
|
+
- **filter.mimeTypes**: `string[]` E.g., ['image/jpeg', 'image/png'] (optional)
|
|
44
|
+
- **filter.tags**: `string[]` All tags must match (optional)
|
|
45
|
+
- **skip**: `number` Pagination offset (default: 0)
|
|
46
|
+
- **limit**: `number` Max results (default: 100)
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Query uploads by tag and uploaded by this client
|
|
52
|
+
const { items, total } = await kmClient.storage.listUploads(
|
|
53
|
+
{ clientId: kmClient.id, tags: ["tag1"] },
|
|
54
|
+
skip,
|
|
55
|
+
limit
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### storage.updateUpload(id, update): Promise<Upload>
|
|
60
|
+
|
|
61
|
+
Replace uploaded file tags with new tags
|
|
62
|
+
|
|
63
|
+
**Parameters:**
|
|
64
|
+
|
|
65
|
+
- **id**: `string` Upload id
|
|
66
|
+
- **update.tags**: `string[]` (optional)
|
|
67
|
+
|
|
68
|
+
**Example:**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const updatedUpload: Upload = await kmClient.storage.updateUpload(upload.id, {
|
|
72
|
+
tags: ["new"],
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### storage.deleteUpload(id): Promise<{ acknowledged: boolean; deletedCount: number }>
|
|
77
|
+
|
|
78
|
+
Permanently delete uploaded file
|
|
79
|
+
|
|
80
|
+
**Parameters:**
|
|
81
|
+
|
|
82
|
+
- **id**: `string` Upload id
|
|
83
|
+
|
|
84
|
+
**Example:**
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
await kmClient.storage.deleteUpload(upload.id);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Types
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface Upload {
|
|
94
|
+
id: string; // unique id
|
|
95
|
+
url: string; // file url (CDN)
|
|
96
|
+
name: string; // original filename
|
|
97
|
+
size: number; // in bytes
|
|
98
|
+
mimeType: string;
|
|
99
|
+
clientId: string; // who uploaded
|
|
100
|
+
tags: string[]; // metadata for filtering and organization
|
|
101
|
+
completed: boolean; // upload status
|
|
102
|
+
createdAt: Date;
|
|
103
|
+
appId: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface Paginated<T> {
|
|
107
|
+
items: T[];
|
|
108
|
+
total: number;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Common Patterns
|
|
113
|
+
|
|
114
|
+
### Example: User-specific uploads
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Get uploaded files by clientId
|
|
118
|
+
const clientUploads = await kmClient.storage.listUploads({
|
|
119
|
+
clientId: kmClient.id,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Example: Filter by file type
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Get only uploaded images
|
|
127
|
+
const images = await kmClient.storage.listUploads({
|
|
128
|
+
mimeTypes: ["image/jpeg", "image/png"],
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Example: Tag-based uploads
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Upload file with tag
|
|
136
|
+
await kmClient.storage.upload("avatar.jpg", blob, ["profile"]);
|
|
137
|
+
|
|
138
|
+
// Query uploads by tag
|
|
139
|
+
const profileUploads = await kmClient.storage.listUploads({
|
|
140
|
+
tags: ["profile"],
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Example: Usage in Kokimoki Store
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Upload image from Blob
|
|
148
|
+
const upload = await kmClient.storage.upload("file.jpg", blob);
|
|
149
|
+
|
|
150
|
+
await kmClient.transact([store], (state) => {
|
|
151
|
+
// Add image to images array in the store
|
|
152
|
+
state.playerImages[upload.id] = { url: upload.url };
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Key Points
|
|
157
|
+
|
|
158
|
+
- **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
|
|
159
|
+
- **CDN**: File `upload.url` is public and can be used directly
|
|
160
|
+
- **Tags**: Use tag system to organize uploads
|
|
161
|
+
- **Pagination**: Use skip/limit to paginate results
|
|
162
|
+
- **Filtering**: Combine clientId, mimeTypes, and tags to query uploads
|
package/llms.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Kokimoki SDK Documentation
|
|
2
|
+
|
|
3
|
+
> Documentation for @kokimoki/app - the core SDK for building real-time multiplayer games.
|
|
4
|
+
|
|
5
|
+
## Instruction Files
|
|
6
|
+
|
|
7
|
+
The following instruction files provide detailed guidance for AI assistants:
|
|
8
|
+
|
|
9
|
+
### Core SDK
|
|
10
|
+
- docs/kokimoki-sdk.instructions.md: Core SDK usage including client initialization, stores, transactions, server time synchronization, and store connections. Start here for basic Kokimoki development.
|
|
11
|
+
|
|
12
|
+
### Dynamic Stores
|
|
13
|
+
- docs/kokimoki-dynamic-stores.instructions.md: Room-based state isolation for teams, chat rooms, and breakout rooms. Includes the useDynamicStore hook pattern and lifecycle management.
|
|
14
|
+
|
|
15
|
+
### Services
|
|
16
|
+
|
|
17
|
+
- docs/kokimoki-ai.instructions.md: AI-powered text and image generation using generateText() and generateImage() APIs.
|
|
18
|
+
|
|
19
|
+
- docs/kokimoki-storage.instructions.md: File storage and CDN uploads using the storage service.
|
|
20
|
+
|
|
21
|
+
- docs/kokimoki-i18n.instructions.md: Internationalization with AI-powered translation support.
|
|
22
|
+
|
|
23
|
+
- docs/kokimoki-leaderboard.instructions.md: Player rankings and score management.
|
|
24
|
+
|
|
25
|
+
## Key Concepts
|
|
26
|
+
|
|
27
|
+
- **KmClient**: Central client instance obtained via getKmClient()
|
|
28
|
+
- **Stores**: Synchronized state containers using Valtio proxies
|
|
29
|
+
- **Transactions**: Atomic state updates via kmClient.transact()
|
|
30
|
+
- **Dynamic Stores**: Room-scoped stores with join/leave lifecycle
|
|
31
|
+
- **Server Time**: Synchronized timestamps via kmClient.serverTimestamp()
|
|
32
|
+
|
|
33
|
+
## File Patterns
|
|
34
|
+
|
|
35
|
+
Instruction files use YAML frontmatter with:
|
|
36
|
+
- `description`: Brief description of the file's purpose
|
|
37
|
+
- `applyTo`: Glob pattern for when the instructions apply (e.g., "**/*.{ts,tsx}")
|
|
38
|
+
|
|
39
|
+
## Recommended Reading Order
|
|
40
|
+
|
|
41
|
+
1. docs/kokimoki-sdk.instructions.md (required - core concepts)
|
|
42
|
+
2. docs/kokimoki-dynamic-stores.instructions.md (if using rooms/teams)
|
|
43
|
+
3. Service-specific files as needed
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kokimoki/app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kokimoki app",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"docs",
|
|
11
|
+
"llms.txt"
|
|
10
12
|
],
|
|
11
13
|
"ts-node": {
|
|
12
14
|
"esm": true,
|
|
@@ -16,19 +18,12 @@
|
|
|
16
18
|
"test": "NODE_OPTIONS='--import tsx' mocha",
|
|
17
19
|
"prebuild": "node scripts/generate-version.js",
|
|
18
20
|
"build": "tsc",
|
|
19
|
-
"postbuild": "rollup --config && cp docs/instructions.md dist/llms.txt",
|
|
20
21
|
"dev": "tsc -w",
|
|
21
|
-
"docs": "typedoc src/index.ts"
|
|
22
|
-
"rollup": "rollup --config"
|
|
22
|
+
"docs": "typedoc src/index.ts"
|
|
23
23
|
},
|
|
24
24
|
"author": "Loquiz OÜ",
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@rollup/plugin-commonjs": "^25.0.7",
|
|
28
|
-
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
29
|
-
"@rollup/plugin-replace": "^5.0.5",
|
|
30
|
-
"@rollup/plugin-terser": "^0.4.4",
|
|
31
|
-
"@rollup/plugin-typescript": "^11.1.6",
|
|
32
27
|
"@types/chai": "^4",
|
|
33
28
|
"@types/mocha": "^10",
|
|
34
29
|
"@types/node": "^18",
|
|
@@ -36,8 +31,6 @@
|
|
|
36
31
|
"bson-objectid": "^2.0.4",
|
|
37
32
|
"chai": "^4",
|
|
38
33
|
"mocha": "^10",
|
|
39
|
-
"rollup": "^4.13.0",
|
|
40
|
-
"rollup-plugin-dts": "^6.1.0",
|
|
41
34
|
"ts-node": "^10",
|
|
42
35
|
"tsx": "^4",
|
|
43
36
|
"typedoc": "^0.24.8",
|
|
@@ -47,9 +40,12 @@
|
|
|
47
40
|
"derive-valtio": "^0.2.0",
|
|
48
41
|
"events": "^3.3.0",
|
|
49
42
|
"farmhash-modern": "^1.1.0",
|
|
43
|
+
"i18next": "^25.7.4",
|
|
44
|
+
"i18next-http-backend": "^3.0.2",
|
|
50
45
|
"typed-emitter": "^2.1.0",
|
|
51
46
|
"valtio": "^2.1.5",
|
|
52
47
|
"valtio-yjs": "^0.6.0",
|
|
53
|
-
"yjs": "^13.6.27"
|
|
48
|
+
"yjs": "^13.6.27",
|
|
49
|
+
"zod": "^4.3.5"
|
|
54
50
|
}
|
|
55
51
|
}
|