@kokimoki/app 0.6.8 → 1.0.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.
Files changed (48) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.js +5 -1
  3. package/dist/kokimoki-client copy.d.ts +57 -0
  4. package/dist/kokimoki-client copy.js +259 -0
  5. package/dist/kokimoki-client-refactored.d.ts +67 -0
  6. package/dist/kokimoki-client-refactored.js +422 -0
  7. package/dist/kokimoki-client.d.ts +26 -14
  8. package/dist/kokimoki-client.js +292 -103
  9. package/dist/kokimoki-queue.d.ts +34 -0
  10. package/dist/kokimoki-queue.js +30 -0
  11. package/dist/kokimoki-schema.d.ts +52 -0
  12. package/dist/kokimoki-schema.js +92 -0
  13. package/dist/kokimoki-store.d.ts +13 -0
  14. package/dist/kokimoki-store.js +48 -0
  15. package/dist/kokimoki-transaction.d.ts +25 -0
  16. package/dist/kokimoki-transaction.js +99 -0
  17. package/dist/message-queue.d.ts +8 -0
  18. package/dist/message-queue.js +19 -0
  19. package/dist/room-subscription-mode.d.ts +5 -0
  20. package/dist/room-subscription-mode.js +6 -0
  21. package/dist/room-subscription.d.ts +15 -0
  22. package/dist/room-subscription.js +49 -0
  23. package/dist/synced-schema.d.ts +52 -0
  24. package/dist/synced-schema.js +92 -0
  25. package/dist/synced-store copy.d.ts +7 -0
  26. package/dist/synced-store copy.js +9 -0
  27. package/dist/synced-types.d.ts +45 -0
  28. package/dist/synced-types.js +72 -0
  29. package/dist/types/events.d.ts +1 -2
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/dist/ws-message/index.d.ts +3 -0
  33. package/dist/ws-message/index.js +3 -0
  34. package/dist/ws-message/ws-message-reader.d.ts +9 -0
  35. package/dist/ws-message/ws-message-reader.js +17 -0
  36. package/dist/ws-message/ws-message-type.d.ts +4 -0
  37. package/dist/ws-message/ws-message-type.js +5 -0
  38. package/dist/ws-message/ws-message-writer.d.ts +12 -0
  39. package/dist/ws-message/ws-message-writer.js +30 -0
  40. package/dist/ws-message-reader.d.ts +11 -0
  41. package/dist/ws-message-reader.js +36 -0
  42. package/dist/ws-message-type.d.ts +9 -0
  43. package/dist/ws-message-type.js +10 -0
  44. package/dist/ws-message-writer.d.ts +9 -0
  45. package/dist/ws-message-writer.js +45 -0
  46. package/dist/ws-message.d.ts +14 -0
  47. package/dist/ws-message.js +34 -0
  48. package/package.json +4 -4
@@ -1,6 +1,11 @@
1
- import { HocuspocusProvider } from "@hocuspocus/provider";
2
1
  import EventEmitter from "events";
3
2
  import { KOKIMOKI_APP_VERSION } from "./version";
