@kokimoki/app 1.6.0 → 1.7.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.
@@ -0,0 +1,68 @@
1
+ import type TypedEmitter from "typed-emitter";
2
+ import type { KokimokiClientEventsRefactored } from "./types/events";
3
+ import type { Upload } from "./types/upload";
4
+ import type { Paginated } from "./types/common";
5
+ import { KokimokiTransaction } from "./kokimoki-transaction";
6
+ import type { KokimokiStore } from "./kokimoki-store";
7
+ import type { SyncedGeneric } from "./synced-schema";
8
+ import { RoomSubscriptionMode } from "./room-subscription-mode";
9
+ declare const KokimokiClientRefactored_base: new () => TypedEmitter<KokimokiClientEventsRefactored>;
10
+ export declare class KokimokiClientRefactored<ClientContextT = any> extends KokimokiClientRefactored_base {
11
+ readonly host: string;
12
+ readonly appId: string;
13
+ readonly code: string;
14
+ private _wsUrl;
15
+ private _apiUrl;
16
+ private _id?;
17
+ private _token?;
18
+ private _apiHeaders?;
19
+ private _serverTimeOffset;
20
+ private _clientContext?;
21
+ private _ws?;
22
+ private _subscriptions;
23
+ private _stores;
24
+ private _reqPromises;
25
+ private _transactionResolves;
26
+ private _roomHashes;
27
+ private _roomHashToDoc;
28
+ private _connected;
29
+ private _connectPromise?;
30
+ private _messageId;
31
+ constructor(host: string, appId: string, code?: string);
32
+ get id(): string;
33
+ get token(): string;
34
+ get apiUrl(): string;
35
+ get apiHeaders(): Headers;
36
+ get clientContext(): ClientContextT & ({} | null);
37
+ get connected(): boolean;
38
+ get ws(): WebSocket;
39
+ connect(): Promise<void>;
40
+ private handleInitMessage;
41
+ private handleBinaryMessage;
42
+ private handleErrorMessage;
43
+ private handleSubscribeResMessage;
44
+ private handleRoomUpdateMessage;
45
+ serverTimestamp(): number;
46
+ patchRoomState(room: string, update: Uint8Array): Promise<any>;
47
+ private createUpload;
48
+ private uploadChunks;
49
+ private completeUpload;
50
+ upload(name: string, blob: Blob, tags?: string[]): Promise<Upload>;
51
+ updateUpload(id: string, update: {
52
+ tags?: string[];
53
+ }): Promise<Upload>;
54
+ listUploads(filter?: {
55
+ clientId?: string;
56
+ mimeTypes?: string[];
57
+ tags?: string[];
58
+ }, skip?: number, limit?: number): Promise<Paginated<Upload>>;
59
+ deleteUpload(id: string): Promise<{
60
+ acknowledged: boolean;
61
+ deletedCount: number;
62
+ }>;
63
+ exposeScriptingContext(context: any): Promise<void>;
64
+ private sendSubscriptionReq;
65
+ join<T extends SyncedGeneric<unknown>>(store: KokimokiStore<T>, mode?: RoomSubscriptionMode): Promise<void>;
66
+ transact(handler: (t: KokimokiTransaction) => void): Promise<void>;
67
+ }
68
+ export {};
@@ -0,0 +1,394 @@
1
+ import EventEmitter from "events";
2
+ import { KOKIMOKI_APP_VERSION } from "./version";
3
+ import * as Y from "yjs";
4
+ import { KokimokiTransaction } from "./kokimoki-transaction";
5
+ import { WsMessageType } from "./ws-message-type";
6
+ import { WsMessageWriter } from "./ws-message-writer";
7
+ import { WsMessageReader } from "./ws-message-reader";
8
+ import { RoomSubscriptionMode } from "./room-subscription-mode";
9
+ export class KokimokiClientRefactored extends EventEmitter {
10
+ host;
11
+ appId;
12
+ code;
13
+ _wsUrl;
14
+ _apiUrl;
15
+ _id;
16
+ _token;
17
+ _apiHeaders;
18
+ _serverTimeOffset = 0;
19
+ _clientContext;
20
+ _ws;
21
+ _subscriptions = new Map();
22
+ _stores = new Map();
23
+ _reqPromises = new Map();
24
+ _transactionResolves = new Map();
25
+ _roomHashes = new Map();
26
+ _roomHashToDoc = new Map();
27
+ _connected = false;
28
+ _connectPromise;
29
+ _messageId = 0;
30
+ constructor(host, appId, code = "") {
31
+ super();
32
+ this.host = host;
33
+ this.appId = appId;
34
+ this.code = code;
35
+ // Set up the URLs
36
+ const secure = this.host.indexOf(":") === -1;
37
+ this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
38
+ this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
39
+ }
40
+ get id() {
41
+ if (!this._id) {
42
+ throw new Error("Client not connected");
43
+ }
44
+ return this._id;
45
+ }
46
+ get token() {
47
+ if (!this._token) {
48
+ throw new Error("Client not connected");
49
+ }
50
+ return this._token;
51
+ }
52
+ get apiUrl() {
53
+ if (!this._apiUrl) {
54
+ throw new Error("Client not connected");
55
+ }
56
+ return this._apiUrl;
57
+ }
58
+ get apiHeaders() {
59
+ if (!this._apiHeaders) {
60
+ throw new Error("Client not connected");
61
+ }
62
+ return this._apiHeaders;
63
+ }
64
+ get clientContext() {
65
+ if (this._clientContext === undefined) {
66
+ throw new Error("Client not connected");
67
+ }
68
+ return this._clientContext;
69
+ }
70
+ get connected() {
71
+ return this._connected;
72
+ }
73
+ get ws() {
74
+ if (!this._ws) {
75
+ throw new Error("Not connected");
76
+ }
77
+ return this._ws;
78
+ }
79
+ async connect() {
80
+ if (this._connectPromise) {
81
+ return await this._connectPromise;
82
+ }
83
+ this._ws = new WebSocket(`${this._wsUrl}/apps/${this.appId}?clientVersion=${KOKIMOKI_APP_VERSION}`);
84
+ this._ws.binaryType = "arraybuffer";
85
+ this._connectPromise = new Promise((onInit) => {
86
+ // Fetch the auth token
87
+ let clientToken = localStorage.getItem("KM_TOKEN");
88
+ // Send the app token on first connect
89
+ this.ws.onopen = () => {
90
+ this.ws.send(JSON.stringify({ type: "auth", code: this.code, token: clientToken }));
91
+ };
92
+ this.ws.onclose = () => {
93
+ this._connected = false;
94
+ this._connectPromise = undefined;
95
+ this._ws = undefined;
96
+ // Clean up
97
+ this._reqPromises.clear();
98
+ this._transactionResolves.clear();
99
+ this._roomHashes.clear();
100
+ this._roomHashToDoc.clear();
101
+ this._subscriptions.clear();
102
+ // Emit disconnected event
103
+ this.emit("disconnected");
104
+ // Attempt to reconnect
105
+ // setTimeout(async () => await this.connect(), 2000);
106
+ };
107
+ this.ws.onmessage = (e) => {
108
+ console.log(`Received WS message: ${e.data}`);
109
+ // Handle JSON messages
110
+ if (typeof e.data === "string") {
111
+ const message = JSON.parse(e.data);
112
+ switch (message.type) {
113
+ case "serverTime": {
114
+ this._serverTimeOffset = Date.now() - message.serverTime;
115
+ console.log(`Server time offset: ${this._serverTimeOffset}`);
116
+ break;
117
+ }
118
+ case "init":
119
+ this.handleInitMessage(message);
120
+ onInit();
121
+ break;
122
+ }
123
+ return;
124
+ }
125
+ // Handle binary messages
126
+ this.handleBinaryMessage(e.data);
127
+ };
128
+ });
129
+ await this._connectPromise;
130
+ }
131
+ handleInitMessage(message) {
132
+ localStorage.setItem("KM_TOKEN", message.clientToken);
133
+ this._id = message.clientId;
134
+ this._token = message.appToken;
135
+ this._clientContext = message.clientContext;
136
+ // Set up the auth headers
137
+ this._apiHeaders = new Headers({
138
+ Authorization: `Bearer ${this.token}`,
139
+ "Content-Type": "application/json",
140
+ });
141
+ this._connected = true;
142
+ this.emit("connected");
143
+ }
144
+ handleBinaryMessage(data) {
145
+ const reader = new WsMessageReader(data);
146
+ const type = reader.readInt32();
147
+ switch (type) {
148
+ case WsMessageType.SubscribeRes:
149
+ this.handleSubscribeResMessage(reader);
150
+ break;
151
+ case WsMessageType.RoomUpdate:
152
+ this.handleRoomUpdateMessage(reader);
153
+ break;
154
+ case WsMessageType.Error:
155
+ this.handleErrorMessage(reader);
156
+ break;
157
+ }
158
+ }
159
+ handleErrorMessage(msg) {
160
+ const reqId = msg.readInt32();
161
+ const error = msg.readString();
162
+ const promise = this._reqPromises.get(reqId);
163
+ if (promise) {
164
+ this._reqPromises.delete(reqId);
165
+ promise.reject(error);
166
+ }
167
+ else {
168
+ console.warn(`Received error for unknown request ${reqId}: ${error}`);
169
+ }
170
+ }
171
+ handleSubscribeResMessage(msg) {
172
+ const reqId = msg.readInt32();
173
+ const roomHash = msg.readUint32();
174
+ const promise = this._reqPromises.get(reqId);
175
+ if (promise) {
176
+ this._reqPromises.delete(reqId);
177
+ // In Write mode, no initial state is sent
178
+ if (!msg.end) {
179
+ promise.resolve(roomHash, msg.readUint8Array());
180
+ }
181
+ else {
182
+ promise.resolve(roomHash);
183
+ }
184
+ }
185
+ }
186
+ handleRoomUpdateMessage(msg) {
187
+ const appliedId = msg.readInt32();
188
+ const roomHash = msg.readUint32();
189
+ // Apply update if not in Write mode
190
+ if (!msg.end) {
191
+ const doc = this._roomHashToDoc.get(roomHash);
192
+ if (doc) {
193
+ Y.applyUpdate(doc, msg.readUint8Array(), this);
194
+ }
195
+ else {
196
+ console.warn(`Received update for unknown room ${roomHash}`);
197
+ }
198
+ }
199
+ // Check transaction resolves
200
+ for (const [transactionId, resolve,] of this._transactionResolves.entries()) {
201
+ if (appliedId >= transactionId) {
202
+ this._transactionResolves.delete(transactionId);
203
+ resolve();
204
+ }
205
+ }
206
+ }
207
+ serverTimestamp() {
208
+ return Date.now() - this._serverTimeOffset;
209
+ }
210
+ // Send Y update to room
211
+ async patchRoomState(room, update) {
212
+ const res = await fetch(`${this._apiUrl}/rooms/${room}`, {
213
+ method: "PATCH",
214
+ headers: this.apiHeaders,
215
+ body: update,
216
+ });
217
+ return await res.json();
218
+ }
219
+ // Storage
220
+ async createUpload(name, blob, tags) {
221
+ const res = await fetch(`${this._apiUrl}/uploads`, {
222
+ method: "POST",
223
+ headers: this.apiHeaders,
224
+ body: JSON.stringify({
225
+ name,
226
+ size: blob.size,
227
+ mimeType: blob.type,
228
+ tags,
229
+ }),
230
+ });
231
+ return await res.json();
232
+ }
233
+ async uploadChunks(blob, chunkSize, signedUrls) {
234
+ return await Promise.all(signedUrls.map(async (url, index) => {
235
+ const start = index * chunkSize;
236
+ const end = Math.min(start + chunkSize, blob.size);
237
+ const chunk = blob.slice(start, end);
238
+ const res = await fetch(url, { method: "PUT", body: chunk });
239
+ return JSON.parse(res.headers.get("ETag") || '""');
240
+ }));
241
+ }
242
+ async completeUpload(id, etags) {
243
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
244
+ method: "PUT",
245
+ headers: this.apiHeaders,
246
+ body: JSON.stringify({ etags }),
247
+ });
248
+ return await res.json();
249
+ }
250
+ async upload(name, blob, tags = []) {
251
+ const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
252
+ const etags = await this.uploadChunks(blob, chunkSize, urls);
253
+ return await this.completeUpload(id, etags);
254
+ }
255
+ async updateUpload(id, update) {
256
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
257
+ method: "PUT",
258
+ headers: this.apiHeaders,
259
+ body: JSON.stringify(update),
260
+ });
261
+ return await res.json();
262
+ }
263
+ async listUploads(filter = {}, skip = 0, limit = 100) {
264
+ const url = new URL("/uploads", this._apiUrl);
265
+ url.searchParams.set("skip", skip.toString());
266
+ url.searchParams.set("limit", limit.toString());
267
+ if (filter.clientId) {
268
+ url.searchParams.set("clientId", filter.clientId);
269
+ }
270
+ if (filter.mimeTypes) {
271
+ url.searchParams.set("mimeTypes", filter.mimeTypes.join());
272
+ }
273
+ if (filter.tags) {
274
+ url.searchParams.set("tags", filter.tags.join());
275
+ }
276
+ const res = await fetch(url.href, {
277
+ headers: this.apiHeaders,
278
+ });
279
+ return await res.json();
280
+ }
281
+ async deleteUpload(id) {
282
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
283
+ method: "DELETE",
284
+ headers: this.apiHeaders,
285
+ });
286
+ return await res.json();
287
+ }
288
+ async exposeScriptingContext(context) {
289
+ // @ts-ignore
290
+ window.KM_SCRIPTING_CONTEXT = context;
291
+ // @ts-ignore
292
+ window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
293
+ }
294
+ async sendSubscriptionReq(roomName, mode) {
295
+ // Set up sync resolver
296
+ const reqId = ++this._messageId;
297
+ return await new Promise((resolve, reject) => {
298
+ this._reqPromises.set(reqId, {
299
+ resolve: (roomHash, initialUpdate) => {
300
+ resolve({ roomHash, initialUpdate });
301
+ },
302
+ reject,
303
+ });
304
+ // Send subscription request
305
+ const msg = new WsMessageWriter();
306
+ msg.writeInt32(WsMessageType.SubscribeReq);
307
+ msg.writeInt32(reqId);
308
+ msg.writeString(roomName);
309
+ msg.writeChar(mode);
310
+ this.ws.send(msg.getBuffer());
311
+ });
312
+ }
313
+ async join(store, mode = RoomSubscriptionMode.ReadWrite) {
314
+ if (this._subscriptions.has(store.roomName)) {
315
+ throw new Error(`Already joined room "${store.roomName}"`);
316
+ }
317
+ this._subscriptions.set(store.roomName, mode);
318
+ // Send req
319
+ try {
320
+ const res = await this.sendSubscriptionReq(store.roomName, mode);
321
+ // Set room hash
322
+ this._roomHashes.set(store.roomName, res.roomHash);
323
+ this._roomHashToDoc.set(res.roomHash, store.doc);
324
+ this._stores.set(res.roomHash, store);
325
+ // Apply initial state
326
+ if (res.initialUpdate) {
327
+ Y.applyUpdate(store.doc, res.initialUpdate, this);
328
+ }
329
+ // Set defaults if doc is empty after sync (only possible in ReadWrite mode)
330
+ if (mode === RoomSubscriptionMode.ReadWrite) {
331
+ await this.transact((t) => {
332
+ for (const key in store.defaultValue) {
333
+ // @ts-ignore
334
+ if (!store.proxy.hasOwnProperty(key)) {
335
+ t.set(store.root[key], store.defaultValue[key]);
336
+ }
337
+ }
338
+ });
339
+ }
340
+ }
341
+ catch (err) {
342
+ this._subscriptions.delete(store.roomName);
343
+ throw err;
344
+ }
345
+ }
346
+ async transact(handler) {
347
+ const transaction = new KokimokiTransaction(this);
348
+ handler(transaction);
349
+ const updates = await transaction.getUpdates();
350
+ if (!updates.length) {
351
+ return;
352
+ }
353
+ // Construct buffer
354
+ const writer = new WsMessageWriter();
355
+ // Write message type
356
+ writer.writeInt32(WsMessageType.Transaction);
357
+ // Update and write transaction ID
358
+ const transactionId = ++this._messageId;
359
+ writer.writeInt32(transactionId);
360
+ // Write updates
361
+ for (const { roomName, update } of updates) {
362
+ const roomHash = this._roomHashes.get(roomName);
363
+ if (!roomHash) {
364
+ throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
365
+ }
366
+ writer.writeUint32(roomHash);
367
+ writer.writeUint8Array(update);
368
+ }
369
+ const buffer = writer.getBuffer();
370
+ // Wait for server to apply transaction
371
+ await new Promise((resolve) => {
372
+ this._transactionResolves.set(transactionId, resolve);
373
+ // Send update to server
374
+ try {
375
+ this.ws.send(buffer);
376
+ }
377
+ catch (e) {
378
+ // Not connected
379
+ console.log("Failed to send update to server:", e);
380
+ // TODO: merge updates or something
381
+ throw e;
382
+ }
383
+ });
384
+ /* // Reset doc in Write-only mode
385
+ for (const { roomName, update } of updates) {
386
+ const mode = this._subscriptions.get(roomName);
387
+
388
+ if (mode === RoomSubscriptionMode.Write) {
389
+ // @ts-ignore
390
+ // this._stores.get(this._roomHashes.get(roomName)!)!.doc = new Y.Doc();
391
+ }
392
+ } */
393
+ }
394
+ }
@@ -86,5 +86,37 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
86
86
  queue<T extends S.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
