@kokimoki/app 1.17.0 → 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/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/kokimoki-client.d.ts +361 -0
- package/dist/core/kokimoki-client.js +819 -0
- package/dist/core/room-subscription-mode.d.ts +5 -0
- package/dist/core/room-subscription-mode.js +6 -0
- package/dist/core/room-subscription.d.ts +15 -0
- package/dist/core/room-subscription.js +53 -0
- package/dist/fields.d.ts +110 -0
- package/dist/fields.js +158 -0
- package/dist/index.d.ts +4 -8
- package/dist/index.js +4 -9
- package/dist/kokimoki-ai.d.ts +153 -0
- package/dist/kokimoki-ai.js +164 -0
- package/dist/kokimoki-awareness.d.ts +14 -13
- package/dist/kokimoki-awareness.js +41 -33
- package/dist/kokimoki-client-refactored.d.ts +80 -0
- package/dist/kokimoki-client-refactored.js +400 -0
- package/dist/kokimoki-client.d.ts +282 -76
- package/dist/kokimoki-client.js +295 -232
- package/dist/kokimoki-leaderboard.d.ts +175 -0
- package/dist/kokimoki-leaderboard.js +203 -0
- package/dist/kokimoki-schema.d.ts +113 -0
- package/dist/kokimoki-schema.js +162 -0
- package/dist/kokimoki-storage.d.ts +156 -0
- package/dist/kokimoki-storage.js +208 -0
- package/dist/kokimoki.min.d.ts +775 -110
- package/dist/kokimoki.min.js +4088 -2408
- package/dist/kokimoki.min.js.map +1 -1
- package/dist/llms.txt +657 -0
- package/dist/message-queue.d.ts +8 -0
- package/dist/message-queue.js +19 -0
- package/dist/protocol/ws-message/index.d.ts +3 -0
- package/dist/protocol/ws-message/index.js +3 -0
- package/dist/protocol/ws-message/reader.d.ts +11 -0
- package/dist/protocol/ws-message/reader.js +36 -0
- package/dist/protocol/ws-message/type.d.ts +11 -0
- package/dist/protocol/ws-message/type.js +12 -0
- package/dist/protocol/ws-message/writer.d.ts +9 -0
- package/dist/protocol/ws-message/writer.js +45 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/kokimoki-ai.d.ts +153 -0
- package/dist/services/kokimoki-ai.js +164 -0
- package/dist/services/kokimoki-leaderboard.d.ts +175 -0
- package/dist/services/kokimoki-leaderboard.js +203 -0
- package/dist/services/kokimoki-storage.d.ts +155 -0
- package/dist/services/kokimoki-storage.js +208 -0
- package/dist/stores/index.d.ts +3 -0
- package/dist/stores/index.js +3 -0
- package/dist/stores/kokimoki-local-store.d.ts +11 -0
- package/dist/stores/kokimoki-local-store.js +40 -0
- package/dist/stores/kokimoki-store.d.ts +22 -0
- package/dist/stores/kokimoki-store.js +117 -0
- package/dist/stores/kokimoki-transaction.d.ts +18 -0
- package/dist/stores/kokimoki-transaction.js +143 -0
- package/dist/synced-schema.d.ts +74 -0
- package/dist/synced-schema.js +83 -0
- package/dist/synced-store.d.ts +10 -0
- package/dist/synced-store.js +9 -0
- package/dist/synced-types.d.ts +47 -0
- package/dist/synced-types.js +67 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/utils/valtio.d.ts +7 -0
- package/dist/utils/valtio.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -1
- package/dist/ws-message-type copy.d.ts +6 -0
- package/dist/ws-message-type copy.js +7 -0
- package/package.json +4 -3
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kokimoki Leaderboard Service
|
|
3
|
+
*
|
|
4
|
+
* Provides efficient player ranking and score tracking with database indexes and optimized queries.
|
|
5
|
+
* Ideal for games with large numbers of players and competitive scoring.
|
|
6
|
+
*
|
|
7
|
+
* **Key Features:**
|
|
8
|
+
* - Efficient ranking with database indexes
|
|
9
|
+
* - Support for ascending (lowest-is-best) and descending (highest-is-best) sorting
|
|
10
|
+
* - Pagination for large leaderboards
|
|
11
|
+
* - Public and private metadata for entries
|
|
12
|
+
* - Insert (preserve all attempts) or upsert (keep latest only) modes
|
|
13
|
+
*
|
|
14
|
+
* **When to use Leaderboard API vs Global Store:**
|
|
15
|
+
*
|
|
16
|
+
* Use the **Leaderboard API** when:
|
|
17
|
+
* - You have a large number of entries (hundreds to thousands of players)
|
|
18
|
+
* - You need efficient ranking and sorting with database indexes
|
|
19
|
+
* - You want pagination and optimized queries for top scores
|
|
20
|
+
* - Memory and network efficiency are important
|
|
21
|
+
*
|
|
22
|
+
* Use a **Global Store** when:
|
|
23
|
+
* - You have a small number of players (typically under 100)
|
|
24
|
+
* - You need real-time updates and live leaderboard changes
|
|
25
|
+
* - You want to combine player scores with other game state
|
|
26
|
+
* - The leaderboard is temporary (session-based or reset frequently)
|
|
27
|
+
*
|
|
28
|
+
* Access via `kmClient.leaderboard`
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Submit a high score
|
|
33
|
+
* const { rank } = await kmClient.leaderboard.upsertEntry(
|
|
34
|
+
* 'high-scores',
|
|
35
|
+
* 'desc',
|
|
36
|
+
* 1500,
|
|
37
|
+
* { playerName: 'Alice' },
|
|
38
|
+
* {}
|
|
39
|
+
* );
|
|
40
|
+
*
|
|
41
|
+
* // Get top 10
|
|
42
|
+
* const { items } = await kmClient.leaderboard.listEntries('high-scores', 'desc', 0, 10);
|
|
43
|
+
*
|
|
44
|
+
* // Get player's best
|
|
45
|
+
* const best = await kmClient.leaderboard.getBestEntry('high-scores', 'desc');
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export class KokimokiLeaderboardService {
|
|
49
|
+
client;
|
|
50
|
+
constructor(client) {
|
|
51
|
+
this.client = client;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add a new entry to a leaderboard.
|
|
55
|
+
*
|
|
56
|
+
* Creates a new entry each time it's called, preserving all attempts. Use this when you want
|
|
57
|
+
* to track every score submission (e.g., all game attempts).
|
|
58
|
+
*
|
|
59
|
+
* @param leaderboardName The name of the leaderboard to add the entry to
|
|
60
|
+
* @param sortDir Sort direction: "asc" for lowest-is-best (e.g., completion time),
|
|
61
|
+
* "desc" for highest-is-best (e.g., points)
|
|
62
|
+
* @param score The numeric score value
|
|
63
|
+
* @param metadata Public metadata visible to all players (e.g., player name, level)
|
|
64
|
+
* @param privateMetadata Private metadata only accessible via API calls (e.g., session ID)
|
|
65
|
+
* @returns A promise resolving to an object with the entry's rank
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const { rank } = await kmClient.leaderboard.insertEntry(
|
|
70
|
+
* 'high-scores',
|
|
71
|
+
* 'desc',
|
|
72
|
+
* 1500,
|
|
73
|
+
* { playerName: 'Alice', level: 10 },
|
|
74
|
+
* { sessionId: 'abc123' }
|
|
75
|
+
* );
|
|
76
|
+
* console.log(`New rank: ${rank}`);
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
async insertEntry(leaderboardName, sortDir, score, metadata, privateMetadata) {
|
|
80
|
+
const res = await fetch(`${this.client.apiUrl}/leaderboard-entries`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: this.client.apiHeaders,
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
leaderboardName,
|
|
85
|
+
sortDir,
|
|
86
|
+
score,
|
|
87
|
+
metadata,
|
|
88
|
+
privateMetadata,
|
|
89
|
+
upsert: false,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
return await res.json();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Add or update the latest entry for the current client in a leaderboard.
|
|
96
|
+
*
|
|
97
|
+
* Replaces the previous entry if one exists for this client. Use this when you only want
|
|
98
|
+
* to keep the latest or best score per player (e.g., daily high score).
|
|
99
|
+
*
|
|
100
|
+
* @param leaderboardName The name of the leaderboard to upsert the entry in
|
|
101
|
+
* @param sortDir Sort direction: "asc" for lowest-is-best (e.g., completion time),
|
|
102
|
+
* "desc" for highest-is-best (e.g., points)
|
|
103
|
+
* @param score The numeric score value
|
|
104
|
+
* @param metadata Public metadata visible to all players (e.g., player name, completion time)
|
|
105
|
+
* @param privateMetadata Private metadata only accessible via API calls (e.g., device ID)
|
|
106
|
+
* @returns A promise resolving to an object with the entry's updated rank
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const { rank } = await kmClient.leaderboard.upsertEntry(
|
|
111
|
+
* 'daily-scores',
|
|
112
|
+
* 'desc',
|
|
113
|
+
* 2000,
|
|
114
|
+
* { playerName: 'Bob', completionTime: 120 },
|
|
115
|
+
* { deviceId: 'xyz789' }
|
|
116
|
+
* );
|
|
117
|
+
* console.log(`Updated rank: ${rank}`);
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
async upsertEntry(leaderboardName, sortDir, score, metadata, privateMetadata) {
|
|
121
|
+
const res = await fetch(`${this.client.apiUrl}/leaderboard-entries`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: this.client.apiHeaders,
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
leaderboardName,
|
|
126
|
+
sortDir,
|
|
127
|
+
score,
|
|
128
|
+
metadata,
|
|
129
|
+
privateMetadata,
|
|
130
|
+
upsert: true,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
return await res.json();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* List entries in a leaderboard with pagination.
|
|
137
|
+
*
|
|
138
|
+
* Retrieves a sorted list of leaderboard entries. Use skip and limit parameters for
|
|
139
|
+
* pagination (e.g., showing top 10, or implementing "load more" functionality).
|
|
140
|
+
*
|
|
141
|
+
* @param leaderboardName The name of the leaderboard to query
|
|
142
|
+
* @param sortDir Sort direction: "asc" for lowest-is-best (e.g., completion time),
|
|
143
|
+
* "desc" for highest-is-best (e.g., points)
|
|
144
|
+
* @param skip Number of entries to skip for pagination (default: 0)
|
|
145
|
+
* @param limit Maximum number of entries to return (default: 100)
|
|
146
|
+
* @returns A promise resolving to a paginated list of entries with rank, score, and metadata
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* // Get top 10 scores
|
|
151
|
+
* const { items, total } = await kmClient.leaderboard.listEntries(
|
|
152
|
+
* 'weekly-scores',
|
|
153
|
+
* 'desc',
|
|
154
|
+
* 0,
|
|
155
|
+
* 10
|
|
156
|
+
* );
|
|
157
|
+
*
|
|
158
|
+
* items.forEach(entry => {
|
|
159
|
+
* console.log(`Rank ${entry.rank}: ${entry.metadata.playerName} - ${entry.score}`);
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
async listEntries(leaderboardName, sortDir, skip = 0, limit = 100) {
|
|
164
|
+
const encodedLeaderboardName = encodeURIComponent(leaderboardName);
|
|
165
|
+
const res = await fetch(`${this.client.apiUrl}/leaderboard-entries?leaderboardName=${encodedLeaderboardName}&sortDir=${sortDir}&skip=${skip}&limit=${limit}`, {
|
|
166
|
+
headers: this.client.apiHeaders,
|
|
167
|
+
});
|
|
168
|
+
return await res.json();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get the best entry for a specific client in a leaderboard.
|
|
172
|
+
*
|
|
173
|
+
* Retrieves the highest-ranked entry for a client based on the sort direction.
|
|
174
|
+
* Defaults to the current client if no clientId is provided.
|
|
175
|
+
*
|
|
176
|
+
* @param leaderboardName The name of the leaderboard to query
|
|
177
|
+
* @param sortDir Sort direction: "asc" for lowest-is-best (e.g., completion time),
|
|
178
|
+
* "desc" for highest-is-best (e.g., points)
|
|
179
|
+
* @param clientId The client ID to get the best entry for (optional, defaults to current client)
|
|
180
|
+
* @returns A promise resolving to the best entry with rank, score, and metadata
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* // Get current client's best entry
|
|
185
|
+
* const myBest = await kmClient.leaderboard.getBestEntry('all-time-high', 'desc');
|
|
186
|
+
* console.log(`My best: Rank ${myBest.rank}, Score ${myBest.score}`);
|
|
187
|
+
*
|
|
188
|
+
* // Get another player's best entry
|
|
189
|
+
* const otherBest = await kmClient.leaderboard.getBestEntry(
|
|
190
|
+
* 'all-time-high',
|
|
191
|
+
* 'desc',
|
|
192
|
+
* 'other-client-id'
|
|
193
|
+
* );
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
async getBestEntry(leaderboardName, sortDir, clientId) {
|
|
197
|
+
const encodedLeaderboardName = encodeURIComponent(leaderboardName);
|
|
198
|
+
const res = await fetch(`${this.client.apiUrl}/leaderboard-entries/best?leaderboardName=${encodedLeaderboardName}&sortDir=${sortDir}&clientId=${clientId || this.client.id}`, {
|
|
199
|
+
headers: this.client.apiHeaders,
|
|
200
|
+
});
|
|
201
|
+
return await res.json();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { KokimokiClient } from "../core";
|
|
2
|
+
import type { Paginated, Upload } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Kokimoki Storage Service
|
|
5
|
+
*
|
|
6
|
+
* Provides file upload and management capabilities for game applications. Ideal for media files
|
|
7
|
+
* (images, videos, audio) and other data not suitable for real-time stores (JSON, text files).
|
|
8
|
+
*
|
|
9
|
+
* **Key Features:**
|
|
10
|
+
* - Upload files to cloud storage with CDN delivery
|
|
11
|
+
* - Tag-based organization and filtering
|
|
12
|
+
* - Query uploads by client, MIME type, or tags
|
|
13
|
+
* - Pagination support for large collections
|
|
14
|
+
* - Public CDN URLs for direct access
|
|
15
|
+
*
|
|
16
|
+
* **Common Use Cases:**
|
|
17
|
+
* - Player avatars and profile images
|
|
18
|
+
* - Game screenshots and replays
|
|
19
|
+
* - User-generated content
|
|
20
|
+
* - Asset uploads (levels, maps, custom skins)
|
|
21
|
+
* - JSON configuration files
|
|
22
|
+
*
|
|
23
|
+
* Access via `kmClient.storage`
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Upload an image
|
|
28
|
+
* const upload = await kmClient.storage.upload('avatar.jpg', imageBlob, ['profile']);
|
|
29
|
+
*
|
|
30
|
+
* // Query user's uploads
|
|
31
|
+
* const myUploads = await kmClient.storage.listUploads({
|
|
32
|
+
* clientId: kmClient.id
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Use in store
|
|
36
|
+
* await kmClient.transact([store], (state) => {
|
|
37
|
+
* state.playerAvatar = upload.url;
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare class KokimokiStorageService {
|
|
42
|
+
private readonly client;
|
|
43
|
+
constructor(client: KokimokiClient);
|
|
44
|
+
private createUpload;
|
|
45
|
+
private uploadChunks;
|
|
46
|
+
private completeUpload;
|
|
47
|
+
/**
|
|
48
|
+
* Upload a file to cloud storage.
|
|
49
|
+
*
|
|
50
|
+
* Uploads a file (Blob) to Kokimoki storage and returns an Upload object with a public CDN URL.
|
|
51
|
+
* Files are automatically chunked for efficient upload. The returned URL can be used directly
|
|
52
|
+
* in your application (e.g., in img tags or store state).
|
|
53
|
+
*
|
|
54
|
+
* @param name The filename for the upload
|
|
55
|
+
* @param blob The Blob object containing the file data
|
|
56
|
+
* @param tags Optional array of tags for organizing and filtering uploads (default: [])
|
|
57
|
+
* @returns A promise resolving to the Upload object with CDN URL and metadata
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* // Upload image with tags
|
|
62
|
+
* const upload = await kmClient.storage.upload(
|
|
63
|
+
* 'avatar.jpg',
|
|
64
|
+
* imageBlob,
|
|
65
|
+
* ['profile', 'avatar']
|
|
66
|
+
* );
|
|
67
|
+
*
|
|
68
|
+
* // Use the CDN URL
|
|
69
|
+
* console.log(upload.url); // https://cdn.kokimoki.com/...
|
|
70
|
+
*
|
|
71
|
+
* // Store in game state
|
|
72
|
+
* await kmClient.transact([store], (state) => {
|
|
73
|
+
* state.players[kmClient.id].avatar = upload.url;
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
upload(name: string, blob: Blob, tags?: string[]): Promise<Upload>;
|
|
78
|
+
/**
|
|
79
|
+
* Update metadata for an existing upload.
|
|
80
|
+
*
|
|
81
|
+
* Allows you to replace the tags associated with an upload. Useful for reorganizing
|
|
82
|
+
* or recategorizing uploaded files.
|
|
83
|
+
*
|
|
84
|
+
* @param id The upload ID to update
|
|
85
|
+
* @param update Object containing the new tags array
|
|
86
|
+
* @returns A promise resolving to the updated Upload object
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* // Update tags
|
|
91
|
+
* const updated = await kmClient.storage.updateUpload(upload.id, {
|
|
92
|
+
* tags: ['archived', 'old-profile']
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
updateUpload(id: string, update: {
|
|
97
|
+
tags?: string[];
|
|
98
|
+
}): Promise<Upload>;
|
|
99
|
+
/**
|
|
100
|
+
* Query uploaded files with filtering and pagination.
|
|
101
|
+
*
|
|
102
|
+
* Retrieves a list of uploads matching the specified criteria. Supports filtering by
|
|
103
|
+
* client (uploader), MIME types, and tags. Use pagination for large collections.
|
|
104
|
+
*
|
|
105
|
+
* @param filter Optional filter criteria
|
|
106
|
+
* @param filter.clientId Filter by uploader's client ID
|
|
107
|
+
* @param filter.mimeTypes Filter by MIME types (e.g., ['image/jpeg', 'image/png'])
|
|
108
|
+
* @param filter.tags Filter by tags (all specified tags must match)
|
|
109
|
+
* @param skip Number of results to skip for pagination (default: 0)
|
|
110
|
+
* @param limit Maximum number of results to return (default: 100)
|
|
111
|
+
* @returns A promise resolving to paginated Upload results with total count
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // Get current user's images
|
|
116
|
+
* const myImages = await kmClient.storage.listUploads({
|
|
117
|
+
* clientId: kmClient.id,
|
|
118
|
+
* mimeTypes: ['image/jpeg', 'image/png']
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* // Get all profile avatars
|
|
122
|
+
* const avatars = await kmClient.storage.listUploads({
|
|
123
|
+
* tags: ['profile', 'avatar']
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* // Pagination
|
|
127
|
+
* const page2 = await kmClient.storage.listUploads({}, 10, 10);
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
listUploads(filter?: {
|
|
131
|
+
clientId?: string;
|
|
132
|
+
mimeTypes?: string[];
|
|
133
|
+
tags?: string[];
|
|
134
|
+
}, skip?: number, limit?: number): Promise<Paginated<Upload>>;
|
|
135
|
+
/**
|
|
136
|
+
* Permanently delete an uploaded file.
|
|
137
|
+
*
|
|
138
|
+
* Removes the file from cloud storage and the CDN. The URL will no longer be accessible.
|
|
139
|
+
* This operation cannot be undone.
|
|
140
|
+
*
|
|
141
|
+
* @param id The upload ID to delete
|
|
142
|
+
* @returns A promise resolving to deletion confirmation
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* // Delete an upload
|
|
147
|
+
* const result = await kmClient.storage.deleteUpload(upload.id);
|
|
148
|
+
* console.log(`Deleted: ${result.deletedCount} file(s)`);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
deleteUpload(id: string): Promise<{
|
|
152
|
+
acknowledged: boolean;
|
|
153
|
+
deletedCount: number;
|
|
154
|
+
}>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kokimoki Storage Service
|
|
3
|
+
*
|
|
4
|
+
* Provides file upload and management capabilities for game applications. Ideal for media files
|
|
5
|
+
* (images, videos, audio) and other data not suitable for real-time stores (JSON, text files).
|
|
6
|
+
*
|
|
7
|
+
* **Key Features:**
|
|
8
|
+
* - Upload files to cloud storage with CDN delivery
|
|
9
|
+
* - Tag-based organization and filtering
|
|
10
|
+
* - Query uploads by client, MIME type, or tags
|
|
11
|
+
* - Pagination support for large collections
|
|
12
|
+
* - Public CDN URLs for direct access
|
|
13
|
+
*
|
|
14
|
+
* **Common Use Cases:**
|
|
15
|
+
* - Player avatars and profile images
|
|
16
|
+
* - Game screenshots and replays
|
|
17
|
+
* - User-generated content
|
|
18
|
+
* - Asset uploads (levels, maps, custom skins)
|
|
19
|
+
* - JSON configuration files
|
|
20
|
+
*
|
|
21
|
+
* Access via `kmClient.storage`
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Upload an image
|
|
26
|
+
* const upload = await kmClient.storage.upload('avatar.jpg', imageBlob, ['profile']);
|
|
27
|
+
*
|
|
28
|
+
* // Query user's uploads
|
|
29
|
+
* const myUploads = await kmClient.storage.listUploads({
|
|
30
|
+
* clientId: kmClient.id
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Use in store
|
|
34
|
+
* await kmClient.transact([store], (state) => {
|
|
35
|
+
* state.playerAvatar = upload.url;
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class KokimokiStorageService {
|
|
40
|
+
client;
|
|
41
|
+
constructor(client) {
|
|
42
|
+
this.client = client;
|
|
43
|
+
}
|
|
44
|
+
// Storage
|
|
45
|
+
async createUpload(name, blob, tags) {
|
|
46
|
+
const res = await fetch(`${this.client.apiUrl}/uploads`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: this.client.apiHeaders,
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
name,
|
|
51
|
+
size: blob.size,
|
|
52
|
+
mimeType: blob.type,
|
|
53
|
+
tags,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
return await res.json();
|
|
57
|
+
}
|
|
58
|
+
async uploadChunks(blob, chunkSize, signedUrls) {
|
|
59
|
+
return await Promise.all(signedUrls.map(async (url, index) => {
|
|
60
|
+
const start = index * chunkSize;
|
|
61
|
+
const end = Math.min(start + chunkSize, blob.size);
|
|
62
|
+
const chunk = blob.slice(start, end);
|
|
63
|
+
const res = await fetch(url, { method: "PUT", body: chunk });
|
|
64
|
+
return JSON.parse(res.headers.get("ETag") || '""');
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async completeUpload(id, etags) {
|
|
68
|
+
const res = await fetch(`${this.client.apiUrl}/uploads/${id}`, {
|
|
69
|
+
method: "PUT",
|
|
70
|
+
headers: this.client.apiHeaders,
|
|
71
|
+
body: JSON.stringify({ etags }),
|
|
72
|
+
});
|
|
73
|
+
return await res.json();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Upload a file to cloud storage.
|
|
77
|
+
*
|
|
78
|
+
* Uploads a file (Blob) to Kokimoki storage and returns an Upload object with a public CDN URL.
|
|
79
|
+
* Files are automatically chunked for efficient upload. The returned URL can be used directly
|
|
80
|
+
* in your application (e.g., in img tags or store state).
|
|
81
|
+
*
|
|
82
|
+
* @param name The filename for the upload
|
|
83
|
+
* @param blob The Blob object containing the file data
|
|
84
|
+
* @param tags Optional array of tags for organizing and filtering uploads (default: [])
|
|
85
|
+
* @returns A promise resolving to the Upload object with CDN URL and metadata
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // Upload image with tags
|
|
90
|
+
* const upload = await kmClient.storage.upload(
|
|
91
|
+
* 'avatar.jpg',
|
|
92
|
+
* imageBlob,
|
|
93
|
+
* ['profile', 'avatar']
|
|
94
|
+
* );
|
|
95
|
+
*
|
|
96
|
+
* // Use the CDN URL
|
|
97
|
+
* console.log(upload.url); // https://cdn.kokimoki.com/...
|
|
98
|
+
*
|
|
99
|
+
* // Store in game state
|
|
100
|
+
* await kmClient.transact([store], (state) => {
|
|
101
|
+
* state.players[kmClient.id].avatar = upload.url;
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async upload(name, blob, tags = []) {
|
|
106
|
+
const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
|
|
107
|
+
const etags = await this.uploadChunks(blob, chunkSize, urls);
|
|
108
|
+
return await this.completeUpload(id, etags);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Update metadata for an existing upload.
|
|
112
|
+
*
|
|
113
|
+
* Allows you to replace the tags associated with an upload. Useful for reorganizing
|
|
114
|
+
* or recategorizing uploaded files.
|
|
115
|
+
*
|
|
116
|
+
* @param id The upload ID to update
|
|
117
|
+
* @param update Object containing the new tags array
|
|
118
|
+
* @returns A promise resolving to the updated Upload object
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* // Update tags
|
|
123
|
+
* const updated = await kmClient.storage.updateUpload(upload.id, {
|
|
124
|
+
* tags: ['archived', 'old-profile']
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
async updateUpload(id, update) {
|
|
129
|
+
const res = await fetch(`${this.client.apiUrl}/uploads/${id}`, {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
headers: this.client.apiHeaders,
|
|
132
|
+
body: JSON.stringify(update),
|
|
133
|
+
});
|
|
134
|
+
return await res.json();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Query uploaded files with filtering and pagination.
|
|
138
|
+
*
|
|
139
|
+
* Retrieves a list of uploads matching the specified criteria. Supports filtering by
|
|
140
|
+
* client (uploader), MIME types, and tags. Use pagination for large collections.
|
|
141
|
+
*
|
|
142
|
+
* @param filter Optional filter criteria
|
|
143
|
+
* @param filter.clientId Filter by uploader's client ID
|
|
144
|
+
* @param filter.mimeTypes Filter by MIME types (e.g., ['image/jpeg', 'image/png'])
|
|
145
|
+
* @param filter.tags Filter by tags (all specified tags must match)
|
|
146
|
+
* @param skip Number of results to skip for pagination (default: 0)
|
|
147
|
+
* @param limit Maximum number of results to return (default: 100)
|
|
148
|
+
* @returns A promise resolving to paginated Upload results with total count
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* // Get current user's images
|
|
153
|
+
* const myImages = await kmClient.storage.listUploads({
|
|
154
|
+
* clientId: kmClient.id,
|
|
155
|
+
* mimeTypes: ['image/jpeg', 'image/png']
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* // Get all profile avatars
|
|
159
|
+
* const avatars = await kmClient.storage.listUploads({
|
|
160
|
+
* tags: ['profile', 'avatar']
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* // Pagination
|
|
164
|
+
* const page2 = await kmClient.storage.listUploads({}, 10, 10);
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
async listUploads(filter = {}, skip = 0, limit = 100) {
|
|
168
|
+
const url = new URL("/uploads", this.client.apiUrl);
|
|
169
|
+
url.searchParams.set("skip", skip.toString());
|
|
170
|
+
url.searchParams.set("limit", limit.toString());
|
|
171
|
+
if (filter.clientId) {
|
|
172
|
+
url.searchParams.set("clientId", filter.clientId);
|
|
173
|
+
}
|
|
174
|
+
if (filter.mimeTypes) {
|
|
175
|
+
url.searchParams.set("mimeTypes", filter.mimeTypes.join());
|
|
176
|
+
}
|
|
177
|
+
if (filter.tags) {
|
|
178
|
+
url.searchParams.set("tags", filter.tags.join());
|
|
179
|
+
}
|
|
180
|
+
const res = await fetch(url.href, {
|
|
181
|
+
headers: this.client.apiHeaders,
|
|
182
|
+
});
|
|
183
|
+
return await res.json();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Permanently delete an uploaded file.
|
|
187
|
+
*
|
|
188
|
+
* Removes the file from cloud storage and the CDN. The URL will no longer be accessible.
|
|
189
|
+
* This operation cannot be undone.
|
|
190
|
+
*
|
|
191
|
+
* @param id The upload ID to delete
|
|
192
|
+
* @returns A promise resolving to deletion confirmation
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // Delete an upload
|
|
197
|
+
* const result = await kmClient.storage.deleteUpload(upload.id);
|
|
198
|
+
* console.log(`Deleted: ${result.deletedCount} file(s)`);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async deleteUpload(id) {
|
|
202
|
+
const res = await fetch(`${this.client.apiUrl}/uploads/${id}`, {
|
|
203
|
+
method: "DELETE",
|
|
204
|
+
headers: this.client.apiHeaders,
|
|
205
|
+
});
|
|
206
|
+
return await res.json();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
2
|
+
export declare class KokimokiLocalStore<T extends object> extends KokimokiStore<T> {
|
|
3
|
+
private readonly localRoomName;
|
|
4
|
+
private _stateKey?;
|
|
5
|
+
private get stateKey();
|
|
6
|
+
constructor(localRoomName: string, defaultState: T);
|
|
7
|
+
getInitialUpdate(appId: string, clientId: string): {
|
|
8
|
+
roomHash: number;
|
|
9
|
+
initialUpdate: Uint8Array<ArrayBufferLike> | undefined;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fingerprint32 } from "farmhash-modern";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { RoomSubscriptionMode } from "../core";
|
|
4
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
5
|
+
export class KokimokiLocalStore extends KokimokiStore {
|
|
6
|
+
localRoomName;
|
|
7
|
+
_stateKey;
|
|
8
|
+
get stateKey() {
|
|
9
|
+
if (!this._stateKey) {
|
|
10
|
+
throw new Error("Not initialized");
|
|
11
|
+
}
|
|
12
|
+
return this._stateKey;
|
|
13
|
+
}
|
|
14
|
+
constructor(localRoomName, defaultState) {
|
|
15
|
+
super(`/l/${localRoomName}`, defaultState, RoomSubscriptionMode.ReadWrite);
|
|
16
|
+
this.localRoomName = localRoomName;
|
|
17
|
+
// Synchronize doc changes to local storage
|
|
18
|
+
// TODO: maybe do not serialize full state every time
|
|
19
|
+
this.doc.on("update", () => {
|
|
20
|
+
const value = Y.encodeStateAsUpdate(this.doc);
|
|
21
|
+
const valueString = String.fromCharCode(...value);
|
|
22
|
+
const valueBase64 = btoa(valueString);
|
|
23
|
+
localStorage.setItem(this.stateKey, valueBase64);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
getInitialUpdate(appId, clientId) {
|
|
27
|
+
this._stateKey = `${appId}/${clientId}/${this.localRoomName}`;
|
|
28
|
+
// get initial update from local storage
|
|
29
|
+
let initialUpdate = undefined;
|
|
30
|
+
const state = localStorage.getItem(this.stateKey);
|
|
31
|
+
if (state) {
|
|
32
|
+
const valueString = atob(state);
|
|
33
|
+
initialUpdate = Uint8Array.from(valueString, (c) => c.charCodeAt(0));
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
roomHash: fingerprint32(this.roomName),
|
|
37
|
+
initialUpdate,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Snapshot } from "valtio/vanilla";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { type KokimokiClient, RoomSubscriptionMode } from "../core";
|
|
4
|
+
export declare class KokimokiStore<T extends object> {
|
|
5
|
+
readonly roomName: string;
|
|
6
|
+
readonly defaultValue: T;
|
|
7
|
+
readonly mode: RoomSubscriptionMode;
|
|
8
|
+
readonly doc: Y.Doc;
|
|
9
|
+
readonly proxy: T;
|
|
10
|
+
readonly docRoot: Y.Map<unknown>;
|
|
11
|
+
readonly connections: {
|
|
12
|
+
connectionIds: Set<string>;
|
|
13
|
+
clientIds: Set<string>;
|
|
14
|
+
};
|
|
15
|
+
private _unsubscribeConnectionsHandler;
|
|
16
|
+
constructor(roomName: string, defaultValue: T, mode?: RoomSubscriptionMode);
|
|
17
|
+
get(): Snapshot<T>;
|
|
18
|
+
subscribe(set: (value: Snapshot<T>) => void): () => void;
|
|
19
|
+
onJoin(client: KokimokiClient<any>): Promise<void>;
|
|
20
|
+
onBeforeLeave(_client: KokimokiClient): Promise<void>;
|
|
21
|
+
onLeave(_client: KokimokiClient): Promise<void>;
|
|
22
|
+
}
|