@kokimoki/app 1.0.2 → 1.0.4

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.
@@ -4,10 +4,10 @@ import type { Upload } from "./types/upload";
4
4
  import type { Paginated } from "./types/common";
5
5
  import { KokimokiTransaction } from "./kokimoki-transaction";
6
6
  import type { KokimokiStore } from "./kokimoki-store";
7
- import type { KokimokiSchema as S } from "./kokimoki-schema";
7
+ import type { SyncedGeneric } from "./synced-schema";
8
8
  import { RoomSubscriptionMode } from "./room-subscription-mode";
9
- declare const KokimokiClient_base: new () => TypedEmitter<KokimokiClientEventsRefactored>;
10
- export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient_base {
9
+ declare const KokimokiClientRefactored_base: new () => TypedEmitter<KokimokiClientEventsRefactored>;
10
+ export declare class KokimokiClientRefactored<ClientContextT = any> extends KokimokiClientRefactored_base {
11
11
  readonly host: string;
12
12
  readonly appId: string;
13
13
  readonly code: string;
@@ -19,14 +19,15 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
19
19
  private _serverTimeOffset;
20
20
  private _clientContext?;
21
21
  private _ws?;
22
- private _subscriptionsByName;
23
- private _subscriptionsByHash;
24
- private _subscribeReqPromises;
25
- private _transactionPromises;
22
+ private _subscriptions;
23
+ private _stores;
24
+ private _reqPromises;
25
+ private _transactionResolves;
26
+ private _roomHashes;
27
+ private _roomHashToDoc;
26
28
  private _connected;
27
29
  private _connectPromise?;
28
30
  private _messageId;
29
- private _reconnectTimeout;
30
31
  constructor(host: string, appId: string, code?: string);
31
32
  get id(): string;
32
33
  get token(): string;
@@ -61,7 +62,7 @@ export declare class KokimokiClient<ClientContextT = any> extends KokimokiClient
61
62
  }>;
62
63
  exposeScriptingContext(context: any): Promise<void>;
63
64
  private sendSubscriptionReq;
64
- join<T extends S.Generic<unknown>>(store: KokimokiStore<T>, mode?: RoomSubscriptionMode): Promise<void>;
65
+ join<T extends SyncedGeneric<unknown>>(store: KokimokiStore<T>, mode?: RoomSubscriptionMode): Promise<void>;
65
66
  transact(handler: (t: KokimokiTransaction) => void): Promise<void>;
66
67
  }
67
68
  export {};
@@ -6,8 +6,7 @@ import { WsMessageType } from "./ws-message-type";
6
6
  import { WsMessageWriter } from "./ws-message-writer";
7
7
  import { WsMessageReader } from "./ws-message-reader";
8
8
  import { RoomSubscriptionMode } from "./room-subscription-mode";