3
+ import * as Y from "yjs";
4
+ import { KokimokiTransaction } from "./kokimoki-transaction";
5
+ import { WsMessageType } from "./ws-message-type";
6
+ import { WsMessageWriter } from "./ws-message-writer";
7
+ import { WsMessageReader } from "./ws-message-reader";
8
+ import { RoomSubscription } from "./room-subscription";
4
9
  export class KokimokiClient extends EventEmitter {
5
10
  host;
6
11
  appId;
@@ -10,11 +15,18 @@ export class KokimokiClient extends EventEmitter {
10
15
  _id;
11
16
  _token;
12
17
  _apiHeaders;
13
- _providers = new Map();
14
18
  _serverTimeOffset = 0;
15
19
  _clientContext;
20
+ _ws;
21
+ _subscriptionsByName = new Map();
22
+ _subscriptionsByHash = new Map();
23
+ _subscribeReqPromises = new Map();
24
+ _transactionPromises = new Map();
16
25
  _connected = false;
17
- _lastPongAt = 0;
26
+ _connectPromise;
27
+ _messageId = 0;
28
+ _reconnectTimeout = 0;
29
+ _pingInterval;
18
30
  constructor(host, appId, code = "") {
19
31
  super();
20
32
  this.host = host;
@@ -24,6 +36,15 @@ export class KokimokiClient extends EventEmitter {
24
36
  const secure = this.host.indexOf(":") === -1;
25
37
  this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
26
38
  this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
39
+ // Set up ping interval
40
+ const pingMsg = new WsMessageWriter();
41
+ pingMsg.writeInt32(WsMessageType.Ping);
42
+ const pingBuffer = pingMsg.getBuffer();
43
+ this._pingInterval = setInterval(() => {
44
+ if (this.connected) {
45
+ this.ws.send(pingBuffer);
46
+ }
47
+ }, 5000);
27
48
  }
28
49
  get id() {
29
50
  if (!this._id) {
@@ -55,122 +76,192 @@ export class KokimokiClient extends EventEmitter {
55
76
  }
56
77
  return this._clientContext;
57
78
  }
79
+ get connected() {
80
+ return this._connected;
81
+ }
82
+ get ws() {
83
+ if (!this._ws) {
84
+ throw new Error("Not connected");
85
+ }
86
+ return this._ws;
87
+ }
58
88
  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
- }),
89
+ if (this._connectPromise) {
90
+ return await this._connectPromise;
91
+ }
92
+ this._ws = new WebSocket(`${this._wsUrl}/apps/${this.appId}?clientVersion=${KOKIMOKI_APP_VERSION}`);
93
+ this._ws.binaryType = "arraybuffer";
94
+ // Close previous connection in hot-reload scenarios
95
+ if (window) {
96
+ if (!window.__KOKIMOKI_WS__) {
97
+ window.__KOKIMOKI_WS__ = {};
98
+ }
99
+ if (this.appId in window.__KOKIMOKI_WS__) {
100
+ window.__KOKIMOKI_WS__[this.appId].close();
101
+ }
102
+ window.__KOKIMOKI_WS__[this.appId] = this._ws;
103
+ }
104
+ // Wait for connection
105
+ this._connectPromise = new Promise((onInit) => {
106
+ // Fetch the auth token
107
+ let clientToken = localStorage.getItem("KM_TOKEN");
108
+ // Send the app token on first connect
109
+ this.ws.onopen = () => {
110
+ this.ws.send(JSON.stringify({ type: "auth", code: this.code, token: clientToken }));
111
+ };
112
+ this.ws.onclose = () => {
113
+ this._connected = false;
114
+ this._connectPromise = undefined;
115
+ this._ws.onmessage = null;
116
+ this._ws = undefined;
117
+ if (window && window.__KOKIMOKI_WS__) {
118
+ delete window.__KOKIMOKI_WS__[this.appId];
119
+ }
120
+ // Clean up
121
+ this._subscribeReqPromises.clear();
122
+ this._transactionPromises.clear();
123
+ // Attempt to reconnect
124
+ console.log(`[Kokimoki] Connection lost, attempting to reconnect in ${this._reconnectTimeout} seconds...`);
125
+ setTimeout(async () => await this.connect(), this._reconnectTimeout * 1000);
126
+ this._reconnectTimeout = Math.min(3, this._reconnectTimeout + 1);
127
+ // Emit disconnected event
128
+ this.emit("disconnected");
129
+ };
130
+ this.ws.onmessage = (e) => {
131
+ console.log(`Received WS message: ${e.data}`);
132
+ // Handle JSON messages
133
+ if (typeof e.data === "string") {
134
+ const message = JSON.parse(e.data);
135
+ switch (message.type) {
136
+ case "init":
137
+ this.handleInitMessage(message);
138
+ onInit();
139
+ break;
140
+ }
141
+ return;
142
+ }
143
+ // Handle binary messages
144
+ this.handleBinaryMessage(e.data);
145
+ };
68
146
  });
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);
147
+ await this._connectPromise;
148
+ this._connected = true;
149
+ // Connection established
150
+ console.log(`[Kokimoki] Client id: ${this.id}`);
151
+ console.log(`[Kokimoki] Client context:`, this.clientContext);
152
+ // Restore subscriptions if reconnected
153
+ const roomNames = Array.from(this._subscriptionsByName.keys()).map((name) => `"${name}"`);
154
+ if (roomNames.length) {
155
+ console.log(`[Kokimoki] Restoring subscriptions: ${roomNames}`);
156
+ }
157
+ for (const subscription of this._subscriptionsByName.values()) {
158
+ try {
159
+ await this.join(subscription.store);
160
+ }
161
+ catch (err) {
162
+ console.error(`[Kokimoki] Failed to restore subscription for "${subscription.roomName}":`, err);
163
+ }
164
+ }
165
+ // Emit connected event
166
+ this._reconnectTimeout = 0;
167
+ this.emit("connected");
168
+ }
169
+ handleInitMessage(message) {
170
+ localStorage.setItem("KM_TOKEN", message.clientToken);
171
+ this._id = message.clientId;
172
+ this._token = message.appToken;
173
+ this._clientContext = message.clientContext;
77
174
  // Set up the auth headers
78
175
  this._apiHeaders = new Headers({
79
176
  Authorization: `Bearer ${this.token}`,
80
177
  "Content-Type": "application/json",
81
178
  });
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
179
  }