87
87
  awareness<T extends S.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
88
88
  reqRes<Req extends S.Generic<unknown>, Res extends S.Generic<unknown>>(serviceName: string, reqSchema: Req, resSchema: Res, handleRequest: (payload: Req["defaultValue"]) => Promise<Res["defaultValue"]>): KokimokiReqRes<Req, Res>;
89
+ /**
90
+ * Add a new entry to a leaderboard
91
+ * @param leaderboardName
92
+ * @param score
93
+ * @param metadata
94
+ * @returns
95
+ */
96
+ insertLeaderboardEntry<MetadataT, PrivateMetadataT>(leaderboardName: string, score: number, metadata: MetadataT, privateMetadata: PrivateMetadataT): Promise<{
97
+ rank: number;
98
+ }>;
99
+ /**
100
+ * Add or update latest entry to a leaderboard
101
+ * @param leaderboardName
102
+ * @param score
103
+ * @param metadata
104
+ * @param privateMetadata Can only be read using the leaderboard API
105
+ * @returns
106
+ */
107
+ upsertLeaderboardEntry<MetadataT, PrivateMetadataT>(leaderboardName: string, score: number, metadata: MetadataT, privateMetadata: PrivateMetadataT): Promise<{
108
+ rank: number;
109
+ }>;
110
+ /**
111
+ * List entries in a leaderboard
112
+ * @param leaderboardName
113
+ * @param skip
114
+ * @param limit
115
+ * @returns
116
+ */
117
+ listLeaderboardEntries<MetadataT>(leaderboardName: string, skip?: number, limit?: number): Promise<Paginated<{
118
+ score: number;
119
+ metadata: MetadataT;
120
+ }>>;
89
121
  }
