@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.
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/kokimoki-client copy.d.ts +57 -0
- package/dist/kokimoki-client copy.js +259 -0
- package/dist/kokimoki-client-refactored.d.ts +67 -0
- package/dist/kokimoki-client-refactored.js +422 -0
- package/dist/kokimoki-client.d.ts +26 -14
- package/dist/kokimoki-client.js +292 -103
- package/dist/kokimoki-queue.d.ts +34 -0
- package/dist/kokimoki-queue.js +30 -0
- package/dist/kokimoki-schema.d.ts +52 -0
- package/dist/kokimoki-schema.js +92 -0
- package/dist/kokimoki-store.d.ts +13 -0
- package/dist/kokimoki-store.js +48 -0
- package/dist/kokimoki-transaction.d.ts +25 -0
- package/dist/kokimoki-transaction.js +99 -0
- package/dist/message-queue.d.ts +8 -0
- package/dist/message-queue.js +19 -0
- package/dist/room-subscription-mode.d.ts +5 -0
- package/dist/room-subscription-mode.js +6 -0
- package/dist/room-subscription.d.ts +15 -0
- package/dist/room-subscription.js +49 -0
- package/dist/synced-schema.d.ts +52 -0
- package/dist/synced-schema.js +92 -0
- package/dist/synced-store copy.d.ts +7 -0
- package/dist/synced-store copy.js +9 -0
- package/dist/synced-types.d.ts +45 -0
- package/dist/synced-types.js +72 -0
- package/dist/types/events.d.ts +1 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws-message/index.d.ts +3 -0
- package/dist/ws-message/index.js +3 -0
- package/dist/ws-message/ws-message-reader.d.ts +9 -0
- package/dist/ws-message/ws-message-reader.js +17 -0
- package/dist/ws-message/ws-message-type.d.ts +4 -0
- package/dist/ws-message/ws-message-type.js +5 -0
- package/dist/ws-message/ws-message-writer.d.ts +12 -0
- package/dist/ws-message/ws-message-writer.js +30 -0
- package/dist/ws-message-reader.d.ts +11 -0
- package/dist/ws-message-reader.js +36 -0
- package/dist/ws-message-type.d.ts +9 -0
- package/dist/ws-message-type.js +10 -0
- package/dist/ws-message-writer.d.ts +9 -0
- package/dist/ws-message-writer.js +45 -0
- package/dist/ws-message.d.ts +14 -0
- package/dist/ws-message.js +34 -0
- package/package.json +4 -4
package/dist/kokimoki-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
+
}
|