@kokimoki/app 1.4.7 → 1.5.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,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>;
@@ -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
- const res = await this.sendSubscribeReq(store.roomName, store.mode);
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
- throw new Error("Client not connected");
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 buffer
483
- const writer = new WsMessageWriter();
489
+ // Construct buffers
490
+ const remoteUpdateWriter = new WsMessageWriter();
491
+ const localUpdateWriter = new WsMessageWriter();
484
492
  // Write message type
485
- writer.writeInt32(WsMessageType.Transaction);
493
+ remoteUpdateWriter.writeInt32(WsMessageType.Transaction);
486
494
  // Update and write transaction ID
487
495
  const transactionId = ++this._messageId;
488
- writer.writeInt32(transactionId);
489
- // Write room hashes where messages were consumed
490
- writer.writeInt32(consumedMessages.size);
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
- writer.writeUint32(subscription.roomHash);
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
- writer.writeUint32(subscription.roomHash);
505
- writer.writeUint8Array(update);
506
- }
507
- const buffer = writer.getBuffer();
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
- catch (e) {
516
- // Not connected
517
- console.log("Failed to send update to server:", e);
518
- // TODO: merge updates or something
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
+ }
@@ -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>;