90
122
  export {};
@@ -608,4 +608,58 @@ export class KokimokiClient extends EventEmitter {
608
608
  reqRes(serviceName, reqSchema, resSchema, handleRequest) {
609
609
  return new KokimokiReqRes(this, serviceName, reqSchema, resSchema, handleRequest);
610
610
  }
611
+ /**
612
+ * Add a new entry to a leaderboard
613
+ * @param leaderboardName
614
+ * @param score
615
+ * @param metadata
616
+ * @returns
617
+ */
618
+ async insertLeaderboardEntry(leaderboardName, score, metadata, privateMetadata) {
619
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries`, {
620
+ method: "POST",
621
+ headers: this.apiHeaders,
622
+ body: JSON.stringify({
623
+ leaderboardName,
624
+ score,
625
+ metadata,
626
+ privateMetadata,
627
+ upsert: false,
628
+ }),
629
+ });
630
+ return await res.json();
631
+ }
632
+ /**
633
+ * Add or update latest entry to a leaderboard
634
+ * @param leaderboardName
635
+ * @param score
636
+ * @param metadata
637
+ * @param privateMetadata Can only be read using the leaderboard API
638
+ * @returns
639
+ */
640
+ async upsertLeaderboardEntry(leaderboardName, score, metadata, privateMetadata) {
641
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries`, {
642
+ method: "POST",
643
+ headers: this.apiHeaders,
644
+ body: JSON.stringify({
645
+ leaderboardName,
646
+ score,
647
+ metadata,
648
+ privateMetadata,
649
+ upsert: true,
650
+ }),
651
+ });
652
+ return await res.json();
653
+ }
654
+ /**
655
+ * List entries in a leaderboard
656
+ * @param leaderboardName
657
+ * @param skip
658
+ * @param limit
659
+ * @returns
660
+ */
661
+ async listLeaderboardEntries(leaderboardName, skip = 0, limit = 100) {
662
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries?leaderboardName=${leaderboardName}&skip=${skip}&limit=${limit}`);
663
+ return await res.json();
664
+ }
611
665
  }
@@ -405,6 +405,38 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
405
405
  queue<T extends KokimokiSchema.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
406
406
  awareness<T extends KokimokiSchema.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
407
407
  reqRes<Req extends KokimokiSchema.Generic<unknown>, Res extends KokimokiSchema.Generic<unknown>>(serviceName: string, reqSchema: Req, resSchema: Res, handleRequest: (payload: Req["defaultValue"]) => Promise<Res["defaultValue"]>): KokimokiReqRes<Req, Res>;
408
+ /**
409
+ * Add a new entry to a leaderboard
410
+ * @param leaderboardName
411
+ * @param score
412
+ * @param metadata
413
+ * @returns
414
+ */
415
+ insertLeaderboardEntry<MetadataT, PrivateMetadataT>(leaderboardName: string, score: number, metadata: MetadataT, privateMetadata: PrivateMetadataT): Promise<{
416
+ rank: number;
417
+ }>;
418
+ /**
419
+ * Add or update latest entry to a leaderboard
420
+ * @param leaderboardName
421
+ * @param score
422
+ * @param metadata
423
+ * @param privateMetadata Can only be read using the leaderboard API
424
+ * @returns
425
+ */
426
+ upsertLeaderboardEntry<MetadataT, PrivateMetadataT>(leaderboardName: string, score: number, metadata: MetadataT, privateMetadata: PrivateMetadataT): Promise<{
427
+ rank: number;
428
+ }>;
429
+ /**
430
+ * List entries in a leaderboard
431
+ * @param leaderboardName
432
+ * @param skip
433
+ * @param limit
434
+ * @returns
435
+ */
436
+ listLeaderboardEntries<MetadataT>(leaderboardName: string, skip?: number, limit?: number): Promise<Paginated<{
437
+ score: number;
438
+ metadata: MetadataT;
439
+ }>>;
408
440
  }
409
441
 
410
442
  declare class RoomSubscription {
@@ -639,7 +639,7 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
639
639
  var eventsExports = events.exports;
640
640
  var EventEmitter$1 = /*@__PURE__*/getDefaultExportFromCjs(eventsExports);
641
641
 
642
- const KOKIMOKI_APP_VERSION = "1.6.0";
642
+ const KOKIMOKI_APP_VERSION = "1.7.0";
643
643
 
644
644
  /**
645
645
  * Utility module to work with key-value stores.
@@ -15391,6 +15391,60 @@ class KokimokiClient extends EventEmitter$1 {
15391
15391
  reqRes(serviceName, reqSchema, resSchema, handleRequest) {
15392
15392
  return new KokimokiReqRes(this, serviceName, reqSchema, resSchema, handleRequest);
15393
15393
  }
15394
+ /**
15395
+ * Add a new entry to a leaderboard
15396
+ * @param leaderboardName
15397
+ * @param score
15398
+ * @param metadata
15399
+ * @returns
15400
+ */
15401
+ async insertLeaderboardEntry(leaderboardName, score, metadata, privateMetadata) {
15402
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries`, {
15403
+ method: "POST",
15404
+ headers: this.apiHeaders,
15405
+ body: JSON.stringify({
15406
+ leaderboardName,
15407
+ score,
15408
+ metadata,
15409
+ privateMetadata,
15410
+ upsert: false,
15411
+ }),
15412
+ });
15413
+ return await res.json();
15414
+ }
15415
+ /**
15416
+ * Add or update latest entry to a leaderboard
15417
+ * @param leaderboardName
15418
+ * @param score
15419
+ * @param metadata
15420
+ * @param privateMetadata Can only be read using the leaderboard API
15421
+ * @returns
15422
+ */
15423
+ async upsertLeaderboardEntry(leaderboardName, score, metadata, privateMetadata) {
15424
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries`, {
15425
+ method: "POST",
15426
+ headers: this.apiHeaders,
15427
+ body: JSON.stringify({
15428
+ leaderboardName,
15429
+ score,
15430
+ metadata,
15431
+ privateMetadata,
15432
+ upsert: true,
15433
+ }),
15434
+ });
15435
+ return await res.json();
15436
+ }
15437
+ /**
15438
+ * List entries in a leaderboard
15439
+ * @param leaderboardName
15440
+ * @param skip
15441
+ * @param limit
15442
+ * @returns
15443
+ */
15444
+ async listLeaderboardEntries(leaderboardName, skip = 0, limit = 100) {
15445
+ const res = await fetch(`${this._apiUrl}/leaderboard-entries?leaderboardName=${leaderboardName}&skip=${skip}&limit=${limit}`);
15446
+ return await res.json();
15447
+ }
15394
15448
  }
15395
15449
 
15396
15450
  export { BooleanField, ConstField, EnumField, Field, FloatField, Form, FormArray, FormGroup, ImageField, IntegerField, KokimokiAwareness, KokimokiClient, KokimokiQueue, KokimokiSchema, KokimokiStore, RoomSubscription, RoomSubscriptionMode, TextField };