@kokimoki/app 1.4.7 → 1.6.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/kokimoki-awareness copy.d.ts +22 -0
- package/dist/kokimoki-awareness copy.js +82 -0
- package/dist/kokimoki-client.d.ts +2 -0
- package/dist/kokimoki-client.js +63 -27
- package/dist/kokimoki-local-store.d.ts +12 -0
- package/dist/kokimoki-local-store.js +40 -0
- package/dist/kokimoki.min.d.ts +12 -0
- package/dist/kokimoki.min.js +2660 -54
- package/dist/kokimoki.min.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/dist/kokimoki-client-refactored.d.ts +0 -68
- package/dist/kokimoki-client-refactored.js +0 -394
- package/dist/message-queue.d.ts +0 -8
- package/dist/message-queue.js +0 -19
- package/dist/synced-schema.d.ts +0 -59
- package/dist/synced-schema.js +0 -84
- package/dist/synced-store.d.ts +0 -7
- package/dist/synced-store.js +0 -9
- package/dist/synced-types.d.ts +0 -38
- package/dist/synced-types.js +0 -68
- package/dist/ws-message-type copy.d.ts +0 -6
- package/dist/ws-message-type copy.js +0 -7
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
2
|
+
import { KokimokiSchema as S } from "./kokimoki-schema";
|
|
3
|
+
import { RoomSubscriptionMode } from "./room-subscription-mode";
|
|
4
|
+
import type { KokimokiClient } from "./kokimoki-client";
|
|
5
|
+
export declare class KokimokiAwareness<Data extends S.Generic<unknown>> extends KokimokiStore<S.Dict<S.Struct<{
|
|
6
|
+
clientId: S.String;
|
|
7
|
+
lastPing: S.Number;
|
|
8
|
+
data: Data;
|
|
9
|
+
}>>> {
|
|
10
|
+
readonly dataSchema: Data;
|
|
11
|
+
private _data;
|
|
12
|
+
private _pingInterval;
|
|
13
|
+
private _kmClients;
|
|
14
|
+
constructor(roomName: string, dataSchema: Data, _data: Data["defaultValue"], mode?: RoomSubscriptionMode, pingTimeout?: number);
|
|
15
|
+
onJoin(client: KokimokiClient<any>): Promise<void>;
|
|
16
|
+
onBeforeLeave(client: KokimokiClient<any>): Promise<void>;
|
|
17
|
+
onLeave(client: KokimokiClient<any>): Promise<void>;
|
|
18
|
+
getClients(): {
|
|
19
|
+
[clientId: string]: Data["defaultValue"];
|
|
20
|
+
};
|
|
21
|
+
setData(data: Data["defaultValue"]): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
2
|
+
import { KokimokiSchema as S } from "./kokimoki-schema";
|
|
3
|
+
import { RoomSubscriptionMode } from "./room-subscription-mode";
|
|
4
|
+
export class KokimokiAwareness extends KokimokiStore {
|
|
5
|
+
dataSchema;
|
|
6
|
+
_data;
|
|
7
|
+
_pingInterval = null;
|
|
8
|
+
_kmClients = new Set();
|
|
9
|
+
constructor(roomName, dataSchema, _data, mode = RoomSubscriptionMode.ReadWrite, pingTimeout = 3000) {
|
|
10
|
+
super(`/a/${roomName}`, S.dict(S.struct({
|
|
11
|
+
clientId: S.string(),
|
|
12
|
+
lastPing: S.number(),
|
|
13
|
+
data: dataSchema,
|
|
14
|
+
})), mode);
|
|
15
|
+
this.dataSchema = dataSchema;
|
|
16
|
+
this._data = _data;
|
|
17
|
+
this._pingInterval = setInterval(async () => {
|
|
18
|
+
const kmClients = Array.from(this._kmClients);
|
|
19
|
+
await Promise.all(kmClients.map(async (client) => {
|
|
20
|
+
try {
|
|
21
|
+
await client.transact((t) => {
|
|
22
|
+
const timestamp = client.serverTimestamp();
|
|
23
|
+
// Update self
|
|
24
|
+
if (this.proxy[client.connectionId]) {
|
|
25
|
+
t.set(this.root[client.connectionId].lastPing, timestamp);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
t.set(this.root[client.connectionId], {
|
|
29
|
+
clientId: client.id,
|
|
30
|
+
lastPing: timestamp,
|
|
31
|
+
data: this._data,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Delete clients that haven't pinged in a while
|
|
35
|
+
for (const connectionId in this.proxy) {
|
|
36
|
+
const { lastPing } = this.proxy[connectionId];
|
|
37
|
+
if (!lastPing || timestamp - lastPing > pingTimeout * 2) {
|
|
38
|
+
t.delete(this.root[connectionId]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (e) { }
|
|
44
|
+
}));
|
|
45
|
+
}, pingTimeout);
|
|
46
|
+
}
|
|
47
|
+
async onJoin(client) {
|
|
48
|
+
this._kmClients.add(client);
|
|
49
|
+
await client.transact((t) => {
|
|
50
|
+
t.set(this.root[client.connectionId], {
|
|
51
|
+
clientId: client.id,
|
|
52
|
+
lastPing: client.serverTimestamp(),
|
|
53
|
+
data: this._data,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async onBeforeLeave(client) {
|
|
58
|
+
await client.transact((t) => {
|
|
59
|
+
t.delete(this.root[client.connectionId]);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async onLeave(client) {
|
|
63
|
+
this._kmClients.delete(client);
|
|
64
|
+
}
|
|
65
|
+
getClients() {
|
|
66
|
+
const clients = {};
|
|
67
|
+
for (const connectionId in this.proxy) {
|
|
68
|
+
clients[this.proxy[connectionId].clientId] =
|
|
69
|
+
this.proxy[connectionId].data;
|
|
70
|
+
}
|
|
71
|
+
return clients;
|
|
72
|
+
}
|
|
73
|
+
async setData(data) {
|
|
74
|
+
this._data = data;
|
|
75
|
+
const kmClients = Array.from(this._kmClients);
|
|
76
|
+
await Promise.all(kmClients.map(async (client) => {
|
|
77
|
+
await client.transact((t) => {
|
|
78
|
+
t.set(this.root[client.connectionId].data, this._data);
|
|
79
|
+
});
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { RoomSubscriptionMode } from "./room-subscription-mode";
|
|
|
9
9
|
import { KokimokiQueue } from "./kokimoki-queue";
|
|
10
10
|
import { KokimokiAwareness } from "./kokimoki-awareness";
|
|
11
11
|
import { KokimokiReqRes } from "./kokimoki-req-res";
|
|
12
|
+
import { KokimokiLocalStore } from "./kokimoki-local-store";
|
|
12
13
|
declare const KokimokiClient_base: new () => TypedEmitter<KokimokiClientEvents>;
|
|
13
14
|
export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
|
|
14
15
|
readonly host: string;
|
|
@@ -81,6 +82,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
|
|
|
81
82
|
getRoomHash<T extends S.Generic<unknown>>(store: KokimokiStore<T>): number;
|
|
82
83
|
/** Initializers */
|
|
83
84
|
store<T extends S.Generic<unknown>>(name: string, schema: T, autoJoin?: boolean): KokimokiStore<T, T["defaultValue"]>;
|
|
85
|
+
localStore<T extends S.Generic<unknown>>(name: string, schema: T): KokimokiLocalStore<T>;
|
|
84
86
|
queue<T extends S.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
|
|
85
87
|
awareness<T extends S.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
|
|
86
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>;
|
package/dist/kokimoki-client.js
CHANGED
|
@@ -10,6 +10,7 @@ import { RoomSubscription } from "./room-subscription";
|
|
|
10
10
|
import { KokimokiQueue } from "./kokimoki-queue";
|
|
11
11
|
import { KokimokiAwareness } from "./kokimoki-awareness";
|
|
12
12
|
import { KokimokiReqRes } from "./kokimoki-req-res";
|
|
13
|
+
import { KokimokiLocalStore } from "./kokimoki-local-store";
|
|
13
14
|
export class KokimokiClient extends EventEmitter {
|
|
14
15
|
host;
|
|
15
16
|
appId;
|
|
@@ -450,7 +451,13 @@ export class KokimokiClient extends EventEmitter {
|
|
|
450
451
|
}
|
|
451
452
|
// Send subscription request if connected to server
|
|
452
453
|
if (!subscription.joined) {
|
|
453
|
-
|
|
454
|
+
let res;
|
|
455
|
+
if (store instanceof KokimokiLocalStore) {
|
|
456
|
+
res = store.getInitialUpdate(this.appId, this.id);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
res = await this.sendSubscribeReq(store.roomName, store.mode);
|
|
460
|
+
}
|
|
454
461
|
this._subscriptionsByHash.set(res.roomHash, subscription);
|
|
455
462
|
await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
|
|
456
463
|
// Trigger onJoin event
|
|
@@ -470,55 +477,76 @@ export class KokimokiClient extends EventEmitter {
|
|
|
470
477
|
}
|
|
471
478
|
}
|
|
472
479
|
async transact(handler) {
|
|
473
|
-
if (!this._connected) {
|
|
474
|
-
|
|
475
|
-
}
|
|
480
|
+
// if (!this._connected) {
|
|
481
|
+
// throw new Error("Client not connected");
|
|
482
|
+
// }
|
|
476
483
|
const transaction = new KokimokiTransaction(this);
|
|
477
484
|
await handler(transaction);
|
|
478
485
|
const { updates, consumedMessages } = await transaction.getUpdates();
|
|
479
486
|
if (!updates.length) {
|
|
480
487
|
return;
|
|
481
488
|
}
|
|
482
|
-
// Construct
|
|
483
|
-
const
|
|
489
|
+
// Construct buffers
|
|
490
|
+
const remoteUpdateWriter = new WsMessageWriter();
|
|
491
|
+
const localUpdateWriter = new WsMessageWriter();
|
|
484
492
|
// Write message type
|
|
485
|
-
|
|
493
|
+
remoteUpdateWriter.writeInt32(WsMessageType.Transaction);
|
|
486
494
|
// Update and write transaction ID
|
|
487
495
|
const transactionId = ++this._messageId;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
496
|
+
remoteUpdateWriter.writeInt32(transactionId);
|
|
497
|
+
localUpdateWriter.writeInt32(transactionId);
|
|
498
|
+
// Write room hashes where messages were consumed (remote only)
|
|
499
|
+
remoteUpdateWriter.writeInt32(consumedMessages.size);
|
|
491
500
|
for (const roomName of consumedMessages) {
|
|
492
501
|
const subscription = this._subscriptionsByName.get(roomName);
|
|
493
502
|
if (!subscription) {
|
|
494
503
|
throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
|
|
495
504
|
}
|
|
496
|
-
|
|
505
|
+
remoteUpdateWriter.writeUint32(subscription.roomHash);
|
|
497
506
|
}
|
|
498
507
|
// Write updates
|
|
508
|
+
let localUpdates = 0, remoteUpdates = 0;
|
|
499
509
|
for (const { roomName, update } of updates) {
|
|
500
510
|
const subscription = this._subscriptionsByName.get(roomName);
|
|
501
511
|
if (!subscription) {
|
|
502
512
|
throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
|
|
503
513
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// Wait for server to apply transaction
|
|
509
|
-
await new Promise((resolve, reject) => {
|
|
510
|
-
this._transactionPromises.set(transactionId, { resolve, reject });
|
|
511
|
-
// Send update to server
|
|
512
|
-
try {
|
|
513
|
-
this.ws.send(buffer);
|
|
514
|
+
if (subscription.store instanceof KokimokiLocalStore) {
|
|
515
|
+
localUpdates++;
|
|
516
|
+
localUpdateWriter.writeUint32(subscription.roomHash);
|
|
517
|
+
localUpdateWriter.writeUint8Array(update);
|
|
514
518
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
throw e;
|
|
519
|
+
else {
|
|
520
|
+
remoteUpdates++;
|
|
521
|
+
remoteUpdateWriter.writeUint32(subscription.roomHash);
|
|
522
|
+
remoteUpdateWriter.writeUint8Array(update);
|
|
520
523
|
}
|
|
521
|
-
}
|
|
524
|
+
}
|
|
525
|
+
// Wait for server to apply transaction
|
|
526
|
+
if (remoteUpdates) {
|
|
527
|
+
const remoteBuffer = remoteUpdateWriter.getBuffer();
|
|
528
|
+
await new Promise((resolve, reject) => {
|
|
529
|
+
this._transactionPromises.set(transactionId, { resolve, reject });
|
|
530
|
+
// Send update to server
|
|
531
|
+
try {
|
|
532
|
+
this.ws.send(remoteBuffer);
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
// Not connected
|
|
536
|
+
console.log("Failed to send update to server:", e);
|
|
537
|
+
// Delete transaction promise
|
|
538
|
+
this._transactionPromises.delete(transactionId);
|
|
539
|
+
// TODO: merge updates or something
|
|
540
|
+
reject(e);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
// Apply local updates
|
|
545
|
+
if (localUpdates) {
|
|
546
|
+
const localBuffer = localUpdateWriter.getBuffer();
|
|
547
|
+
const reader = new WsMessageReader(localBuffer);
|
|
548
|
+
this.handleRoomUpdateMessage(reader);
|
|
549
|
+
}
|
|
522
550
|
}
|
|
523
551
|
async close() {
|
|
524
552
|
this._autoReconnect = false;
|
|
@@ -548,6 +576,14 @@ export class KokimokiClient extends EventEmitter {
|
|
|
548
576
|
}
|
|
549
577
|
return store;
|
|
550
578
|
}
|
|
579
|
+
// local store
|
|
580
|
+
localStore(name, schema) {
|
|
581
|
+
const store = new KokimokiLocalStore(name, schema);
|
|
582
|
+
this.join(store)
|
|
583
|
+
.then(() => { })
|
|
584
|
+
.catch(() => { });
|
|
585
|
+
return store;
|
|
586
|
+
}
|
|
551
587
|
// queue
|
|
552
588
|
queue(name, schema, mode, autoJoin = true) {
|
|
553
589
|
const queue = new KokimokiQueue(name, schema, mode);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
2
|
+
import type { KokimokiSchema as S } from "./kokimoki-schema";
|
|
3
|
+
export declare class KokimokiLocalStore<Data extends S.Generic<unknown>> extends KokimokiStore<Data> {
|
|
4
|
+
private readonly localRoomName;
|
|
5
|
+
private _stateKey?;
|
|
6
|
+
private get stateKey();
|
|
7
|
+
constructor(localRoomName: string, schema: Data);
|
|
8
|
+
getInitialUpdate(appId: string, clientId: string): {
|
|
9
|
+
roomHash: number;
|
|
10
|
+
initialUpdate: Uint8Array | undefined;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KokimokiStore } from "./kokimoki-store";
|
|
2
|
+
import { RoomSubscriptionMode } from "./room-subscription-mode";
|
|
3
|
+
import { fingerprint32 } from "farmhash-modern";
|
|
4
|
+
import * as Y from "yjs";
|
|
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, schema) {
|
|
15
|
+
super(`/l/${localRoomName}`, schema, 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
|
+
}
|
package/dist/kokimoki.min.d.ts
CHANGED
|
@@ -318,6 +318,17 @@ declare class KokimokiReqRes<Req extends KokimokiSchema.Generic<unknown>, Res ex
|
|
|
318
318
|
send(toClientId: string, payload: Req["defaultValue"], timeout?: number): Promise<Res["defaultValue"]>;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
declare class KokimokiLocalStore<Data extends KokimokiSchema.Generic<unknown>> extends KokimokiStore<Data> {
|
|
322
|
+
private readonly localRoomName;
|
|
323
|
+
private _stateKey?;
|
|
324
|
+
private get stateKey();
|
|
325
|
+
constructor(localRoomName: string, schema: Data);
|
|
326
|
+
getInitialUpdate(appId: string, clientId: string): {
|
|
327
|
+
roomHash: number;
|
|
328
|
+
initialUpdate: Uint8Array | undefined;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
321
332
|
declare const KokimokiClient_base: new () => TypedEventEmitter<KokimokiClientEvents>;
|
|
322
333
|
declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
|
|
323
334
|
readonly host: string;
|
|
@@ -390,6 +401,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
|
|
|
390
401
|
getRoomHash<T extends KokimokiSchema.Generic<unknown>>(store: KokimokiStore<T>): number;
|
|
391
402
|
/** Initializers */
|
|
392
403
|
store<T extends KokimokiSchema.Generic<unknown>>(name: string, schema: T, autoJoin?: boolean): KokimokiStore<T, T["defaultValue"]>;
|
|
404
|
+
localStore<T extends KokimokiSchema.Generic<unknown>>(name: string, schema: T): KokimokiLocalStore<T>;
|
|
393
405
|
queue<T extends KokimokiSchema.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
|
|
394
406
|
awareness<T extends KokimokiSchema.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
|
|
395
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>;
|