@kokimoki/app 1.0.10 → 1.1.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/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export * from "./kokimoki-store";
8
8
  export * from "./kokimoki-queue";
9
9
  export * from "./room-subscription";
10
10
  export * from "./room-subscription-mode";
11
+ export * from "./kokimoki-awareness";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export * from "./kokimoki-store";
8
8
  export * from "./kokimoki-queue";
9
9
  export * from "./room-subscription";
10
10
  export * from "./room-subscription-mode";
11
+ export * from "./kokimoki-awareness";
@@ -0,0 +1,15 @@
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 extends KokimokiStore<S.Dict<S.Struct<{
6
+ clientId: S.String;
7
+ lastPing: S.Number;
8
+ }>>> {
9
+ private _pingInterval;
10
+ private _kmClients;
11
+ constructor(roomName: string, mode?: RoomSubscriptionMode, pingTimeout?: number);
12
+ onJoin(client: KokimokiClient<any>): Promise<void>;
13
+ onBeforeLeave(client: KokimokiClient<any>): Promise<void>;
14
+ onLeave(client: KokimokiClient<any>): Promise<void>;
15
+ }
@@ -0,0 +1,64 @@
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
5
+ // <Data extends S.Generic<unknown>>
6
+ extends KokimokiStore {
7
+ _pingInterval = null;
8
+ _kmClients = new Set();
9
+ // private _data: Data["defaultValue"];
10
+ constructor(roomName,
11
+ // public readonly dataSchema: Data,
12
+ // initialData: Data["defaultValue"],
13
+ mode = RoomSubscriptionMode.ReadWrite, pingTimeout = 2500) {
14
+ super(`/a/${roomName}`, S.dict(S.struct({
15
+ clientId: S.string(),
16
+ lastPing: S.number(),
17
+ // data: dataSchema,
18
+ })), mode);
19
+ // this._data = initialData;
20
+ this._pingInterval = setInterval(async () => {
21
+ const kmClients = Array.from(this._kmClients);
22
+ await Promise.all(kmClients.map(async (client) => {
23
+ try {
24
+ await client.transact((t) => {
25
+ const timestamp = client.serverTimestamp();
26
+ // Update self last ping
27
+ t.set(this.root[client.connectionId].lastPing, timestamp);
28
+ /* if (!this.proxy[client.connectionId]) {
29
+ t.set(this.root[client.connectionId].data, this._data);
30
+ } else {
31
+ this._data = this.proxy[client.connectionId].data;
32
+ } */
33
+ // Delete clients that haven't pinged in a while
34
+ for (const connectionId in this.proxy) {
35
+ const { lastPing } = this.proxy[connectionId];
36
+ if (timestamp - lastPing > pingTimeout * 2) {
37
+ t.delete(this.root[connectionId]);
38
+ }
39
+ }
40
+ });
41
+ }
42
+ catch (e) { }
43
+ }));
44
+ }, pingTimeout);
45
+ }
46
+ async onJoin(client) {
47
+ this._kmClients.add(client);
48
+ await client.transact((t) => {
49
+ t.set(this.root[client.connectionId], {
50
+ clientId: client.id,
51
+ lastPing: client.serverTimestamp(),
52
+ // data: this._data,
53
+ });
54
+ });
55
+ }
56
+ async onBeforeLeave(client) {
57
+ await client.transact((t) => {
58
+ t.delete(this.root[client.connectionId]);
59
+ });
60
+ }
61
+ async onLeave(client) {
62
+ this._kmClients.delete(client);
63
+ }
64
+ }
@@ -13,6 +13,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
13
13
  private _wsUrl;
14
14
  private _apiUrl;
15
15
  private _id?;
16
+ private _connectionId?;
16
17
  private _token?;
17
18
  private _apiHeaders?;
18
19
  private _serverTimeOffset;
@@ -21,14 +22,17 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
21
22
  private _subscriptionsByName;
22
23
  private _subscriptionsByHash;
23
24
  private _subscribeReqPromises;
25
+ private _unsubscribeReqPromises;
24
26
  private _transactionPromises;
25
27
  private _connected;
26
28
  private _connectPromise?;
27
29
  private _messageId;
30
+ private _autoReconnect;
28
31
  private _reconnectTimeout;
29
32
  private _pingInterval;
30
33
  constructor(host: string, appId: string, code?: string);
31
34
  get id(): string;
35
+ get connectionId(): string;
32
36
  get token(): string;
33
37
  get apiUrl(): string;
34
38
  get apiHeaders(): Headers;
@@ -40,6 +44,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
40
44
  private handleBinaryMessage;
