@kokimoki/app 1.4.6 → 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;
@@ -35,6 +36,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
35
36
  private _reconnectTimeout;
36
37
  private _pingInterval;
37
38
  private _clientTokenKey;
39
+ private _editorContext;
38
40
  constructor(host: string, appId: string, code?: string);
39
41
  get id(): string;
40
42
  get connectionId(): string;
@@ -44,6 +46,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
44
46
  get clientContext(): ClientContextT & ({} | null);
45
47
  get connected(): boolean;
46
48
  get ws(): WebSocket;
49
+ get isEditor(): boolean;
47
50
  connect(): Promise<void>;
48
51
  private handleInitMessage;
49
52
  private handleBinaryMessage;
@@ -79,6 +82,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
79
82
  getRoomHash<T extends S.Generic<unknown>>(store: KokimokiStore<T>): number;
80
83
  /** Initializers */
81
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>;
82
86
  queue<T extends S.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
83
87
  awareness<T extends S.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
84
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;
@@ -35,6 +36,7 @@ export class KokimokiClient extends EventEmitter {
35
36
  _reconnectTimeout = 0;
36
37
  _pingInterval;
37
38
  _clientTokenKey = "KM_TOKEN";
39
+ _editorContext;
38
40
  constructor(host, appId, code = "") {
39
41
  super();
40
42
  this.host = host;
@@ -57,6 +59,7 @@ export class KokimokiClient extends EventEmitter {
57
59
  if (window.parent && window.self !== window.parent) {
58
60
  window.addEventListener("message", (e) => {
59
61
  console.log(`[KM TOOLS] ${e.data}`);
62
+ this._editorContext = e.data;
60
63
  if (e.data === "km:clearStorage") {
61
64
  localStorage.removeItem(this._clientTokenKey);
62
65
  window.location.reload();
@@ -112,6 +115,9 @@ export class KokimokiClient extends EventEmitter {
112
115
  }
113
116
  return this._ws;
114
117
  }
118
+ get isEditor() {
119
+ return !!this._editorContext;
120
+ }
115
121
  async connect() {
116
122
  if (this._connectPromise) {
117
123
  return await this._connectPromise;
@@ -445,7 +451,13 @@ export class KokimokiClient extends EventEmitter {
445
451
  }
446
452
  // Send subscription request if connected to server
447
453
  if (!subscription.joined) {
448
- 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
+ }
449
461
  this._subscriptionsByHash.set(res.roomHash, subscription);
450
462
  await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
451
463
  // Trigger onJoin event
@@ -465,55 +477,76 @@ export class KokimokiClient extends EventEmitter {
465
477
  }
466
478
  }
467
479
  async transact(handler) {
468
- if (!this._connected) {
469
- throw new Error("Client not connected");
470
- }
480
+ // if (!this._connected) {
481
+ // throw new Error("Client not connected");
482
+ // }
471
483
  const transaction = new KokimokiTransaction(this);
472
484
  await handler(transaction);
473
485
  const { updates, consumedMessages } = await transaction.getUpdates();
474
486
  if (!updates.length) {
475
487
  return;
476
488
  }
477
- // Construct buffer
478
- const writer = new WsMessageWriter();
489
+ // Construct buffers
490
+ const remoteUpdateWriter = new WsMessageWriter();
491
+ const localUpdateWriter = new WsMessageWriter();
479
492
  // Write message type
480
- writer.writeInt32(WsMessageType.Transaction);
493
+ remoteUpdateWriter.writeInt32(WsMessageType.Transaction);
481
494
  // Update and write transaction ID
482
495
  const transactionId = ++this._messageId;
483
- writer.writeInt32(transactionId);
484
- // Write room hashes where messages were consumed
485
- 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);
486
500
  for (const roomName of consumedMessages) {
487
501
  const subscription = this._subscriptionsByName.get(roomName);
488
502
  if (!subscription) {
489
503
  throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
490
504
  }
491
- writer.writeUint32(subscription.roomHash);
505
+ remoteUpdateWriter.writeUint32(subscription.roomHash);
492
506
  }
493
507
  // Write updates
508
+ let localUpdates = 0, remoteUpdates = 0;
494
509
  for (const { roomName, update } of updates) {
495
510
  const subscription = this._subscriptionsByName.get(roomName);
496
511
  if (!subscription) {
497
512
  throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
498
513
  }
499
- writer.writeUint32(subscription.roomHash);
500
- writer.writeUint8Array(update);
501
- }
502
- const buffer = writer.getBuffer();
503
- // Wait for server to apply transaction
504
- await new Promise((resolve, reject) => {
505
- this._transactionPromises.set(transactionId, { resolve, reject });
506
- // Send update to server
507
- try {
508
- this.ws.send(buffer);
514
+ if (subscription.store instanceof KokimokiLocalStore) {
515
+ localUpdates++;
516
+ localUpdateWriter.writeUint32(subscription.roomHash);
517
+ localUpdateWriter.writeUint8Array(update);
509
518
  }
510
- catch (e) {
511
- // Not connected
512
- console.log("Failed to send update to server:", e);
513
- // TODO: merge updates or something
514
- throw e;
519
+ else {
520
+ remoteUpdates++;
521
+ remoteUpdateWriter.writeUint32(subscription.roomHash);
522
+ remoteUpdateWriter.writeUint8Array(update);
515
523
  }
516
- });
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
+ }
517
550
  }
518
551
  async close() {
519
552
  this._autoReconnect = false;
@@ -543,6 +576,14 @@ export class KokimokiClient extends EventEmitter {
543
576
  }
544
577
  return store;
545
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
+ }
546
587
  // queue
547
588
  queue(name, schema, mode, autoJoin = true) {
548
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;
@@ -344,6 +355,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
344
355
  private _reconnectTimeout;
345
356
  private _pingInterval;
346
357
  private _clientTokenKey;
358
+ private _editorContext;
347
359
  constructor(host: string, appId: string, code?: string);
348
360
  get id(): string;
349
361
  get connectionId(): string;
@@ -353,6 +365,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
353
365
  get clientContext(): ClientContextT & ({} | null);
354
366
  get connected(): boolean;
355
367
  get ws(): WebSocket;
368
+ get isEditor(): boolean;
356
369
  connect(): Promise<void>;
357
370
  private handleInitMessage;
358
371
  private handleBinaryMessage;
@@ -388,6 +401,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
388
401
  getRoomHash<T extends KokimokiSchema.Generic<unknown>>(store: KokimokiStore<T>): number;
389
402
  /** Initializers */
390
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>;
391
405
  queue<T extends KokimokiSchema.Generic<unknown>>(name: string, schema: T, mode: RoomSubscriptionMode, autoJoin?: boolean): KokimokiQueue<T>;
392
406
  awareness<T extends KokimokiSchema.Generic<unknown>>(name: string, dataSchema: T, initialData?: T["defaultValue"], autoJoin?: boolean): KokimokiAwareness<T>;
393
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>;