9
- import { RoomSubscription } from "./room-subscription";
10
- export class KokimokiClient extends EventEmitter {
9
+ export class KokimokiClientRefactored extends EventEmitter {
11
10
  host;
12
11
  appId;
13
12
  code;
@@ -19,14 +18,15 @@ export class KokimokiClient extends EventEmitter {
19
18
  _serverTimeOffset = 0;
20
19
  _clientContext;
21
20
  _ws;
22
- _subscriptionsByName = new Map();
23
- _subscriptionsByHash = new Map();
24
- _subscribeReqPromises = new Map();
25
- _transactionPromises = new Map();
21
+ _subscriptions = new Map();
22
+ _stores = new Map();
23
+ _reqPromises = new Map();
24
+ _transactionResolves = new Map();
25
+ _roomHashes = new Map();
26
+ _roomHashToDoc = new Map();
26
27
  _connected = false;
27
28
  _connectPromise;
28
29
  _messageId = 0;
29
- _reconnectTimeout = 0;
30
30
  constructor(host, appId, code = "") {
31
31
  super();
32
32
  this.host = host;
@@ -82,17 +82,6 @@ export class KokimokiClient extends EventEmitter {
82
82
  }
83
83
  this._ws = new WebSocket(`${this._wsUrl}/apps/${this.appId}?clientVersion=${KOKIMOKI_APP_VERSION}`);
84
84
  this._ws.binaryType = "arraybuffer";
85
- // Close previous connection in hot-reload scenarios
86
- if (window) {
87
- if (!window.__KOKIMOKI_WS__) {
88
- window.__KOKIMOKI_WS__ = {};
89
- }
90
- if (this.appId in window.__KOKIMOKI_WS__) {
91
- window.__KOKIMOKI_WS__[this.appId].close();
92
- }
93
- window.__KOKIMOKI_WS__[this.appId] = this._ws;
94
- }
95
- // Wait for connection
96
85
  this._connectPromise = new Promise((onInit) => {
97
86
  // Fetch the auth token
98
87
  let clientToken = localStorage.getItem("KM_TOKEN");
@@ -104,18 +93,16 @@ export class KokimokiClient extends EventEmitter {
104
93
  this._connected = false;
105
94
  this._connectPromise = undefined;
106
95
  this._ws = undefined;
107
- if (window && window.__KOKIMOKI_WS__) {
108
- delete window.__KOKIMOKI_WS__[this.appId];
109
- }
110
96
  // Clean up
111
- this._subscribeReqPromises.clear();
112
- this._transactionPromises.clear();
113
- // Attempt to reconnect
114
- console.log(`[Kokimoki] Connection lost, attempting to reconnect in ${this._reconnectTimeout} seconds...`);
115
- setTimeout(async () => await this.connect(), this._reconnectTimeout * 1000);
116
- this._reconnectTimeout = Math.min(3, this._reconnectTimeout + 1);
97
+ this._reqPromises.clear();
98
+ this._transactionResolves.clear();
99
+ this._roomHashes.clear();
100
+ this._roomHashToDoc.clear();
101
+ this._subscriptions.clear();
117
102
  // Emit disconnected event
118
103
  this.emit("disconnected");
104
+ // Attempt to reconnect
105
+ // setTimeout(async () => await this.connect(), 2000);
119
106
  };
120
107
  this.ws.onmessage = (e) => {
121
108
  console.log(`Received WS message: ${e.data}`);
@@ -140,26 +127,6 @@ export class KokimokiClient extends EventEmitter {
140
127
  };
141
128
  });
142
129
  await this._connectPromise;
143
- this._connected = true;
144
- // Connection established
145
- console.log(`[Kokimoki] Client id: ${this.id}`);
146
- console.log(`[Kokimoki] Client context:`, this.clientContext);
147
- // Restore subscriptions if reconnected
148
- const roomNames = Array.from(this._subscriptionsByName.keys()).map((name) => `"${name}"`);
149
- if (roomNames.length) {
150
- console.log(`[Kokimoki] Restoring subscriptions: ${roomNames}`);
151
- }
152
- for (const subscription of this._subscriptionsByName.values()) {
153
- try {
154
- await this.join(subscription.store);
155
- }
156
- catch (err) {
157
- console.error(`[Kokimoki] Failed to restore subscription for "${subscription.roomName}":`, err);
158
- }
159
- }
160
- // Emit connected event
161
- this._reconnectTimeout = 0;
162
- this.emit("connected");
163
130
  }
164
131
  handleInitMessage(message) {
165
132
  localStorage.setItem("KM_TOKEN", message.clientToken);
@@ -171,6 +138,8 @@ export class KokimokiClient extends EventEmitter {
171
138
  Authorization: `Bearer ${this.token}`,
172
139
  "Content-Type": "application/json",
173
140
  });
141
+ this._connected = true;
142
+ this.emit("connected");
174
143
  }
175
144
  handleBinaryMessage(data) {
176
145
  const reader = new WsMessageReader(data);
@@ -190,15 +159,10 @@ export class KokimokiClient extends EventEmitter {
190
159
  handleErrorMessage(msg) {
191
160
  const reqId = msg.readInt32();
192
161
  const error = msg.readString();
193
- const subscribeReqPromise = this._subscribeReqPromises.get(reqId);
194
- const transactionPromise = this._transactionPromises.get(reqId);
195
- if (subscribeReqPromise) {
196
- this._subscribeReqPromises.delete(reqId);
197
- subscribeReqPromise.reject(error);
198
- }
199
- else if (transactionPromise) {
200
- this._transactionPromises.delete(reqId);
201
- transactionPromise.reject(error);
162
+ const promise = this._reqPromises.get(reqId);
163
+ if (promise) {
164
+ this._reqPromises.delete(reqId);
165
+ promise.reject(error);
202
166
  }
203
167
  else {
204
168
  console.warn(`Received error for unknown request ${reqId}: ${error}`);
@@ -207,9 +171,9 @@ export class KokimokiClient extends EventEmitter {
207
171
  handleSubscribeResMessage(msg) {
208
172
  const reqId = msg.readInt32();
209
173
  const roomHash = msg.readUint32();
210
- const promise = this._subscribeReqPromises.get(reqId);
174
+ const promise = this._reqPromises.get(reqId);
211
175
  if (promise) {
212
- this._subscribeReqPromises.delete(reqId);
176
+ this._reqPromises.delete(reqId);
213
177
  // In Write mode, no initial state is sent
214
178
  if (!msg.end) {
215
179
  promise.resolve(roomHash, msg.readUint8Array());
@@ -224,18 +188,18 @@ export class KokimokiClient extends EventEmitter {
224
188
  const roomHash = msg.readUint32();
225
189
  // Apply update if not in Write mode
226
190
  if (!msg.end) {
227
- const subscription = this._subscriptionsByHash.get(roomHash);
228
- if (subscription) {
229
- Y.applyUpdate(subscription.store.doc, msg.readUint8Array(), this);
191
+ const doc = this._roomHashToDoc.get(roomHash);
192
+ if (doc) {
193
+ Y.applyUpdate(doc, msg.readUint8Array(), this);
230
194
  }
231
195
  else {
232
196
  console.warn(`Received update for unknown room ${roomHash}`);
233
197
  }
234
198
  }
235
199
  // Check transaction resolves
236
- for (const [transactionId, { resolve },] of this._transactionPromises.entries()) {
200
+ for (const [transactionId, resolve,] of this._transactionResolves.entries()) {
237
201
  if (appliedId >= transactionId) {
238
- this._transactionPromises.delete(transactionId);
202
+ this._transactionResolves.delete(transactionId);
239
203
  resolve();
240
204
  }
241
205
  }
@@ -331,7 +295,7 @@ export class KokimokiClient extends EventEmitter {
331
295
  // Set up sync resolver
332
296
  const reqId = ++this._messageId;
333
297
  return await new Promise((resolve, reject) => {
334
- this._subscribeReqPromises.set(reqId, {
298
+ this._reqPromises.set(reqId, {
335
299
  resolve: (roomHash, initialUpdate) => {
336
300
  resolve({ roomHash, initialUpdate });
337
301
  },
@@ -347,25 +311,42 @@ export class KokimokiClient extends EventEmitter {
347
311
  });
348
312
  }
349
313
  async join(store, mode = RoomSubscriptionMode.ReadWrite) {
350
- let subscription = this._subscriptionsByName.get(store.roomName);
351
- if (!subscription) {
352
- subscription = new RoomSubscription(this, store, mode);
353
- this._subscriptionsByName.set(store.roomName, subscription);
314
+ if (this._subscriptions.has(store.roomName)) {
315
+ throw new Error(`Already joined room "${store.roomName}"`);
354
316
  }
355
- // Send subscription request if connected to server
356
- if (!subscription.joined) {
317
+ this._subscriptions.set(store.roomName, mode);
318
+ // Send req
319
+ try {
357
320
  const res = await this.sendSubscriptionReq(store.roomName, mode);
358
- this._subscriptionsByHash.set(res.roomHash, subscription);
359
- await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
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;
360
344
  }
361
345
  }
362
346
  async transact(handler) {
363
- if (!this._connected) {
364
- throw new Error("Client not connected");
365
- }
366
347
  const transaction = new KokimokiTransaction(this);
367
348
  handler(transaction);
368
- const { updates, consumedMessages } = await transaction.getUpdates();
349
+ const updates = await transaction.getUpdates();
369
350
  if (!updates.length) {
370
351
  return;
371
352
  }
@@ -376,28 +357,19 @@ export class KokimokiClient extends EventEmitter {
376
357
  // Update and write transaction ID
377
358
  const transactionId = ++this._messageId;
378
359
  writer.writeInt32(transactionId);
379
- // Write room hashes where messages were consumed
380
- writer.writeInt32(consumedMessages.size);
381
- for (const roomName of consumedMessages) {
382
- const subscription = this._subscriptionsByName.get(roomName);
383
- if (!subscription) {
384
- throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
385
- }
386
- writer.writeUint32(subscription.roomHash);
387
- }
388
360
  // Write updates
389
361
  for (const { roomName, update } of updates) {
390
- const subscription = this._subscriptionsByName.get(roomName);
391
- if (!subscription) {
362
+ const roomHash = this._roomHashes.get(roomName);
363
+ if (!roomHash) {
392
364
  throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
393
365
  }
394
- writer.writeUint32(subscription.roomHash);
366
+ writer.writeUint32(roomHash);
395
367
  writer.writeUint8Array(update);
396
368
  }
397
369
  const buffer = writer.getBuffer();
398
370
  // Wait for server to apply transaction
399
- await new Promise((resolve, reject) => {
400
- this._transactionPromises.set(transactionId, { resolve, reject });
371
+ await new Promise((resolve) => {
372
+ this._transactionResolves.set(transactionId, resolve);
401
373
  // Send update to server
402
374
  try {
403
375
  this.ws.send(buffer);
@@ -49,10 +49,30 @@ export declare namespace KokimokiSchema {
49
49
  constructor(schema: T, defaultValue?: T["defaultValue"][]);
50
50
  }
51
51
  function list<T extends Generic<unknown>>(schema: T, defaultValue?: T["defaultValue"][]): List<T>;
52
+ /**
53
+ * Nullable
54
+ */
52
55
  class Nullable<T extends Generic<unknown>> extends Generic<T["defaultValue"] | null> {
53
56
  schema: T;
54
57
  defaultValue: T["defaultValue"] | null;
55
58
  constructor(schema: T, defaultValue: T["defaultValue"] | null);
56
59
  }
57
60
  function nullable<T extends Generic<unknown>>(schema: T, defaultValue?: T["defaultValue"] | null): Nullable<T>;
61
+ class StringEnum<T extends readonly string[]> extends Generic<string> {
62
+ readonly options: T;
63
+ defaultValue: T[number];
64
+ constructor(options: T, defaultValue: T[number]);
65
+ }
66
+ function stringEnum<T extends readonly string[]>(options: T, defaultValue: T[number]): StringEnum<T>;
67
+ class StringConst<T extends string> extends Generic<string> {
68
+ readonly defaultValue: T;
69
+ constructor(defaultValue: T);
70
+ }
71
+ function stringConst<T extends string>(defaultValue: T): StringConst<T>;
72
+ class AnyOf<T extends readonly Generic<unknown>[]> extends Generic<T[number]["defaultValue"]> {
73
+ readonly schemas: T;
74
+ defaultValue: T[number]["defaultValue"];
75
+ constructor(schemas: T, defaultValue: T[number]["defaultValue"]);
76
+ }
77
+ function anyOf<T extends readonly Generic<unknown>[]>(schemas: T, defaultValue: T[number]["defaultValue"]): AnyOf<T>;
58
78
  }
@@ -89,6 +89,9 @@ export var KokimokiSchema;
89
89
  return new List(schema, defaultValue);
90
90
  }
91
91
  KokimokiSchema.list = list;
92
+ /**
93
+ * Nullable
94
+ */
92
95
  class Nullable extends Generic {
93
96
  schema;
94
97
  defaultValue;
@@ -103,4 +106,44 @@ export var KokimokiSchema;
103
106
  return new Nullable(schema, defaultValue);
104
107
  }
105
108
  KokimokiSchema.nullable = nullable;
109
+ class StringEnum extends Generic {
110
+ options;
111
+ defaultValue;
112
+ constructor(options, defaultValue) {
113
+ super();
114
+ this.options = options;
115
+ this.defaultValue = defaultValue;
116
+ }
117
+ }
118
+ KokimokiSchema.StringEnum = StringEnum;
119
+ function stringEnum(options, defaultValue) {
120
+ return new StringEnum(options, defaultValue);
121
+ }
122
+ KokimokiSchema.stringEnum = stringEnum;
123
+ class StringConst extends Generic {
124
+ defaultValue;
125
+ constructor(defaultValue) {
126
+ super();
127
+ this.defaultValue = defaultValue;
128
+ }
129
+ }
130
+ KokimokiSchema.StringConst = StringConst;
131
+ function stringConst(defaultValue) {
132
+ return new StringConst(defaultValue);
133
+ }
134
+ KokimokiSchema.stringConst = stringConst;
135
+ class AnyOf extends Generic {
136
+ schemas;
137
+ defaultValue;
138
+ constructor(schemas, defaultValue) {
139
+ super();
140
+ this.schemas = schemas;
141
+ this.defaultValue = defaultValue;
142
+ }
143
+ }
144
+ KokimokiSchema.AnyOf = AnyOf;
145
+ function anyOf(schemas, defaultValue) {
146
+ return new AnyOf(schemas, defaultValue);
147
+ }
148
+ KokimokiSchema.anyOf = anyOf;
106
149
  })(KokimokiSchema || (KokimokiSchema = {}));
@@ -10,7 +10,7 @@ export declare class KokimokiTransaction {
10
10
  private _parseTarget;
11
11
  private _parsePath;
12
12
  private _getClone;
13
- get<T>(target: T): any;
13
+ get<T>(target: T): T;
14
14
  set<T>(target: T, value: T): void;
15
15
  delete<T>(target: T): void;
16
16
  push<T>(target: T[], value: T): void;
@@ -1,52 +1,59 @@
1
- export declare namespace S {
2
- abstract class Generic<T> {
3
- abstract get defaultValue(): T;
4
- abstract set defaultValue(value: T);
5
- }
6
- class Number extends Generic<number> {
7
- defaultValue: number;
8
- constructor(defaultValue?: number);
9
- }
10
- function number(defaultValue?: number): Number;
11
- class String extends Generic<string> {
12
- defaultValue: string;
13
- constructor(defaultValue?: string);
14
- }
15
- function string(defaultValue?: string): String;
16
- class Boolean extends Generic<boolean> {
17
- defaultValue: boolean;
18
- constructor(defaultValue?: boolean);
19
- }
20
- function boolean(defaultValue?: boolean): Boolean;
21
- class Struct<Data extends Record<string, Generic<unknown>>> extends Generic<{
1
+ export declare abstract class SyncedGeneric<T> {
2
+ abstract get defaultValue(): T;
3
+ abstract set defaultValue(value: T);
4
+ }
5
+ export declare class SyncedNumber extends SyncedGeneric<number> {
6
+ defaultValue: number;
7
+ constructor(defaultValue?: number);
8
+ }
9
+ declare function syncedNumber(defaultValue?: number): SyncedNumber;
10
+ export declare class SyncedString extends SyncedGeneric<string> {
11
+ defaultValue: string;
12
+ constructor(defaultValue?: string);
13
+ }
14
+ declare function syncedString(defaultValue?: string): SyncedString;
15
+ export declare class SyncedBoolean extends SyncedGeneric<boolean> {
16
+ defaultValue: boolean;
17
+ constructor(defaultValue?: boolean);
18
+ }
19
+ declare function syncedBoolean(defaultValue?: boolean): SyncedBoolean;
20
+ export declare class SyncedObject<Data extends Record<string, SyncedGeneric<unknown>>> extends SyncedGeneric<{
21
+ [key in keyof Data]: Data[key]["defaultValue"];
22
+ }> {
23
+ fields: Data;
24
+ constructor(fields: Data);
25
+ get defaultValue(): {
22
26
  [key in keyof Data]: Data[key]["defaultValue"];
23
- }> {
24
- fields: Data;
25
- constructor(fields: Data);
26
- get defaultValue(): {
27
- [key in keyof Data]: Data[key]["defaultValue"];
28
- };
29
- set defaultValue(value: {
30
- [key in keyof Data]: Data[key]["defaultValue"];
31
- });
32
- }
33
- function struct<Data extends Record<string, Generic<unknown>>>(schema: Data): Struct<Data>;
34
- class Dict<T extends Generic<unknown>> {
35
- schema: T;
36
- defaultValue: {
37
- [key: string]: T["defaultValue"];
38
- };
39
- constructor(schema: T, defaultValue?: {
40
- [key: string]: T["defaultValue"];
41
- });
42
- }
43
- function dict<T extends Generic<unknown>>(schema: T, defaultValue?: {
27
+ };
28
+ set defaultValue(value: {
29
+ [key in keyof Data]: Data[key]["defaultValue"];
30
+ });
31
+ }
32
+ declare function syncedObject<Data extends Record<string, SyncedGeneric<unknown>>>(schema: Data): SyncedObject<Data>;
33
+ export declare class SyncedMap<T extends SyncedGeneric<unknown>> {
34
+ schema: T;
35
+ defaultValue: {
44
36
  [key: string]: T["defaultValue"];
45
- }): Dict<T>;
46
- class List<T extends Generic<unknown>> extends Generic<T["defaultValue"][]> {
47
- schema: T;
48
- defaultValue: T["defaultValue"][];
49
- constructor(schema: T, defaultValue?: T["defaultValue"][]);
50
- }
51
- function list<T extends Generic<unknown>>(schema: T, defaultValue?: T["defaultValue"][]): List<T>;
37
+ };
38
+ constructor(schema: T, defaultValue?: {
39
+ [key: string]: T["defaultValue"];
40
+ });
41
+ }
42
+ declare function syncedMap<T extends SyncedGeneric<unknown>>(schema: T, defaultValue?: {
43
+ [key: string]: T["defaultValue"];
44
+ }): SyncedMap<T>;
45
+ export declare class SyncedArray<T extends SyncedGeneric<unknown>> extends SyncedGeneric<T["defaultValue"][]> {
46
+ schema: T;
47
+ defaultValue: T["defaultValue"][];
48
+ constructor(schema: T, defaultValue?: T["defaultValue"][]);
52
49
  }
50
+ declare function syncedArray<T extends SyncedGeneric<unknown>>(schema: T, defaultValue?: T["defaultValue"][]): SyncedArray<T>;
51
+ export declare const S: {
52
+ number: typeof syncedNumber;
53
+ string: typeof syncedString;
54
+ boolean: typeof syncedBoolean;
55
+ object: typeof syncedObject;
56
+ map: typeof syncedMap;
57
+ array: typeof syncedArray;
58
+ };
59
+ export {};
@@ -1,92 +1,84 @@
1
- export var S;
2
- (function (S) {
3
- class Generic {
1
+ export class SyncedGeneric {
2
+ }
3
+ export class SyncedNumber extends SyncedGeneric {
4
+ defaultValue;
5
+ constructor(defaultValue = 0) {
6
+ super();
7
+ this.defaultValue = defaultValue;
4
8
  }
5
- S.Generic = Generic;
6
- class Number extends Generic {
7
- defaultValue;
8
- constructor(defaultValue = 0) {
9
- super();
10
- this.defaultValue = defaultValue;
11
- }
12
- }
13
- S.Number = Number;
14
- function number(defaultValue = 0) {
15
- return new Number(defaultValue);
16
- }
17
- S.number = number;
18
- class String extends Generic {
19
- defaultValue;
20
- constructor(defaultValue = "") {
21
- super();
22
- this.defaultValue = defaultValue;
23
- }
24
- }
25
- S.String = String;
26
- function string(defaultValue = "") {
27
- return new String(defaultValue);
28
- }
29
- S.string = string;
30
- class Boolean extends Generic {
31
- defaultValue;
32
- constructor(defaultValue = false) {
33
- super();
34
- this.defaultValue = defaultValue;
35
- }
9
+ }
10
+ function syncedNumber(defaultValue = 0) {
11
+ return new SyncedNumber(defaultValue);
12
+ }
13
+ export class SyncedString extends SyncedGeneric {
14
+ defaultValue;
15
+ constructor(defaultValue = "") {
16
+ super();
17
+ this.defaultValue = defaultValue;
36
18
  }
37
- S.Boolean = Boolean;
38
- function boolean(defaultValue = false) {
39
- return new Boolean(defaultValue);
19
+ }
20
+ function syncedString(defaultValue = "") {
21
+ return new SyncedString(defaultValue);
22
+ }
23
+ export class SyncedBoolean extends SyncedGeneric {
24
+ defaultValue;
25
+ constructor(defaultValue = false) {
26
+ super();
27
+ this.defaultValue = defaultValue;
40
28
  }
41
- S.boolean = boolean;
42
- class Struct extends Generic {
43
- fields;
44
- constructor(fields) {
45
- super();
46
- this.fields = fields;
47
- }
48
- get defaultValue() {
49
- return Object.entries(this.fields).reduce((acc, [key, field]) => {
50
- acc[key] = field.defaultValue;
51
- return acc;
52
- }, {});
53
- }
54
- set defaultValue(value) {
55
- for (const [key, field] of Object.entries(this.fields)) {
56
- field.defaultValue = value[key];
57
- }
58
- }
29
+ }
30
+ function syncedBoolean(defaultValue = false) {
31
+ return new SyncedBoolean(defaultValue);
32
+ }
33
+ export class SyncedObject extends SyncedGeneric {
34
+ fields;
35
+ constructor(fields) {
36
+ super();
37
+ this.fields = fields;
59
38
  }
60
- S.Struct = Struct;
61
- function struct(schema) {
62
- return new Struct(schema);
39
+ get defaultValue() {
40
+ return Object.entries(this.fields).reduce((acc, [key, field]) => {
41
+ acc[key] = field.defaultValue;
42
+ return acc;
43
+ }, {});
63
44
  }
64
- S.struct = struct;
65
- class Dict {
66
- schema;
67
- defaultValue;
68
- constructor(schema, defaultValue = {}) {
69
- this.schema = schema;
70
- this.defaultValue = defaultValue;
45
+ set defaultValue(value) {
46
+ for (const [key, field] of Object.entries(this.fields)) {
47
+ field.defaultValue = value[key];
71
48
  }
72
49
  }
73
- S.Dict = Dict;
74
- function dict(schema, defaultValue = {}) {
75
- return new Dict(schema, defaultValue);
76
- }
77
- S.dict = dict;
78
- class List extends Generic {
79
- schema;
80
- defaultValue;
81
- constructor(schema, defaultValue = []) {
82
- super();
83
- this.schema = schema;
84
- this.defaultValue = defaultValue;
85
- }
50
+ }
51
+ function syncedObject(schema) {
52
+ return new SyncedObject(schema);
53
+ }
54
+ export class SyncedMap {
55
+ schema;
56
+ defaultValue;
57
+ constructor(schema, defaultValue = {}) {
58
+ this.schema = schema;
59
+ this.defaultValue = defaultValue;
86
60
  }
87
- S.List = List;
88
- function list(schema, defaultValue = []) {
89
- return new List(schema, defaultValue);
61
+ }
62
+ function syncedMap(schema, defaultValue = {}) {
63
+ return new SyncedMap(schema, defaultValue);
64
+ }
65
+ export class SyncedArray extends SyncedGeneric {
66
+ schema;
67
+ defaultValue;
68
+ constructor(schema, defaultValue = []) {
69
+ super();
70
+ this.schema = schema;
71
+ this.defaultValue = defaultValue;
90
72
  }
91
- S.list = list;
92
- })(S || (S = {}));
73
+ }
74
+ function syncedArray(schema, defaultValue = []) {
75
+ return new SyncedArray(schema, defaultValue);
76
+ }
77
+ export const S = {
78
+ number: syncedNumber,
79
+ string: syncedString,
80
+ boolean: syncedBoolean,
81
+ object: syncedObject,
82
+ map: syncedMap,
83
+ array: syncedArray,
84
+ };
@@ -36,10 +36,3 @@ export declare class SyncedArray<T extends SyncedGeneric<unknown>> extends Synce
36
36
  constructor(schema: T, defaultValue?: T["defaultValue"][]);
37
37
  }
38
38
  export declare function syncedArray<T extends SyncedGeneric<unknown>>(schema: T, defaultValue?: T["defaultValue"][]): SyncedArray<T>;
39
- export declare const S: {
40
- number: typeof syncedNumber;
41
- string: typeof syncedString;
42
- boolean: typeof syncedBoolean;
43
- map: typeof syncedMap;
44
- array: typeof syncedArray;
45
- };
@@ -1,3 +1,6 @@
1
+ import * as Y from "yjs";
2
+ import { proxy as yjsProxy } from "valtio";
3
+ import { bind as yjsBind } from "valtio-yjs";
1
4
  export class SyncedGeneric {
2
5
  }
3
6
  export class SyncedNumber extends SyncedGeneric {
@@ -63,10 +66,3 @@ export class SyncedArray extends SyncedGeneric {
63
66
  export function syncedArray(schema, defaultValue = []) {
64
67
  return new SyncedArray(schema, defaultValue);
65
68
  }
66
- export const S = {
67
- number: syncedNumber,
68
- string: syncedString,
69
- boolean: syncedBoolean,
70
- map: syncedMap,
71
- array: syncedArray,
72
- };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const KOKIMOKI_APP_VERSION = "1.0.2";
1
+ export declare const KOKIMOKI_APP_VERSION = "1.0.4";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const KOKIMOKI_APP_VERSION = "1.0.2";
1
+ export const KOKIMOKI_APP_VERSION = "1.0.4";
@@ -0,0 +1,6 @@
1
+ export declare enum WsMessageType {
2
+ SubscribeReq = 1,
3
+ SubscribeRes = 2,
4
+ Transaction = 3,
5
+ RoomUpdate = 4
6
+ }
@@ -2,4 +2,6 @@ export var WsMessageType;
2
2
  (function (WsMessageType) {
3
3
  WsMessageType[WsMessageType["SubscribeReq"] = 1] = "SubscribeReq";
4
4
  WsMessageType[WsMessageType["SubscribeRes"] = 2] = "SubscribeRes";
5
+ WsMessageType[WsMessageType["Transaction"] = 3] = "Transaction";
6
+ WsMessageType[WsMessageType["RoomUpdate"] = 4] = "RoomUpdate";
5
7
  })(WsMessageType || (WsMessageType = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokimoki/app",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Kokimoki app",
6
6
  "main": "dist/index.js",
@@ -1,57 +0,0 @@
1
- import { HocuspocusProvider } from "@hocuspocus/provider";
2
- import type TypedEmitter from "typed-emitter";
3
- import type { SyncedStore } from "./synced-store";
4
- import type { DocTypeDescription } from "@syncedstore/core/types/doc";
5
- import type { KokimokiClientEvents } from "./types/events";
6
- import type { Upload } from "./types/upload";
7
- import type { Paginated } from "./types/common";
8
- declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
9
- export declare class KokimokiClient<StatelessDataT = any, ClientContextT = any> extends KokimokiClient_base<StatelessDataT> {
10
- readonly host: string;
11
- readonly appId: string;
12
- readonly code: string;
13
- private _wsUrl;
14
- private _apiUrl;
15
- private _id?;
16
- private _token?;
17
- private _apiHeaders?;
18
- private _providers;
19
- private _serverTimeOffset;
20
- private _clientContext?;
21
- private _connected;
22
- private _lastPongAt;
23
- constructor(host: string, appId: string, code?: string);
24
- get id(): string;
25
- get token(): string;
26
- get apiUrl(): string;
27
- get apiHeaders(): Headers;
28
- get clientContext(): ClientContextT & ({} | null);
29
- connect(): Promise<void>;
30
- serverTimestamp(): number;
31
- private receivePong;
32
- private checkConnectionState;
33
- setProvider<T extends DocTypeDescription>(name: string, store: SyncedStore<T>): Promise<void>;
34
- removeProvider(name: string): void;
35
- getProvider(name: string): HocuspocusProvider | undefined;
36
- sendStatelessToClient(room: string, clientId: string, data: StatelessDataT): void;
37
- sendStatelessToRoom(room: string, data: StatelessDataT, self?: boolean): void;
38
- patchRoomState(room: string, update: Uint8Array): Promise<any>;
39
- private createUpload;
40
- private uploadChunks;
41
- private completeUpload;
42
- upload(name: string, blob: Blob, tags?: string[]): Promise<Upload>;
43
- updateUpload(id: string, update: {
44
- tags?: string[];
45
- }): Promise<Upload>;
46
- listUploads(filter?: {
47
- clientId?: string;
48
- mimeTypes?: string[];
49
- tags?: string[];
50
- }, skip?: number, limit?: number): Promise<Paginated<Upload>>;
51
- deleteUpload(id: string): Promise<{
52
- acknowledged: boolean;
53
- deletedCount: number;
54
- }>;
55
- exposeScriptingContext(context: any): Promise<void>;
56
- }
57
- export {};
@@ -1,259 +0,0 @@
1
- import { HocuspocusProvider } from "@hocuspocus/provider";
2
- import EventEmitter from "events";
3
- import { KOKIMOKI_APP_VERSION } from "./version";
4
- export class KokimokiClient extends EventEmitter {
5
- host;
6
- appId;
7
- code;
8
- _wsUrl;
9
- _apiUrl;
10
- _id;
11
- _token;
12
- _apiHeaders;
13
- _providers = new Map();
14
- _serverTimeOffset = 0;
15
- _clientContext;
16
- _connected = false;
17
- _lastPongAt = 0;
18
- constructor(host, appId, code = "") {
19
- super();
20
- this.host = host;
21
- this.appId = appId;
22
- this.code = code;
23
- // Set up the URLs
24
- const secure = this.host.indexOf(":") === -1;
25
- this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
26
- this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
27
- }
28
- get id() {
29
- if (!this._id) {
30
- throw new Error("Client not connected");
31
- }
32
- return this._id;
33
- }
34
- get token() {
35
- if (!this._token) {
36
- throw new Error("Client not connected");
37
- }
38
- return this._token;
39
- }
40
- get apiUrl() {
41
- if (!this._apiUrl) {
42
- throw new Error("Client not connected");
43
- }
44
- return this._apiUrl;
45
- }
46
- get apiHeaders() {
47
- if (!this._apiHeaders) {
48
- throw new Error("Client not connected");
49
- }
50
- return this._apiHeaders;
51
- }
52
- get clientContext() {
53
- if (this._clientContext === undefined) {
54
- throw new Error("Client not connected");
55
- }
56
- return this._clientContext;
57
- }
58
- async connect() {
59
- // Fetch the auth token
60
- let clientToken = localStorage.getItem("KM_TOKEN");
61
- const startTime = Date.now();
62
- const res = await fetch(`${this.apiUrl}/auth/token?appId=${this.appId}&code=${this.code}&clientVersion=${KOKIMOKI_APP_VERSION}`, {
63
- method: "GET",
64
- headers: new Headers({
65
- "Content-Type": "application/json",
66
- Authorization: `Bearer ${clientToken}`,
67
- }),
68
- });
69
- const { clientId, appToken, serverTime, token, clientContext } = await res.json();
70
- const endTime = Date.now();
71
- const ping = Math.round((endTime - startTime) / 2);
72
- this._id = clientId;
73
- this._token = appToken;
74
- this._serverTimeOffset = Date.now() - serverTime - ping;
75
- this._clientContext = clientContext;
76
- localStorage.setItem("KM_TOKEN", token);
77
- // Set up the auth headers
78
- this._apiHeaders = new Headers({
79
- Authorization: `Bearer ${this.token}`,
80
- "Content-Type": "application/json",
81
- });
82
- // Ping interval
83
- setInterval(() => {
84
- this._providers.forEach((provider) => provider.sendStateless("ping"));
85
- }, 5000);
86
- // Connection state interval
87
- setInterval(() => {
88
- this.checkConnectionState();
89
- }, 1000);
90
- // Check initial connected state
91
- this.receivePong();
92
- }
93
- serverTimestamp() {
94
- return Date.now() - this._serverTimeOffset;
95
- }
96
- receivePong() {
97
- this._lastPongAt = Date.now();
98
- this.checkConnectionState();
99
- }
100
- checkConnectionState() {
101
- const connected = this._providers.size === 0 || Date.now() - this._lastPongAt < 6000;
102
- if (connected && !this._connected) {
103
- this._connected = true;
104
- this.emit("connected");
105
- }
106
- else if (!connected && this._connected) {
107
- this._connected = false;
108
- this.emit("disconnected");
109
- // Reset connections to providers
110
- this._providers.forEach(async (provider) => {
111
- provider.disconnect();
112
- await provider.connect();
113
- });
114
- }
115
- }
116
- // Realtime database
117
- async setProvider(name, store) {
118
- const provider = new HocuspocusProvider({
119
- url: `${this._wsUrl}/connection`,
120
- name: `${this.appId}/${name}`,
121
- document: store.doc,
122
- token: this.token,
123
- parameters: {
124
- clientVersion: KOKIMOKI_APP_VERSION,
125
- },
126
- });
127
- // Handle incoming stateless messages
128
- provider.on("stateless", (e) => {
129
- if (e.payload === "pong") {
130
- this.receivePong();
131
- return;
132
- }
133
- const payload = JSON.parse(e.payload);
134
- this.emit("stateless", name, payload.from, payload.data);
135
- });
136
- // Wait for initial sync
137
- await new Promise((resolve) => {
138
- const handler = () => {
139
- provider.off("synced", handler);
140
- resolve();
141
- };
142
- provider.on("synced", handler);
143
- });
144
- this._lastPongAt = Date.now();
145
- this._providers.set(name, provider);
146
- this.checkConnectionState();
147
- }
148
- removeProvider(name) {
149
- const provider = this._providers.get(name);
150
- if (!provider) {
151
- throw new Error(`No provider for room ${name}`);
152
- }
153
- provider.destroy();
154
- this._providers.delete(name);
155
- // Connection state can change if the removed provider was not connected or synced
156
- this.checkConnectionState();
157
- }
158
- getProvider(name) {
159
- return this._providers.get(name);
160
- }
161
- sendStatelessToClient(room, clientId, data) {
162
- const provider = this._providers.get(room);
163
- if (!provider) {
164
- throw new Error(`No provider for room ${room}`);
165
- }
166
- provider.sendStateless(JSON.stringify({ to: clientId, data }));
167
- }
168
- sendStatelessToRoom(room, data, self = false) {
169
- const provider = this._providers.get(room);
170
- if (!provider) {
171
- throw new Error(`No provider for room ${room}`);
172
- }
173
- provider.sendStateless(JSON.stringify({ data, self }));
174
- }
175
- // Send Y update to room
176
- async patchRoomState(room, update) {
177
- const res = await fetch(`${this._apiUrl}/rooms/${room}`, {
178
- method: "PATCH",
179
- headers: this.apiHeaders,
180
- body: update,
181
- });
182
- return await res.json();
183
- }
184
- // Storage
185
- async createUpload(name, blob, tags) {
186
- const res = await fetch(`${this._apiUrl}/uploads`, {
187
- method: "POST",
188
- headers: this.apiHeaders,
189
- body: JSON.stringify({
190
- name,
191
- size: blob.size,
192
- mimeType: blob.type,
193
- tags,
194
- }),
195
- });
196
- return await res.json();
197
- }
198
- async uploadChunks(blob, chunkSize, signedUrls) {
199
- return await Promise.all(signedUrls.map(async (url, index) => {
200
- const start = index * chunkSize;
201
- const end = Math.min(start + chunkSize, blob.size);
202
- const chunk = blob.slice(start, end);
203
- const res = await fetch(url, { method: "PUT", body: chunk });
204
- return JSON.parse(res.headers.get("ETag") || '""');
205
- }));
206
- }
207
- async completeUpload(id, etags) {
208
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
209
- method: "PUT",
210
- headers: this.apiHeaders,
211
- body: JSON.stringify({ etags }),
212
- });
213
- return await res.json();
214
- }
215
- async upload(name, blob, tags = []) {
216
- const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
217
- const etags = await this.uploadChunks(blob, chunkSize, urls);
218
- return await this.completeUpload(id, etags);
219
- }
220
- async updateUpload(id, update) {
221
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
222
- method: "PUT",
223
- headers: this.apiHeaders,
224
- body: JSON.stringify(update),
225
- });
226
- return await res.json();
227
- }
228
- async listUploads(filter = {}, skip = 0, limit = 100) {
229
- const url = new URL("/uploads", this._apiUrl);
230
- url.searchParams.set("skip", skip.toString());
231
- url.searchParams.set("limit", limit.toString());
232
- if (filter.clientId) {
233
- url.searchParams.set("clientId", filter.clientId);
234
- }
235
- if (filter.mimeTypes) {
236
- url.searchParams.set("mimeTypes", filter.mimeTypes.join());
237
- }
238
- if (filter.tags) {
239
- url.searchParams.set("tags", filter.tags.join());
240
- }
241
- const res = await fetch(url.href, {
242
- headers: this.apiHeaders,
243
- });
244
- return await res.json();
245
- }
246
- async deleteUpload(id) {
247
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
248
- method: "DELETE",
249
- headers: this.apiHeaders,
250
- });
251
- return await res.json();
252
- }
253
- async exposeScriptingContext(context) {
254
- // @ts-ignore
255
- window.KM_SCRIPTING_CONTEXT = context;
256
- // @ts-ignore
257
- window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
258
- }
259
- }
@@ -1,7 +0,0 @@
1
- import type * as Y from "yjs";
2
- import type { DocTypeDescription, MappedTypeDescription } from "@syncedstore/core/types/doc";
3
- export declare class SyncedStore<T extends DocTypeDescription> {
4
- readonly data: MappedTypeDescription<T>;
5
- readonly doc: Y.Doc;
6
- constructor(initialState: T);
7
- }
@@ -1,9 +0,0 @@
1
- import { syncedStore, getYjsDoc } from "@syncedstore/core";
2
- export class SyncedStore {
3
- data;
4
- doc;
5
- constructor(initialState) {
6
- this.data = syncedStore(initialState);
7
- this.doc = getYjsDoc(this.data);
8
- }
9
- }
@@ -1,3 +0,0 @@
1
- export * from "../ws-message-type";
2
- export * from "../ws-message-reader";
3
- export * from "../ws-message-writer";
@@ -1,3 +0,0 @@
1
- export * from "../ws-message-type";
2
- export * from "../ws-message-reader";
3
- export * from "../ws-message-writer";
@@ -1,9 +0,0 @@
1
- import type { WsMessageType } from "./ws-message-type";
2
- export declare class WsMessageReader {
3
- private buffer;
4
- readonly type: WsMessageType;
5
- private _view;
6
- private _offset;
7
- constructor(buffer: ArrayBuffer);
8
- readInt32(): number;
9
- }
@@ -1,17 +0,0 @@
1
- export class WsMessageReader {
2
- buffer;
3
- type;
4
- _view;
5
- _offset = 0;
6
- constructor(buffer) {
7
- this.buffer = buffer;
8
- // Read the first 4 bytes as the message type
9
- this._view = new DataView(buffer);
10
- this.type = this.readInt32();
11
- }
12
- readInt32() {
13
- const value = this._view.getInt32(this._offset, true);
14
- this._offset += 4;
15
- return value;
16
- }
17
- }
@@ -1,4 +0,0 @@
1
- export declare enum WsMessageType {
2
- SubscribeReq = 1,
3
- SubscribeRes = 2
4
- }
@@ -1,12 +0,0 @@
1
- import type { WsMessageType } from "./ws-message-type";
2
- export declare class WsMessageWriter {
3
- type: WsMessageType;
4
- payloadLength: number;
5
- private _buffer;
6
- private _view;
7
- private _offset;
8
- constructor(type: WsMessageType, payloadLength: number);
9
- writeInt32(value: number): void;
10
- writeString(value: string): void;
11
- get buffer(): ArrayBuffer;
12
- }
@@ -1,30 +0,0 @@
1
- export class WsMessageWriter {
2
- type;
3
- payloadLength;
4
- _buffer;
5
- _view;
6
- _offset = 0;
7
- constructor(type, payloadLength) {
8
- this.type = type;
9
- this.payloadLength = payloadLength;
10
- this._buffer = new ArrayBuffer(4 + payloadLength);
11
- this._view = new DataView(this._buffer);
12
- // Write type
13
- this.writeInt32(type);
14
- }
15
- writeInt32(value) {
16
- this._view.setInt32(this._offset, value, true);
17
- this._offset += 4;
18
- }
19
- writeString(value) {
20
- // Write length
21
- this.writeInt32(value.length);
22
- // Write chars
23
- for (let i = 0; i < value.length; i++) {
24
- this._view.setUint8(this._offset++, value.charCodeAt(i));
25
- }
26
- }
27
- get buffer() {
28
- return this._buffer;
29
- }
30
- }
@@ -1,14 +0,0 @@
1
- export declare enum WsMessageType {
2
- Subscribe = 1
3
- }
4
- export declare class WsMessage {
5
- type: WsMessageType;
6
- payloadLength: number;
7
- private _buffer;
8
- private _view;
9
- private _offset;
10
- constructor(type: WsMessageType, payloadLength: number);
11
- writeInt32(value: number): void;
12
- writeString(value: string): void;
13
- get buffer(): ArrayBuffer;
14
- }
@@ -1,34 +0,0 @@
1
- export var WsMessageType;
2
- (function (WsMessageType) {
3
- WsMessageType[WsMessageType["Subscribe"] = 1] = "Subscribe";
4
- })(WsMessageType || (WsMessageType = {}));
5
- export class WsMessage {
6
- type;
7
- payloadLength;
8
- _buffer;
9
- _view;
10
- _offset = 0;
11
- constructor(type, payloadLength) {
12
- this.type = type;
13
- this.payloadLength = payloadLength;
14
- this._buffer = new ArrayBuffer(4 + payloadLength);
15
- this._view = new DataView(this._buffer);
16
- // Write type
17
- this.writeInt32(type);
18
- }
19
- writeInt32(value) {
20
- this._view.setInt32(this._offset, value, true);
21
- this._offset += 4;
22
- }
23
- writeString(value) {
24
- // Write length
25
- this.writeInt32(value.length);
26
- // Write chars
27
- for (let i = 0; i < value.length; i++) {
28
- this._view.setUint8(this._offset++, value.charCodeAt(i));
29
- }
30
- }
31
- get buffer() {
32
- return this._buffer;
33
- }
34
- }