93
- serverTimestamp() {
94
- return Date.now() - this._serverTimeOffset;
180
+ handleBinaryMessage(data) {
181
+ const reader = new WsMessageReader(data);
182
+ const type = reader.readInt32();
183
+ switch (type) {
184
+ case WsMessageType.SubscribeRes:
185
+ this.handleSubscribeResMessage(reader);
186
+ break;
187
+ case WsMessageType.RoomUpdate:
188
+ this.handleRoomUpdateMessage(reader);
189
+ break;
190
+ case WsMessageType.Error:
191
+ this.handleErrorMessage(reader);
192
+ break;
193
+ case WsMessageType.Pong: {
194
+ const s = reader.readInt32();
195
+ const ms = reader.readInt32();
196
+ this._serverTimeOffset = Date.now() - s * 1000 - ms;
197
+ break;
198
+ }
199
+ }
95
200
  }
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
- });
201
+ handleErrorMessage(msg) {
202
+ const reqId = msg.readInt32();
203
+ const error = msg.readString();
204
+ const subscribeReqPromise = this._subscribeReqPromises.get(reqId);
205
+ const transactionPromise = this._transactionPromises.get(reqId);
206
+ if (subscribeReqPromise) {
207
+ this._subscribeReqPromises.delete(reqId);
208
+ subscribeReqPromise.reject(error);
209
+ }
210
+ else if (transactionPromise) {
211
+ this._transactionPromises.delete(reqId);
212
+ transactionPromise.reject(error);
213
+ }
214
+ else {
215
+ console.warn(`Received error for unknown request ${reqId}: ${error}`);
114
216
  }
115
217
  }
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;
218
+ handleSubscribeResMessage(msg) {
219
+ const reqId = msg.readInt32();
220
+ const roomHash = msg.readUint32();
221
+ const promise = this._subscribeReqPromises.get(reqId);
222
+ if (promise) {
223
+ this._subscribeReqPromises.delete(reqId);
224
+ // In Write mode, no initial state is sent
225
+ if (!msg.end) {
226
+ promise.resolve(roomHash, msg.readUint8Array());
227
+ }
228
+ else {
229
+ promise.resolve(roomHash);
132
230
  }
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
231
  }
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
232
  }