41
45
  private handleErrorMessage;
42
46
  private handleSubscribeResMessage;
47
+ private handleUnsubscribeResMessage;
43
48
  private handleRoomUpdateMessage;
44
49
  serverTimestamp(): number;
45
50
  patchRoomState(room: string, update: Uint8Array): Promise<any>;
@@ -60,8 +65,11 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
60
65
  deletedCount: number;
61
66
  }>;
62
67
  exposeScriptingContext(context: any): Promise<void>;
63
- private sendSubscriptionReq;
68
+ private sendSubscribeReq;
69
+ private sendUnsubscribeReq;
64
70
  join<T extends S.Generic<unknown>>(store: KokimokiStore<T>): Promise<void>;
71
+ leave<T extends S.Generic<unknown>>(store: KokimokiStore<T>): Promise<void>;
65
72
  transact(handler: (t: KokimokiTransaction) => void): Promise<void>;
73
+ close(): Promise<void>;
66
74
  }
67
75
  export {};
@@ -13,6 +13,7 @@ export class KokimokiClient extends EventEmitter {
13
13
  _wsUrl;
14
14
  _apiUrl;
15
15
  _id;
16
+ _connectionId;
16
17
  _token;
17
18
  _apiHeaders;
18
19
  _serverTimeOffset = 0;
@@ -21,10 +22,12 @@ export class KokimokiClient extends EventEmitter {
21
22
  _subscriptionsByName = new Map();
22
23
  _subscriptionsByHash = new Map();
23
24
  _subscribeReqPromises = new Map();
25
+ _unsubscribeReqPromises = new Map();
24
26
  _transactionPromises = new Map();
25
27
  _connected = false;
26
28
  _connectPromise;
27
29
  _messageId = 0;
30
+ _autoReconnect = true;
28
31
  _reconnectTimeout = 0;
29
32
  _pingInterval;
30
33
  constructor(host, appId, code = "") {
@@ -52,6 +55,12 @@ export class KokimokiClient extends EventEmitter {
52
55
  }
53
56
  return this._id;
54
57
  }
58
+ get connectionId() {
59
+ if (!this._connectionId) {
60
+ throw new Error("Client not connected");
61
+ }
62
+ return this._connectionId;
63
+ }
55
64
  get token() {
56
65
  if (!this._token) {
57
66
  throw new Error("Client not connected");
@@ -97,9 +106,10 @@ export class KokimokiClient extends EventEmitter {
97
106
  window.__KOKIMOKI_WS__ = {};
98
107
  }
99
108
  if (this.appId in window.__KOKIMOKI_WS__) {
109
+ console.log(`[Kokimoki] Closing previous connection for ${this.appId}`);
100
110
  window.__KOKIMOKI_WS__[this.appId].close();
101
111
  }
102
- window.__KOKIMOKI_WS__[this.appId] = this._ws;
112
+ window.__KOKIMOKI_WS__[this.appId] = this;
103
113
  }
104
114
  // Wait for connection
105
115
  this._connectPromise = new Promise((onInit) => {
@@ -121,6 +131,9 @@ export class KokimokiClient extends EventEmitter {
121
131
  this._subscribeReqPromises.clear();
122
132
  this._transactionPromises.clear();
123
133
  // Attempt to reconnect
134
+ if (!this._autoReconnect) {
135
+ return;
136
+ }
124
137
  console.log(`[Kokimoki] Connection lost, attempting to reconnect in ${this._reconnectTimeout} seconds...`);
125
138
  setTimeout(async () => await this.connect(), this._reconnectTimeout * 1000);
126
139
  this._reconnectTimeout = Math.min(3, this._reconnectTimeout + 1);
@@ -169,6 +182,7 @@ export class KokimokiClient extends EventEmitter {
169
182
  handleInitMessage(message) {
170
183
  localStorage.setItem("KM_TOKEN", message.clientToken);
171
184
  this._id = message.clientId;
185
+ this._connectionId = message.id;
172
186
  this._token = message.appToken;
173
187
  this._clientContext = message.clientContext;
174
188
  // Set up the auth headers
@@ -184,6 +198,9 @@ export class KokimokiClient extends EventEmitter {
184
198
  case WsMessageType.SubscribeRes:
185
199
  this.handleSubscribeResMessage(reader);
186
200
  break;
201
+ case WsMessageType.UnsubscribeRes:
202
+ this.handleUnsubscribeResMessage(reader);
203
+ break;
187
204
  case WsMessageType.RoomUpdate:
188
205
  this.handleRoomUpdateMessage(reader);
189
206
  break;
@@ -230,6 +247,14 @@ export class KokimokiClient extends EventEmitter {
230
247
  }
231
248
  }
232
249
  }
250
+ handleUnsubscribeResMessage(msg) {
251
+ const reqId = msg.readInt32();
252
+ const promise = this._unsubscribeReqPromises.get(reqId);
253
+ if (promise) {
254
+ this._unsubscribeReqPromises.delete(reqId);
255
+ promise.resolve();
256
+ }
257
+ }
233
258
  handleRoomUpdateMessage(msg) {
234
259
  const appliedId = msg.readInt32();
235
260
  const roomHash = msg.readUint32();
@@ -338,7 +363,7 @@ export class KokimokiClient extends EventEmitter {
338
363
  // @ts-ignore
339
364
  window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
340
365
  }
341
- async sendSubscriptionReq(roomName, mode) {
366
+ async sendSubscribeReq(roomName, mode) {
342
367
  // Set up sync resolver
343
368
  const reqId = ++this._messageId;
344
369
  return await new Promise((resolve, reject) => {
@@ -357,6 +382,21 @@ export class KokimokiClient extends EventEmitter {
357
382
  this.ws.send(msg.getBuffer());
358
383
  });
359
384
  }
385
+ async sendUnsubscribeReq(roomHash) {
386
+ const reqId = ++this._messageId;
387
+ return await new Promise((resolve, reject) => {
388
+ this._unsubscribeReqPromises.set(reqId, {
389
+ resolve,
390
+ reject,
391
+ });
392
+ // Send unsubscribe request
393
+ const msg = new WsMessageWriter();
394
+ msg.writeInt32(WsMessageType.UnsubscribeReq);
395
+ msg.writeInt32(reqId);
396
+ msg.writeUint32(roomHash);
397
+ this.ws.send(msg.getBuffer());
398
+ });
399
+ }
360
400
  async join(store) {
361
401
  let subscription = this._subscriptionsByName.get(store.roomName);
362
402
  if (!subscription) {
@@ -365,9 +405,22 @@ export class KokimokiClient extends EventEmitter {
365
405
  }
366
406
  // Send subscription request if connected to server
367
407
  if (!subscription.joined) {
368
- const res = await this.sendSubscriptionReq(store.roomName, store.mode);
408
+ const res = await this.sendSubscribeReq(store.roomName, store.mode);
369
409
  this._subscriptionsByHash.set(res.roomHash, subscription);
370
410
  await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
411
+ // Trigger onJoin event
412
+ store.onJoin(this);
413
+ }
414
+ }
415
+ async leave(store) {
416
+ const subscription = this._subscriptionsByName.get(store.roomName);
417
+ if (subscription) {
418
+ await store.onBeforeLeave(this);
419
+ await this.sendUnsubscribeReq(subscription.roomHash);
420
+ this._subscriptionsByName.delete(store.roomName);
421
+ this._subscriptionsByHash.delete(subscription.roomHash);
422
+ // Trigger onLeave event
423
+ store.onLeave(this);
371
424
  }
372
425
  }
373
426
  async transact(handler) {
@@ -430,4 +483,13 @@ export class KokimokiClient extends EventEmitter {
430
483
  }
431
484
  } */
432
485
  }
486
+ async close() {
487
+ this._autoReconnect = false;
488
+ if (this._ws) {
489
+ this._ws.close();
490
+ }
491
+ if (this._pingInterval) {
492
+ clearInterval(this._pingInterval);
493
+ }
494
+ }
433
495
  }
@@ -75,4 +75,10 @@ export declare namespace KokimokiSchema {
75
75
  constructor(schemas: T, defaultValue: T[number]["defaultValue"]);
76
76
  }
77
77
  function anyOf<T extends readonly Generic<unknown>[]>(schemas: T, defaultValue: T[number]["defaultValue"]): AnyOf<T>;
78
+ class Optional<T extends Generic<unknown>> extends Generic<T["defaultValue"] | undefined> {
79
+ schema: T;
80
+ defaultValue: T["defaultValue"] | undefined;
81
+ constructor(schema: T, defaultValue?: T["defaultValue"] | undefined);
82
+ }
83
+ function optional<T extends Generic<unknown>>(schema: T, defaultValue?: T["defaultValue"] | undefined): Optional<T>;
78
84
  }
@@ -146,4 +146,18 @@ export var KokimokiSchema;
146
146
  return new AnyOf(schemas, defaultValue);
147
147
  }
148
148
  KokimokiSchema.anyOf = anyOf;
149
+ class Optional extends Generic {
150
+ schema;
151
+ defaultValue;
152
+ constructor(schema, defaultValue = undefined) {
153
+ super();
154
+ this.schema = schema;
155
+ this.defaultValue = defaultValue;
156
+ }
157
+ }
158
+ KokimokiSchema.Optional = Optional;
159
+ function optional(schema, defaultValue = undefined) {
160
+ return new Optional(schema, defaultValue);
161
+ }
162
+ KokimokiSchema.optional = optional;
149
163
  })(KokimokiSchema || (KokimokiSchema = {}));
@@ -1,7 +1,8 @@
1
1
  import * as Y from "yjs";
2
2
  import type { KokimokiSchema as S } from "./kokimoki-schema";
3
3
  import { RoomSubscriptionMode } from "./room-subscription-mode";
4
- export declare class KokimokiStore<T extends S.Generic<unknown>> {
4
+ import type { KokimokiClient } from "./kokimoki-client";
5
+ export declare class KokimokiStore<T extends S.Generic<unknown>, SubscribeT = T["defaultValue"]> {
5
6
  readonly roomName: string;
6
7
  readonly mode: RoomSubscriptionMode;
7
8
  readonly doc: Y.Doc;
@@ -10,5 +11,9 @@ export declare class KokimokiStore<T extends S.Generic<unknown>> {
10
11
  readonly defaultValue: T["defaultValue"];
11
12
  readonly docRoot: Y.Map<unknown>;
12
13
  constructor(roomName: string, schema: T, mode?: RoomSubscriptionMode);
13
- subscribe(callback: (value: T["defaultValue"]) => void): () => void;
14
+ get(): T["defaultValue"];
15
+ subscribe(set: (value: SubscribeT) => void): () => void;
16
+ onJoin(client: KokimokiClient): Promise<void>;
17
+ onBeforeLeave(client: KokimokiClient): Promise<void>;
18
+ onLeave(client: KokimokiClient): Promise<void>;
14
19
  }
@@ -16,8 +16,8 @@ export class KokimokiStore {
16
16
  this.mode = mode;
17
17
  // Construct Y doc
18
18
  this.doc = new Y.Doc();
19
- // Construct proxy object
20
19
  this.docRoot = this.doc.getMap("root");
20
+ // Construct proxy object
21
21
  this.proxy = yjsProxy();
22
22
  // @ts-ignore
23
23
  yjsBind(this.proxy, this.docRoot);
@@ -45,9 +45,18 @@ export class KokimokiStore {
45
45
  // Set default value
46
46
  this.defaultValue = schema.defaultValue;
47
47
  }
48
- subscribe(callback) {
49
- const handler = () => callback(this.proxy);
48
+ get() {
49
+ return this.proxy;
50
+ }
51
+ subscribe(set) {
52
+ // @ts-ignore
53
+ const handler = () => set(this.proxy);
50
54
  this.doc.on("update", handler);
55
+ // @ts-ignore
56
+ set(this.proxy);
51
57
  return () => this.doc.off("update", handler);
52
58
  }
59
+ async onJoin(client) { }
60
+ async onBeforeLeave(client) { }
61
+ async onLeave(client) { }
53
62
  }
@@ -197,6 +197,12 @@ declare namespace KokimokiSchema {
197
197
  constructor(schemas: T, defaultValue: T[number]["defaultValue"]);
198
198
  }
199
199
  function anyOf<T extends readonly Generic<unknown>[]>(schemas: T, defaultValue: T[number]["defaultValue"]): AnyOf<T>;
200
+ class Optional<T extends Generic<unknown>> extends Generic<T["defaultValue"] | undefined> {
201
+ schema: T;
202
+ defaultValue: T["defaultValue"] | undefined;
203
+ constructor(schema: T, defaultValue?: T["defaultValue"] | undefined);
204
+ }
205
+ function optional<T extends Generic<unknown>>(schema: T, defaultValue?: T["defaultValue"] | undefined): Optional<T>;
200
206
  }
201
207
 
202
208
  declare enum RoomSubscriptionMode {
@@ -205,7 +211,7 @@ declare enum RoomSubscriptionMode {
205
211
  ReadWrite = "b"
206
212
  }
207
213
 
208
- declare class KokimokiStore<T extends KokimokiSchema.Generic<unknown>> {
214
+ declare class KokimokiStore<T extends KokimokiSchema.Generic<unknown>, SubscribeT = T["defaultValue"]> {
209
215
  readonly roomName: string;
210
216
  readonly mode: RoomSubscriptionMode;
211
217
  readonly doc: Y.Doc;
@@ -214,7 +220,11 @@ declare class KokimokiStore<T extends KokimokiSchema.Generic<unknown>> {
214
220
  readonly defaultValue: T["defaultValue"];
215
221
  readonly docRoot: Y.Map<unknown>;
216
222
  constructor(roomName: string, schema: T, mode?: RoomSubscriptionMode);
217
- subscribe(callback: (value: T["defaultValue"]) => void): () => void;
223
+ get(): T["defaultValue"];
224
+ subscribe(set: (value: SubscribeT) => void): () => void;
225
+ onJoin(client: KokimokiClient): Promise<void>;
226
+ onBeforeLeave(client: KokimokiClient): Promise<void>;
227
+ onLeave(client: KokimokiClient): Promise<void>;
218
228
  }
219
229
 
220
230
  declare class KokimokiQueue<Req extends KokimokiSchema.Generic<unknown>> extends KokimokiStore<KokimokiSchema.Dict<KokimokiSchema.Struct<{
@@ -280,6 +290,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
280
290
  private _wsUrl;
281
291
  private _apiUrl;
282
292
  private _id?;
293
+ private _connectionId?;
283
294
  private _token?;
284
295
  private _apiHeaders?;
285
296
  private _serverTimeOffset;
@@ -288,14 +299,17 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
288
299
  private _subscriptionsByName;
289
300
  private _subscriptionsByHash;
290
301
  private _subscribeReqPromises;
302
+ private _unsubscribeReqPromises;
291
303
  private _transactionPromises;
292
304
  private _connected;
293
305
  private _connectPromise?;
294
306
  private _messageId;
307
+ private _autoReconnect;
295
308
  private _reconnectTimeout;
296
309
  private _pingInterval;
297
310
  constructor(host: string, appId: string, code?: string);
298
311
  get id(): string;
312
+ get connectionId(): string;
299
313
  get token(): string;
300
314
  get apiUrl(): string;
301
315
  get apiHeaders(): Headers;
@@ -307,6 +321,7 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
307
321
  private handleBinaryMessage;
308
322
  private handleErrorMessage;
309
323
  private handleSubscribeResMessage;
324
+ private handleUnsubscribeResMessage;
310
325
  private handleRoomUpdateMessage;
311
326
  serverTimestamp(): number;
312
327
  patchRoomState(room: string, update: Uint8Array): Promise<any>;
@@ -327,9 +342,12 @@ declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
327
342
  deletedCount: number;
328
343
  }>;
329
344
  exposeScriptingContext(context: any): Promise<void>;
330
- private sendSubscriptionReq;
345
+ private sendSubscribeReq;
346
+ private sendUnsubscribeReq;
331
347
  join<T extends KokimokiSchema.Generic<unknown>>(store: KokimokiStore<T>): Promise<void>;
348
+ leave<T extends KokimokiSchema.Generic<unknown>>(store: KokimokiStore<T>): Promise<void>;
332
349
  transact(handler: (t: KokimokiTransaction) => void): Promise<void>;
350
+ close(): Promise<void>;
333
351
  }
334
352
 
335
353
  declare class RoomSubscription {
@@ -346,4 +364,16 @@ declare class RoomSubscription {
346
364
  close(): void;
347
365
  }
348
366
 
349
- export { BooleanField, ConstField, EnumField, Field, type FieldOptions, FloatField, Form, FormArray, FormGroup, ImageField, IntegerField, KokimokiClient, type KokimokiClientEvents, KokimokiQueue, KokimokiSchema, KokimokiStore, type Paginated, RoomSubscription, RoomSubscriptionMode, TextField, type Upload };
367
+ declare class KokimokiAwareness extends KokimokiStore<KokimokiSchema.Dict<KokimokiSchema.Struct<{
368
+ clientId: KokimokiSchema.String;
369
+ lastPing: KokimokiSchema.Number;
370
+ }>>> {
371
+ private _pingInterval;
372
+ private _kmClients;
373
+ constructor(roomName: string, mode?: RoomSubscriptionMode, pingTimeout?: number);
374
+ onJoin(client: KokimokiClient<any>): Promise<void>;
375
+ onBeforeLeave(client: KokimokiClient<any>): Promise<void>;
376
+ onLeave(client: KokimokiClient<any>): Promise<void>;
377
+ }
378
+
379
+ export { BooleanField, ConstField, EnumField, Field, type FieldOptions, FloatField, Form, FormArray, FormGroup, ImageField, IntegerField, KokimokiAwareness, KokimokiClient, type KokimokiClientEvents, KokimokiQueue, KokimokiSchema, KokimokiStore, type Paginated, RoomSubscription, RoomSubscriptionMode, TextField, type Upload };