161
- sendStatelessToClient(room, clientId, data) {
162
- const provider = this._providers.get(room);
163
- if (!provider) {
164
- throw new Error(`No provider for room ${room}`);
233
+ handleRoomUpdateMessage(msg) {
234
+ const appliedId = msg.readInt32();
235
+ const roomHash = msg.readUint32();
236
+ // Apply update if not in Write mode
237
+ if (!msg.end) {
238
+ const subscription = this._subscriptionsByHash.get(roomHash);
239
+ if (subscription) {
240
+ Y.applyUpdate(subscription.store.doc, msg.readUint8Array(), this);
241
+ }
242
+ else {
243
+ console.warn(`Received update for unknown room ${roomHash}`);
244
+ }
165
245
  }
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}`);
246
+ // Check transaction resolves
247
+ for (const [transactionId, { resolve },] of this._transactionPromises.entries()) {
248
+ if (appliedId >= transactionId) {
249
+ this._transactionPromises.delete(transactionId);
250
+ resolve();
251
+ }
172
252
  }
173
- provider.sendStateless(JSON.stringify({ data, self }));
253
+ }
254
+ serverTimestamp() {
255
+ return Date.now() - this._serverTimeOffset;
256
+ }
257
+ // Send Y update to room
258
+ async patchRoomState(room, update) {
259
+ const res = await fetch(`${this._apiUrl}/rooms/${room}`, {
260
+ method: "PATCH",
261
+ headers: this.apiHeaders,
262
+ body: update,
263
+ });
264
+ return await res.json();
174
265
  }
175
266
  // Storage
176
267
  async createUpload(name, blob, tags) {
@@ -241,4 +332,102 @@ export class KokimokiClient extends EventEmitter {
241
332
  });
242
333
  return await res.json();
243
334
  }
335
+ async exposeScriptingContext(context) {
336
+ // @ts-ignore
337
+ window.KM_SCRIPTING_CONTEXT = context;
338
+ // @ts-ignore
339
+ window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
340
+ }
341
+ async sendSubscriptionReq(roomName, mode) {
342
+ // Set up sync resolver
343
+ const reqId = ++this._messageId;
344
+ return await new Promise((resolve, reject) => {
345
+ this._subscribeReqPromises.set(reqId, {
346
+ resolve: (roomHash, initialUpdate) => {
347
+ resolve({ roomHash, initialUpdate });
348
+ },
349
+ reject,
350
+ });
351
+ // Send subscription request
352
+ const msg = new WsMessageWriter();
353
+ msg.writeInt32(WsMessageType.SubscribeReq);
354
+ msg.writeInt32(reqId);
355
+ msg.writeString(roomName);
356
+ msg.writeChar(mode);
357
+ this.ws.send(msg.getBuffer());
358
+ });
359
+ }
360
+ async join(store) {
361
+ let subscription = this._subscriptionsByName.get(store.roomName);
362
+ if (!subscription) {
363
+ subscription = new RoomSubscription(this, store);
364
+ this._subscriptionsByName.set(store.roomName, subscription);
365
+ }
366
+ // Send subscription request if connected to server
367
+ if (!subscription.joined) {
368
+ const res = await this.sendSubscriptionReq(store.roomName, store.mode);
369
+ this._subscriptionsByHash.set(res.roomHash, subscription);
370
+ await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
371
+ }
372
+ }
373
+ async transact(handler) {
374
+ if (!this._connected) {
375
+ throw new Error("Client not connected");
376
+ }
377
+ const transaction = new KokimokiTransaction(this);
378
+ handler(transaction);
379
+ const { updates, consumedMessages } = await transaction.getUpdates();
380
+ if (!updates.length) {
381
+ return;
382
+ }
383
+ // Construct buffer
384
+ const writer = new WsMessageWriter();
385
+ // Write message type
386
+ writer.writeInt32(WsMessageType.Transaction);
387
+ // Update and write transaction ID
388
+ const transactionId = ++this._messageId;
389
+ writer.writeInt32(transactionId);
390
+ // Write room hashes where messages were consumed
391
+ writer.writeInt32(consumedMessages.size);
392
+ for (const roomName of consumedMessages) {
393
+ const subscription = this._subscriptionsByName.get(roomName);
394
+ if (!subscription) {
395
+ throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
396
+ }
397
+ writer.writeUint32(subscription.roomHash);
398
+ }
399
+ // Write updates
400
+ for (const { roomName, update } of updates) {
401
+ const subscription = this._subscriptionsByName.get(roomName);
402
+ if (!subscription) {
403
+ throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
404
+ }
405
+ writer.writeUint32(subscription.roomHash);
406
+ writer.writeUint8Array(update);
407
+ }
408
+ const buffer = writer.getBuffer();
409
+ // Wait for server to apply transaction
410
+ await new Promise((resolve, reject) => {
411
+ this._transactionPromises.set(transactionId, { resolve, reject });
412
+ // Send update to server
413
+ try {
414
+ this.ws.send(buffer);
415
+ }
416
+ catch (e) {
417
+ // Not connected
418
+ console.log("Failed to send update to server:", e);
419
+ // TODO: merge updates or something
420
+ throw e;
421
+ }
422
+ });
423
+ /* // Reset doc in Write-only mode
424
+ for (const { roomName, update } of updates) {
425
+ const mode = this._subscriptions.get(roomName);
426
+
427
+ if (mode === RoomSubscriptionMode.Write) {
428
+ // @ts-ignore
429
+ // this._stores.get(this._roomHashes.get(roomName)!)!.doc = new Y.Doc();
430
+ }
431
+ } */
432
+ }
244
433
  }
@@ -0,0 +1,34 @@
1
+ import { KokimokiStore } from "./kokimoki-store";
2
+ import { KokimokiSchema as S } from "./kokimoki-schema";
3
+ import type TypedEventEmitter from "typed-emitter";
4
+ import type { RoomSubscriptionMode } from "./room-subscription-mode";
5
+ export declare class KokimokiQueue<Req extends S.Generic<unknown>> extends KokimokiStore<S.Dict<S.Struct<{
6
+ timestamp: S.Number;
7
+ payload: Req;
8
+ }>>> {
9
+ readonly payloadSchema: Req;
10
+ private _emitter;
11
+ readonly on: <E extends "messages">(event: E, listener: {
12
+ messages: (messages: {
13
+ id: string;
14
+ payload: Req["defaultValue"];
15
+ }[]) => void;
16
+ }[E]) => TypedEventEmitter<{
17
+ messages: (messages: {
18
+ id: string;
19
+ payload: Req["defaultValue"];
20
+ }[]) => void;
21
+ }>;
22
+ readonly off: <E extends "messages">(event: E, listener: {
23
+ messages: (messages: {
24
+ id: string;
25
+ payload: Req["defaultValue"];
26
+ }[]) => void;
27
+ }[E]) => TypedEventEmitter<{
28
+ messages: (messages: {
29
+ id: string;
30
+ payload: Req["defaultValue"];
31
+ }[]) => void;
32
+ }>;
33
+ constructor(roomName: string, payloadSchema: Req, mode: RoomSubscriptionMode);
34
+ }
@@ -0,0 +1,30 @@
1
+ import EventEmitter from "events";
2
+ import { KokimokiStore } from "./kokimoki-store";
3
+ import { KokimokiSchema as S } from "./kokimoki-schema";
4
+ export class KokimokiQueue extends KokimokiStore {
5
+ payloadSchema;
6
+ _emitter = new EventEmitter();
7
+ on = this._emitter.on.bind(this._emitter);
8
+ off = this._emitter.off.bind(this._emitter);
9
+ constructor(roomName, payloadSchema, mode) {
10
+ super(`/q/${roomName}`, S.dict(S.struct({
11
+ timestamp: S.number(),
12
+ payload: payloadSchema,
13
+ })), mode);
14
+ this.payloadSchema = payloadSchema;
15
+ const emittedMessageIds = new Set();
16
+ this.doc.on("update", () => {
17
+ const newMessages = [];
18
+ for (const id in this.proxy) {
19
+ if (emittedMessageIds.has(id)) {
20
+ continue;
21
+ }
22
+ emittedMessageIds.add(id);
23
+ newMessages.push({ id, payload: this.proxy[id].payload });
24
+ }
25
+ if (newMessages.length > 0) {
26
+ this._emitter.emit("messages", newMessages);
27
+ }
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,52 @@
1
+ export declare namespace KokimokiSchema {
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<{
22
+ [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?: {
44
+ [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>;
52
+ }
@@ -0,0 +1,92 @@
1
+ export var KokimokiSchema;
2
+ (function (KokimokiSchema) {
3
+ class Generic {
4
+ }
5
+ KokimokiSchema.Generic = Generic;
6
+ class Number extends Generic {
7
+ defaultValue;
8
+ constructor(defaultValue = 0) {
9
+ super();
10
+ this.defaultValue = defaultValue;
11
+ }
12
+ }
13
+ KokimokiSchema.Number = Number;
14
+ function number(defaultValue = 0) {
15
+ return new Number(defaultValue);
16
+ }
17
+ KokimokiSchema.number = number;
18
+ class String extends Generic {
19
+ defaultValue;
20
+ constructor(defaultValue = "") {
21
+ super();
22
+ this.defaultValue = defaultValue;
23
+ }
24
+ }
25
+ KokimokiSchema.String = String;
26
+ function string(defaultValue = "") {
27
+ return new String(defaultValue);
28
+ }
29
+ KokimokiSchema.string = string;
30
+ class Boolean extends Generic {
31
+ defaultValue;
32
+ constructor(defaultValue = false) {
33
+ super();
34
+ this.defaultValue = defaultValue;
35
+ }
36
+ }
37
+ KokimokiSchema.Boolean = Boolean;
38
+ function boolean(defaultValue = false) {
39
+ return new Boolean(defaultValue);
40
+ }
41
+ KokimokiSchema.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
+ }
59
+ }
60
+ KokimokiSchema.Struct = Struct;
61
+ function struct(schema) {
62
+ return new Struct(schema);
63
+ }
64
+ KokimokiSchema.struct = struct;
65
+ class Dict {
66
+ schema;
67
+ defaultValue;
68
+ constructor(schema, defaultValue = {}) {
69
+ this.schema = schema;
70
+ this.defaultValue = defaultValue;
71
+ }
72
+ }
73
+ KokimokiSchema.Dict = Dict;
74
+ function dict(schema, defaultValue = {}) {
75
+ return new Dict(schema, defaultValue);
76
+ }
77
+ KokimokiSchema.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
+ }
86
+ }
87
+ KokimokiSchema.List = List;
88
+ function list(schema, defaultValue = []) {
89
+ return new List(schema, defaultValue);
90
+ }
91
+ KokimokiSchema.list = list;
92
+ })(KokimokiSchema || (KokimokiSchema = {}));
@@ -0,0 +1,13 @@
1
+ import * as Y from "yjs";
2
+ import type { KokimokiSchema as S } from "./kokimoki-schema";
3
+ import { RoomSubscriptionMode } from "./room-subscription-mode";
4
+ export declare class KokimokiStore<T extends S.Generic<unknown>> {
5
+ readonly roomName: string;
6
+ readonly mode: RoomSubscriptionMode;
7
+ readonly doc: Y.Doc;
8
+ readonly proxy: T["defaultValue"];
9
+ readonly root: T["defaultValue"];
10
+ readonly defaultValue: T["defaultValue"];
11
+ readonly docRoot: Y.Map<unknown>;
12
+ constructor(roomName: string, schema: T, mode?: RoomSubscriptionMode);
13
+ }