@kokimoki/app 1.3.2 → 1.4.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.
@@ -634,7 +634,7 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
634
634
  var eventsExports = events.exports;
635
635
  var EventEmitter$1 = /*@__PURE__*/getDefaultExportFromCjs(eventsExports);
636
636
 
637
- const KOKIMOKI_APP_VERSION = "1.3.2";
637
+ const KOKIMOKI_APP_VERSION = "1.4.0";
638
638
 
639
639
  /**
640
640
  * Utility module to work with key-value stores.
@@ -11401,6 +11401,7 @@ class KokimokiTransaction {
11401
11401
  _clones = new Map();
11402
11402
  _updates = new Map();
11403
11403
  _consumeMessagesInRooms = new Set();
11404
+ _queueMessageCounter = 0;
11404
11405
  constructor(kmClient) {
11405
11406
  this.kmClient = kmClient;
11406
11407
  }
@@ -11468,11 +11469,10 @@ class KokimokiTransaction {
11468
11469
  obj[key].push(value);
11469
11470
  }
11470
11471
  queueMessage(queue, payload) {
11471
- const messageId = Math.random().toString(36);
11472
- this.set(queue.root[messageId], {
11473
- timestamp: this.kmClient.serverTimestamp(),
11474
- payload,
11475
- });
11472
+ const messageId = `${this.kmClient.serverTimestamp()}/${this
11473
+ ._queueMessageCounter++}/${Math.random().toString(36).slice(2)}`;
11474
+ this.set(queue.root[messageId], payload);
11475
+ return messageId;
11476
11476
  }
11477
11477
  consumeMessage(queue, messageId) {
11478
11478
  if (!queue.proxy[messageId]) {
@@ -11499,6 +11499,175 @@ class KokimokiTransaction {
11499
11499
  }
11500
11500
  }
11501
11501
 
11502
+ function parsePath(text) {
11503
+ return text.split('.')
11504
+ }
11505
+
11506
+ function push(arr, el) {
11507
+ const newArr = arr.slice();
11508
+ newArr.push(el);
11509
+ return newArr;
11510
+ }
11511
+
11512
+ // names of the traps that can be registered with ES6's Proxy object
11513
+ const trapNames = [
11514
+ 'apply',
11515
+ 'construct',
11516
+ 'defineProperty',
11517
+ 'deleteProperty',
11518
+ 'enumerate', // deprecated
11519
+ 'get',
11520
+ 'getOwnPropertyDescriptor',
11521
+ 'getPrototypeOf',
11522
+ 'has',
11523
+ 'isExtensible',
11524
+ 'ownKeys',
11525
+ 'preventExtensions',
11526
+ 'set',
11527
+ 'setPrototypeOf',
11528
+ ];
11529
+
11530
+ // a list of paramer indexes that indicate that the a recieves a key at that parameter
11531
+ // this information will be used to update the path accordingly
11532
+ const keys = {
11533
+ get: 1,
11534
+ set: 1,
11535
+ deleteProperty: 1,
11536
+ has: 1,
11537
+ defineProperty: 1,
11538
+ getOwnPropertyDescriptor: 1,
11539
+ };
11540
+
11541
+ function DeepProxy(rootTarget, traps, options) {
11542
+
11543
+ let path = [];
11544
+ let userData = {};
11545
+
11546
+ if (options !== undefined && typeof options.path !== 'undefined') {
11547
+ path = parsePath(options.path);
11548
+ }
11549
+ if (options !== undefined && typeof options.userData !== 'undefined') {
11550
+ userData = options.userData;
11551
+ }
11552
+
11553
+ function createProxy(target, path) {
11554
+
11555
+ // avoid creating a new object between two traps
11556
+ const context = { rootTarget, path };
11557
+ Object.assign(context, userData);
11558
+
11559
+ const realTraps = {};
11560
+
11561
+ for (const trapName of trapNames) {
11562
+ const keyParamIdx = keys[trapName]
11563
+ , trap = traps[trapName];
11564
+
11565
+ if (typeof trap !== 'undefined') {
11566
+
11567
+ if (typeof keyParamIdx !== 'undefined') {
11568
+
11569
+ realTraps[trapName] = function () {
11570
+
11571
+ const key = arguments[keyParamIdx];
11572
+
11573
+ // update context for this trap
11574
+ context.nest = function (nestedTarget) {
11575
+ if (nestedTarget === undefined)
11576
+ nestedTarget = rootTarget;
11577
+ return createProxy(nestedTarget, push(path, key));
11578
+ };
11579
+
11580
+ return trap.apply(context, arguments);
11581
+ };
11582
+ } else {
11583
+
11584
+ realTraps[trapName] = function () {
11585
+
11586
+ // update context for this trap
11587
+ context.nest = function (nestedTarget) {
11588
+ if (nestedTarget === undefined)
11589
+ nestedTarget = {};
11590
+ return createProxy(nestedTarget, path);
11591
+ };
11592
+
11593
+ return trap.apply(context, arguments);
11594
+ };
11595
+ }
11596
+ }
11597
+ }
11598
+
11599
+ return new Proxy(target, realTraps);
11600
+ }
11601
+
11602
+ return createProxy(rootTarget, path);
11603
+
11604
+ }
11605
+
11606
+ var RoomSubscriptionMode;
11607
+ (function (RoomSubscriptionMode) {
11608
+ RoomSubscriptionMode["Read"] = "r";
11609
+ RoomSubscriptionMode["Write"] = "w";
11610
+ RoomSubscriptionMode["ReadWrite"] = "b";
11611
+ })(RoomSubscriptionMode || (RoomSubscriptionMode = {}));
11612
+
11613
+ class KokimokiStore {
11614
+ roomName;
11615
+ mode;
11616
+ doc;
11617
+ proxy;
11618
+ root;
11619
+ defaultValue;
11620
+ docRoot;
11621
+ constructor(roomName, schema, mode = RoomSubscriptionMode.ReadWrite) {
11622
+ this.roomName = roomName;
11623
+ this.mode = mode;
11624
+ // Construct Y doc
11625
+ this.doc = new Doc();
11626
+ this.docRoot = this.doc.getMap("root");
11627
+ // Construct proxy object
11628
+ this.proxy = proxy();
11629
+ // @ts-ignore
11630
+ f(this.proxy, this.docRoot);
11631
+ // Create root proxy
11632
+ const store = this;
11633
+ // @ts-ignore
11634
+ this.root = new DeepProxy(this.proxy, {
11635
+ get() {
11636
+ return this.nest(function () { });
11637
+ },
11638
+ apply() {
11639
+ let obj = store.proxy;
11640
+ for (let i = 0; i < this.path.length - 1; i++) {
11641
+ obj = obj[this.path[i]];
11642
+ }
11643
+ return {
11644
+ roomName: store.roomName,
11645
+ doc: store.doc,
11646
+ path: this.path,
11647
+ obj,
11648
+ key: this.path[this.path.length - 1],
11649
+ };
11650
+ },
11651
+ });
11652
+ // Set default value
11653
+ this.defaultValue = schema.defaultValue;
11654
+ }
11655
+ get() {
11656
+ return this.proxy;
11657
+ }
11658
+ subscribe(set) {
11659
+ // @ts-ignore
11660
+ const handler = () => set(this.proxy);
11661
+ this.doc.on("update", handler);
11662
+ // @ts-ignore
11663
+ set(this.proxy);
11664
+ return () => this.doc.off("update", handler);
11665
+ }
11666
+ async onJoin(client) { }
11667
+ async onBeforeLeave(client) { }
11668
+ async onLeave(client) { }
11669
+ }
11670
+
11502
11671
  var WsMessageType;
11503
11672
  (function (WsMessageType) {
11504
11673
  WsMessageType[WsMessageType["SubscribeReq"] = 1] = "SubscribeReq";
@@ -11595,13 +11764,6 @@ class WsMessageReader {
11595
11764
  }
11596
11765
  }
11597
11766
 
11598
- var RoomSubscriptionMode;
11599
- (function (RoomSubscriptionMode) {
11600
- RoomSubscriptionMode["Read"] = "r";
11601
- RoomSubscriptionMode["Write"] = "w";
11602
- RoomSubscriptionMode["ReadWrite"] = "b";
11603
- })(RoomSubscriptionMode || (RoomSubscriptionMode = {}));
11604
-
11605
11767
  class RoomSubscription {
11606
11768
  kmClient;
11607
11769
  store;
@@ -11650,925 +11812,937 @@ class RoomSubscription {
11650
11812
  }
11651
11813
  }
11652
11814
 
11653
- class KokimokiClient extends EventEmitter$1 {
11654
- host;
11655
- appId;
11656
- code;
11657
- _wsUrl;
11658
- _apiUrl;
11659
- _id;
11660
- _connectionId;
11661
- _token;
11662
- _apiHeaders;
11663
- _serverTimeOffset = 0;
11664
- _clientContext;
11665
- _ws;
11666
- _subscriptionsByName = new Map();
11667
- _subscriptionsByHash = new Map();
11668
- _subscribeReqPromises = new Map();
11669
- _unsubscribeReqPromises = new Map();
11670
- _transactionPromises = new Map();
11671
- _connected = false;
11672
- _connectPromise;
11673
- _messageId = 0;
11674
- _autoReconnect = true;
11675
- _reconnectTimeout = 0;
11676
- _pingInterval;
11677
- constructor(host, appId, code = "") {
11678
- super();
11679
- this.host = host;
11680
- this.appId = appId;
11681
- this.code = code;
11682
- // Set up the URLs
11683
- const secure = this.host.indexOf(":") === -1;
11684
- this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
11685
- this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
11686
- // Set up ping interval
11687
- const pingMsg = new WsMessageWriter();
11688
- pingMsg.writeInt32(WsMessageType.Ping);
11689
- const pingBuffer = pingMsg.getBuffer();
11690
- this._pingInterval = setInterval(() => {
11691
- if (this.connected) {
11692
- this.ws.send(pingBuffer);
11693
- }
11694
- }, 5000);
11695
- }
11696
- get id() {
11697
- if (!this._id) {
11698
- throw new Error("Client not connected");
11699
- }
11700
- return this._id;
11815
+ var KokimokiSchema;
11816
+ (function (KokimokiSchema) {
11817
+ class Generic {
11701
11818
  }
11702
- get connectionId() {
11703
- if (!this._connectionId) {
11704
- throw new Error("Client not connected");
11819
+ KokimokiSchema.Generic = Generic;
11820
+ class Number extends Generic {
11821
+ defaultValue;
11822
+ constructor(defaultValue = 0) {
11823
+ super();
11824
+ this.defaultValue = defaultValue;
11705
11825
  }
11706
- return this._connectionId;
11707
11826
  }
11708
- get token() {
11709
- if (!this._token) {
11710
- throw new Error("Client not connected");
11711
- }
11712
- return this._token;
11827
+ KokimokiSchema.Number = Number;
11828
+ function number(defaultValue = 0) {
11829
+ return new Number(defaultValue);
11713
11830
  }
11714
- get apiUrl() {
11715
- if (!this._apiUrl) {
11716
- throw new Error("Client not connected");
11831
+ KokimokiSchema.number = number;
11832
+ class String extends Generic {
11833
+ defaultValue;
11834
+ constructor(defaultValue = "") {
11835
+ super();
11836
+ this.defaultValue = defaultValue;
11717
11837
  }
11718
- return this._apiUrl;
11719
11838
  }
11720
- get apiHeaders() {
11721
- if (!this._apiHeaders) {
11722
- throw new Error("Client not connected");
11723
- }
11724
- return this._apiHeaders;
11839
+ KokimokiSchema.String = String;
11840
+ function string(defaultValue = "") {
11841
+ return new String(defaultValue);
11725
11842
  }
11726
- get clientContext() {
11727
- if (this._clientContext === undefined) {
11728
- throw new Error("Client not connected");
11843
+ KokimokiSchema.string = string;
11844
+ class Boolean extends Generic {
11845
+ defaultValue;
11846
+ constructor(defaultValue = false) {
11847
+ super();
11848
+ this.defaultValue = defaultValue;
11729
11849
  }
11730
- return this._clientContext;
11731
- }
11732
- get connected() {
11733
- return this._connected;
11734
11850
  }
11735
- get ws() {
11736
- if (!this._ws) {
11737
- throw new Error("Not connected");
11738
- }
11739
- return this._ws;
11851
+ KokimokiSchema.Boolean = Boolean;
11852
+ function boolean(defaultValue = false) {
11853
+ return new Boolean(defaultValue);
11740
11854
  }
11741
- async connect() {
11742
- if (this._connectPromise) {
11743
- return await this._connectPromise;
11744
- }
11745
- this._ws = new WebSocket(`${this._wsUrl}/apps/${this.appId}?clientVersion=${KOKIMOKI_APP_VERSION}`);
11746
- this._ws.binaryType = "arraybuffer";
11747
- // Close previous connection in hot-reload scenarios
11748
- if (window) {
11749
- if (!window.__KOKIMOKI_WS__) {
11750
- window.__KOKIMOKI_WS__ = {};
11751
- }
11752
- if (this.appId in window.__KOKIMOKI_WS__) {
11753
- console.log(`[Kokimoki] Closing previous connection for ${this.appId}`);
11754
- window.__KOKIMOKI_WS__[this.appId].close();
11755
- }
11756
- window.__KOKIMOKI_WS__[this.appId] = this;
11855
+ KokimokiSchema.boolean = boolean;
11856
+ class Struct extends Generic {
11857
+ fields;
11858
+ constructor(fields) {
11859
+ super();
11860
+ this.fields = fields;
11757
11861
  }
11758
- // Wait for connection
11759
- this._connectPromise = new Promise((onInit) => {
11760
- // Fetch the auth token
11761
- let clientToken = localStorage.getItem("KM_TOKEN");
11762
- // Send the app token on first connect
11763
- this.ws.onopen = () => {
11764
- this.ws.send(JSON.stringify({ type: "auth", code: this.code, token: clientToken }));
11765
- };
11766
- this.ws.onclose = () => {
11767
- this._connected = false;
11768
- this._connectPromise = undefined;
11769
- this._ws.onmessage = null;
11770
- this._ws = undefined;
11771
- if (window && window.__KOKIMOKI_WS__) {
11772
- delete window.__KOKIMOKI_WS__[this.appId];
11773
- }
11774
- // Clean up
11775
- this._subscribeReqPromises.clear();
11776
- this._transactionPromises.clear();
11777
- // Attempt to reconnect
11778
- if (!this._autoReconnect) {
11779
- return;
11780
- }
11781
- console.log(`[Kokimoki] Connection lost, attempting to reconnect in ${this._reconnectTimeout} seconds...`);
11782
- setTimeout(async () => await this.connect(), this._reconnectTimeout * 1000);
11783
- this._reconnectTimeout = Math.min(3, this._reconnectTimeout + 1);
11784
- // Emit disconnected event
11785
- this.emit("disconnected");
11786
- };
11787
- this.ws.onmessage = (e) => {
11788
- // console.log(`Received WS message: ${e.data}`);
11789
- // Handle JSON messages
11790
- if (typeof e.data === "string") {
11791
- const message = JSON.parse(e.data);
11792
- switch (message.type) {
11793
- case "init":
11794
- this.handleInitMessage(message);
11795
- onInit();
11796
- break;
11797
- }
11798
- return;
11799
- }
11800
- // Handle binary messages
11801
- this.handleBinaryMessage(e.data);
11802
- };
11803
- });
11804
- await this._connectPromise;
11805
- this._connected = true;
11806
- // Connection established
11807
- console.log(`[Kokimoki] Client id: ${this.id}`);
11808
- console.log(`[Kokimoki] Client context:`, this.clientContext);
11809
- // Restore subscriptions if reconnected
11810
- const roomNames = Array.from(this._subscriptionsByName.keys()).map((name) => `"${name}"`);
11811
- if (roomNames.length) {
11812
- console.log(`[Kokimoki] Restoring subscriptions: ${roomNames}`);
11862
+ get defaultValue() {
11863
+ return Object.entries(this.fields).reduce((acc, [key, field]) => {
11864
+ acc[key] = field.defaultValue;
11865
+ return acc;
11866
+ }, {});
11813
11867
  }
11814
- for (const subscription of this._subscriptionsByName.values()) {
11815
- try {
11816
- await this.join(subscription.store);
11817
- }
11818
- catch (err) {
11819
- console.error(`[Kokimoki] Failed to restore subscription for "${subscription.roomName}":`, err);
11868
+ set defaultValue(value) {
11869
+ for (const [key, field] of Object.entries(this.fields)) {
11870
+ field.defaultValue = value[key];
11820
11871
  }
11821
11872
  }
11822
- // Emit connected event
11823
- this._reconnectTimeout = 0;
11824
- this.emit("connected");
11825
11873
  }
11826
- handleInitMessage(message) {
11827
- localStorage.setItem("KM_TOKEN", message.clientToken);
11828
- this._id = message.clientId;
11829
- this._connectionId = message.id;
11830
- this._token = message.appToken;
11831
- this._clientContext = message.clientContext;
11832
- // Set up the auth headers
11833
- this._apiHeaders = new Headers({
11834
- Authorization: `Bearer ${this.token}`,
11835
- "Content-Type": "application/json",
11836
- });
11874
+ KokimokiSchema.Struct = Struct;
11875
+ function struct(schema) {
11876
+ return new Struct(schema);
11837
11877
  }
11838
- handleBinaryMessage(data) {
11839
- const reader = new WsMessageReader(data);
11840
- const type = reader.readInt32();
11841
- switch (type) {
11842
- case WsMessageType.SubscribeRes:
11843
- this.handleSubscribeResMessage(reader);
11844
- break;
11845
- case WsMessageType.UnsubscribeRes:
11846
- this.handleUnsubscribeResMessage(reader);
11847
- break;
11848
- case WsMessageType.RoomUpdate:
11849
- this.handleRoomUpdateMessage(reader);
11850
- break;
11851
- case WsMessageType.Error:
11852
- this.handleErrorMessage(reader);
11853
- break;
11854
- case WsMessageType.Pong: {
11855
- const s = reader.readInt32();
11856
- const ms = reader.readInt32();
11857
- this._serverTimeOffset = Date.now() - s * 1000 - ms;
11858
- break;
11859
- }
11878
+ KokimokiSchema.struct = struct;
11879
+ class Dict {
11880
+ schema;
11881
+ defaultValue;
11882
+ constructor(schema, defaultValue = {}) {
11883
+ this.schema = schema;
11884
+ this.defaultValue = defaultValue;
11860
11885
  }
11861
11886
  }
11862
- handleErrorMessage(msg) {
11863
- const reqId = msg.readInt32();
11864
- const error = msg.readString();
11865
- const subscribeReqPromise = this._subscribeReqPromises.get(reqId);
11866
- const transactionPromise = this._transactionPromises.get(reqId);
11867
- if (subscribeReqPromise) {
11868
- this._subscribeReqPromises.delete(reqId);
11869
- subscribeReqPromise.reject(error);
11870
- }
11871
- else if (transactionPromise) {
11872
- this._transactionPromises.delete(reqId);
11873
- transactionPromise.reject(error);
11874
- }
11875
- else {
11876
- console.warn(`Received error for unknown request ${reqId}: ${error}`);
11877
- }
11887
+ KokimokiSchema.Dict = Dict;
11888
+ function dict(schema, defaultValue = {}) {
11889
+ return new Dict(schema, defaultValue);
11878
11890
  }
11879
- handleSubscribeResMessage(msg) {
11880
- const reqId = msg.readInt32();
11881
- const roomHash = msg.readUint32();
11882
- const promise = this._subscribeReqPromises.get(reqId);
11883
- if (promise) {
11884
- this._subscribeReqPromises.delete(reqId);
11885
- // In Write mode, no initial state is sent
11886
- if (!msg.end) {
11887
- promise.resolve(roomHash, msg.readUint8Array());
11888
- }
11889
- else {
11890
- promise.resolve(roomHash);
11891
- }
11891
+ KokimokiSchema.dict = dict;
11892
+ class List extends Generic {
11893
+ schema;
11894
+ defaultValue;
11895
+ constructor(schema, defaultValue = []) {
11896
+ super();
11897
+ this.schema = schema;
11898
+ this.defaultValue = defaultValue;
11892
11899
  }
11893
11900
  }
11894
- handleUnsubscribeResMessage(msg) {
11895
- const reqId = msg.readInt32();
11896
- const promise = this._unsubscribeReqPromises.get(reqId);
11897
- if (promise) {
11898
- this._unsubscribeReqPromises.delete(reqId);
11899
- promise.resolve();
11900
- }
11901
+ KokimokiSchema.List = List;
11902
+ function list(schema, defaultValue = []) {
11903
+ return new List(schema, defaultValue);
11901
11904
  }
11902
- handleRoomUpdateMessage(msg) {
11903
- const appliedId = msg.readInt32();
11904
- const roomHash = msg.readUint32();
11905
- // Apply update if not in Write mode
11906
- if (!msg.end) {
11907
- const subscription = this._subscriptionsByHash.get(roomHash);
11908
- if (subscription) {
11909
- applyUpdate(subscription.store.doc, msg.readUint8Array(), this);
11910
- }
11911
- else {
11912
- console.warn(`Received update for unknown room ${roomHash}`);
11913
- }
11914
- }
11915
- // Check transaction resolves
11916
- for (const [transactionId, { resolve },] of this._transactionPromises.entries()) {
11917
- if (appliedId >= transactionId) {
11918
- this._transactionPromises.delete(transactionId);
11919
- resolve();
11920
- }
11905
+ KokimokiSchema.list = list;
11906
+ /**
11907
+ * Nullable
11908
+ */
11909
+ class Nullable extends Generic {
11910
+ schema;
11911
+ defaultValue;
11912
+ constructor(schema, defaultValue) {
11913
+ super();
11914
+ this.schema = schema;
11915
+ this.defaultValue = defaultValue;
11921
11916
  }
11922
11917
  }
11923
- serverTimestamp() {
11924
- return Date.now() - this._serverTimeOffset;
11918
+ KokimokiSchema.Nullable = Nullable;
11919
+ function nullable(schema, defaultValue = null) {
11920
+ return new Nullable(schema, defaultValue);
11925
11921
  }
11926
- // Send Y update to room
11927
- async patchRoomState(room, update) {
11928
- const res = await fetch(`${this._apiUrl}/rooms/${room}`, {
11929
- method: "PATCH",
11930
- headers: this.apiHeaders,
11931
- body: update,
11932
- });
11933
- return await res.json();
11934
- }
11935
- // Storage
11936
- async createUpload(name, blob, tags) {
11937
- const res = await fetch(`${this._apiUrl}/uploads`, {
11938
- method: "POST",
11939
- headers: this.apiHeaders,
11940
- body: JSON.stringify({
11941
- name,
11942
- size: blob.size,
11943
- mimeType: blob.type,
11944
- tags,
11945
- }),
11946
- });
11947
- return await res.json();
11948
- }
11949
- async uploadChunks(blob, chunkSize, signedUrls) {
11950
- return await Promise.all(signedUrls.map(async (url, index) => {
11951
- const start = index * chunkSize;
11952
- const end = Math.min(start + chunkSize, blob.size);
11953
- const chunk = blob.slice(start, end);
11954
- const res = await fetch(url, { method: "PUT", body: chunk });
11955
- return JSON.parse(res.headers.get("ETag") || '""');
11956
- }));
11957
- }
11958
- async completeUpload(id, etags) {
11959
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
11960
- method: "PUT",
11961
- headers: this.apiHeaders,
11962
- body: JSON.stringify({ etags }),
11963
- });
11964
- return await res.json();
11965
- }
11966
- async upload(name, blob, tags = []) {
11967
- const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
11968
- const etags = await this.uploadChunks(blob, chunkSize, urls);
11969
- return await this.completeUpload(id, etags);
11970
- }
11971
- async updateUpload(id, update) {
11972
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
11973
- method: "PUT",
11974
- headers: this.apiHeaders,
11975
- body: JSON.stringify(update),
11976
- });
11977
- return await res.json();
11978
- }
11979
- async listUploads(filter = {}, skip = 0, limit = 100) {
11980
- const url = new URL("/uploads", this._apiUrl);
11981
- url.searchParams.set("skip", skip.toString());
11982
- url.searchParams.set("limit", limit.toString());
11983
- if (filter.clientId) {
11984
- url.searchParams.set("clientId", filter.clientId);
11985
- }
11986
- if (filter.mimeTypes) {
11987
- url.searchParams.set("mimeTypes", filter.mimeTypes.join());
11988
- }
11989
- if (filter.tags) {
11990
- url.searchParams.set("tags", filter.tags.join());
11991
- }
11992
- const res = await fetch(url.href, {
11993
- headers: this.apiHeaders,
11994
- });
11995
- return await res.json();
11996
- }
11997
- async deleteUpload(id) {
11998
- const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
11999
- method: "DELETE",
12000
- headers: this.apiHeaders,
12001
- });
12002
- return await res.json();
12003
- }
12004
- async exposeScriptingContext(context) {
12005
- // @ts-ignore
12006
- window.KM_SCRIPTING_CONTEXT = context;
12007
- // @ts-ignore
12008
- window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
12009
- }
12010
- async sendSubscribeReq(roomName, mode) {
12011
- // Set up sync resolver
12012
- const reqId = ++this._messageId;
12013
- return await new Promise((resolve, reject) => {
12014
- this._subscribeReqPromises.set(reqId, {
12015
- resolve: (roomHash, initialUpdate) => {
12016
- resolve({ roomHash, initialUpdate });
12017
- },
12018
- reject,
12019
- });
12020
- // Send subscription request
12021
- const msg = new WsMessageWriter();
12022
- msg.writeInt32(WsMessageType.SubscribeReq);
12023
- msg.writeInt32(reqId);
12024
- msg.writeString(roomName);
12025
- msg.writeChar(mode);
12026
- this.ws.send(msg.getBuffer());
12027
- });
12028
- }
12029
- async sendUnsubscribeReq(roomHash) {
12030
- const reqId = ++this._messageId;
12031
- return await new Promise((resolve, reject) => {
12032
- this._unsubscribeReqPromises.set(reqId, {
12033
- resolve,
12034
- reject,
12035
- });
12036
- // Send unsubscribe request
12037
- const msg = new WsMessageWriter();
12038
- msg.writeInt32(WsMessageType.UnsubscribeReq);
12039
- msg.writeInt32(reqId);
12040
- msg.writeUint32(roomHash);
12041
- this.ws.send(msg.getBuffer());
12042
- });
12043
- }
12044
- async join(store) {
12045
- let subscription = this._subscriptionsByName.get(store.roomName);
12046
- if (!subscription) {
12047
- subscription = new RoomSubscription(this, store);
12048
- this._subscriptionsByName.set(store.roomName, subscription);
12049
- }
12050
- // Send subscription request if connected to server
12051
- if (!subscription.joined) {
12052
- const res = await this.sendSubscribeReq(store.roomName, store.mode);
12053
- this._subscriptionsByHash.set(res.roomHash, subscription);
12054
- await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
12055
- // Trigger onJoin event
12056
- store.onJoin(this);
12057
- }
12058
- }
12059
- async leave(store) {
12060
- const subscription = this._subscriptionsByName.get(store.roomName);
12061
- if (subscription) {
12062
- await store.onBeforeLeave(this);
12063
- await this.sendUnsubscribeReq(subscription.roomHash);
12064
- this._subscriptionsByName.delete(store.roomName);
12065
- this._subscriptionsByHash.delete(subscription.roomHash);
12066
- // Trigger onLeave event
12067
- store.onLeave(this);
12068
- }
12069
- }
12070
- async transact(handler) {
12071
- if (!this._connected) {
12072
- throw new Error("Client not connected");
12073
- }
12074
- const transaction = new KokimokiTransaction(this);
12075
- handler(transaction);
12076
- const { updates, consumedMessages } = await transaction.getUpdates();
12077
- if (!updates.length) {
12078
- return;
12079
- }
12080
- // Construct buffer
12081
- const writer = new WsMessageWriter();
12082
- // Write message type
12083
- writer.writeInt32(WsMessageType.Transaction);
12084
- // Update and write transaction ID
12085
- const transactionId = ++this._messageId;
12086
- writer.writeInt32(transactionId);
12087
- // Write room hashes where messages were consumed
12088
- writer.writeInt32(consumedMessages.size);
12089
- for (const roomName of consumedMessages) {
12090
- const subscription = this._subscriptionsByName.get(roomName);
12091
- if (!subscription) {
12092
- throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
12093
- }
12094
- writer.writeUint32(subscription.roomHash);
12095
- }
12096
- // Write updates
12097
- for (const { roomName, update } of updates) {
12098
- const subscription = this._subscriptionsByName.get(roomName);
12099
- if (!subscription) {
12100
- throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
12101
- }
12102
- writer.writeUint32(subscription.roomHash);
12103
- writer.writeUint8Array(update);
12104
- }
12105
- const buffer = writer.getBuffer();
12106
- // Wait for server to apply transaction
12107
- await new Promise((resolve, reject) => {
12108
- this._transactionPromises.set(transactionId, { resolve, reject });
12109
- // Send update to server
12110
- try {
12111
- this.ws.send(buffer);
12112
- }
12113
- catch (e) {
12114
- // Not connected
12115
- console.log("Failed to send update to server:", e);
12116
- // TODO: merge updates or something
12117
- throw e;
12118
- }
12119
- });
12120
- /* // Reset doc in Write-only mode
12121
- for (const { roomName, update } of updates) {
12122
- const mode = this._subscriptions.get(roomName);
12123
-
12124
- if (mode === RoomSubscriptionMode.Write) {
12125
- // @ts-ignore
12126
- // this._stores.get(this._roomHashes.get(roomName)!)!.doc = new Y.Doc();
12127
- }
12128
- } */
12129
- }
12130
- async close() {
12131
- this._autoReconnect = false;
12132
- if (this._ws) {
12133
- this._ws.close();
12134
- }
12135
- if (this._pingInterval) {
12136
- clearInterval(this._pingInterval);
11922
+ KokimokiSchema.nullable = nullable;
11923
+ class StringEnum extends Generic {
11924
+ options;
11925
+ defaultValue;
11926
+ constructor(options, defaultValue) {
11927
+ super();
11928
+ this.options = options;
11929
+ this.defaultValue = defaultValue;
12137
11930
  }
12138
11931
  }
12139
- }
12140
-
12141
- var KokimokiSchema;
12142
- (function (KokimokiSchema) {
12143
- class Generic {
11932
+ KokimokiSchema.StringEnum = StringEnum;
11933
+ function stringEnum(options, defaultValue) {
11934
+ return new StringEnum(options, defaultValue);
12144
11935
  }
12145
- KokimokiSchema.Generic = Generic;
12146
- class Number extends Generic {
11936
+ KokimokiSchema.stringEnum = stringEnum;
11937
+ class StringConst extends Generic {
12147
11938
  defaultValue;
12148
- constructor(defaultValue = 0) {
11939
+ constructor(defaultValue) {
12149
11940
  super();
12150
11941
  this.defaultValue = defaultValue;
12151
11942
  }
12152
11943
  }
12153
- KokimokiSchema.Number = Number;
12154
- function number(defaultValue = 0) {
12155
- return new Number(defaultValue);
11944
+ KokimokiSchema.StringConst = StringConst;
11945
+ function stringConst(defaultValue) {
11946
+ return new StringConst(defaultValue);
12156
11947
  }
12157
- KokimokiSchema.number = number;
12158
- class String extends Generic {
11948
+ KokimokiSchema.stringConst = stringConst;
11949
+ class AnyOf extends Generic {
11950
+ schemas;
12159
11951
  defaultValue;
12160
- constructor(defaultValue = "") {
11952
+ constructor(schemas, defaultValue) {
12161
11953
  super();
11954
+ this.schemas = schemas;
12162
11955
  this.defaultValue = defaultValue;
12163
11956
  }
12164
11957
  }
12165
- KokimokiSchema.String = String;
12166
- function string(defaultValue = "") {
12167
- return new String(defaultValue);
11958
+ KokimokiSchema.AnyOf = AnyOf;
11959
+ function anyOf(schemas, defaultValue) {
11960
+ return new AnyOf(schemas, defaultValue);
12168
11961
  }
12169
- KokimokiSchema.string = string;
12170
- class Boolean extends Generic {
11962
+ KokimokiSchema.anyOf = anyOf;
11963
+ class Optional extends Generic {
11964
+ schema;
12171
11965
  defaultValue;
12172
- constructor(defaultValue = false) {
11966
+ constructor(schema, defaultValue = undefined) {
12173
11967
  super();
11968
+ this.schema = schema;
12174
11969
  this.defaultValue = defaultValue;
12175
11970
  }
12176
11971
  }
12177
- KokimokiSchema.Boolean = Boolean;
12178
- function boolean(defaultValue = false) {
12179
- return new Boolean(defaultValue);
11972
+ KokimokiSchema.Optional = Optional;
11973
+ function optional(schema, defaultValue = undefined) {
11974
+ return new Optional(schema, defaultValue);
12180
11975
  }
12181
- KokimokiSchema.boolean = boolean;
12182
- class Struct extends Generic {
12183
- fields;
12184
- constructor(fields) {
12185
- super();
12186
- this.fields = fields;
12187
- }
12188
- get defaultValue() {
12189
- return Object.entries(this.fields).reduce((acc, [key, field]) => {
12190
- acc[key] = field.defaultValue;
12191
- return acc;
12192
- }, {});
12193
- }
12194
- set defaultValue(value) {
12195
- for (const [key, field] of Object.entries(this.fields)) {
12196
- field.defaultValue = value[key];
11976
+ KokimokiSchema.optional = optional;
11977
+ })(KokimokiSchema || (KokimokiSchema = {}));
11978
+
11979
+ class KokimokiQueue extends KokimokiStore {
11980
+ payloadSchema;
11981
+ _emitter = new EventEmitter$1();
11982
+ on = this._emitter.on.bind(this._emitter);
11983
+ off = this._emitter.off.bind(this._emitter);
11984
+ constructor(roomName, payloadSchema, mode) {
11985
+ super(`/q/${roomName}`, KokimokiSchema.dict(payloadSchema), mode);
11986
+ this.payloadSchema = payloadSchema;
11987
+ const emittedMessageIds = new Set();
11988
+ this.doc.on("update", () => {
11989
+ const newMessages = [];
11990
+ for (const id in this.proxy) {
11991
+ if (emittedMessageIds.has(id)) {
11992
+ continue;
11993
+ }
11994
+ emittedMessageIds.add(id);
11995
+ newMessages.push({ id, payload: this.proxy[id] });
12197
11996
  }
12198
- }
12199
- }
12200
- KokimokiSchema.Struct = Struct;
12201
- function struct(schema) {
12202
- return new Struct(schema);
12203
- }
12204
- KokimokiSchema.struct = struct;
12205
- class Dict {
12206
- schema;
12207
- defaultValue;
12208
- constructor(schema, defaultValue = {}) {
12209
- this.schema = schema;
12210
- this.defaultValue = defaultValue;
12211
- }
12212
- }
12213
- KokimokiSchema.Dict = Dict;
12214
- function dict(schema, defaultValue = {}) {
12215
- return new Dict(schema, defaultValue);
12216
- }
12217
- KokimokiSchema.dict = dict;
12218
- class List extends Generic {
12219
- schema;
12220
- defaultValue;
12221
- constructor(schema, defaultValue = []) {
12222
- super();
12223
- this.schema = schema;
12224
- this.defaultValue = defaultValue;
12225
- }
12226
- }
12227
- KokimokiSchema.List = List;
12228
- function list(schema, defaultValue = []) {
12229
- return new List(schema, defaultValue);
11997
+ if (newMessages.length > 0) {
11998
+ this._emitter.emit("messages", newMessages);
11999
+ }
12000
+ });
12230
12001
  }
12231
- KokimokiSchema.list = list;
12232
- /**
12233
- * Nullable
12234
- */
12235
- class Nullable extends Generic {
12236
- schema;
12237
- defaultValue;
12238
- constructor(schema, defaultValue) {
12239
- super();
12240
- this.schema = schema;
12241
- this.defaultValue = defaultValue;
12242
- }
12002
+ }
12003
+
12004
+ class KokimokiAwareness extends KokimokiStore {
12005
+ dataSchema;
12006
+ _data;
12007
+ _pingInterval = null;
12008
+ _kmClients = new Set();
12009
+ constructor(roomName, dataSchema, _data, mode = RoomSubscriptionMode.ReadWrite, pingTimeout = 3000) {
12010
+ super(`/a/${roomName}`, KokimokiSchema.dict(KokimokiSchema.struct({
12011
+ clientId: KokimokiSchema.string(),
12012
+ lastPing: KokimokiSchema.number(),
12013
+ data: dataSchema,
12014
+ })), mode);
12015
+ this.dataSchema = dataSchema;
12016
+ this._data = _data;
12017
+ this._pingInterval = setInterval(async () => {
12018
+ const kmClients = Array.from(this._kmClients);
12019
+ await Promise.all(kmClients.map(async (client) => {
12020
+ try {
12021
+ await client.transact((t) => {
12022
+ const timestamp = client.serverTimestamp();
12023
+ // Update self
12024
+ if (this.proxy[client.connectionId]) {
12025
+ t.set(this.root[client.connectionId].lastPing, timestamp);
12026
+ }
12027
+ else {
12028
+ t.set(this.root[client.connectionId], {
12029
+ clientId: client.id,
12030
+ lastPing: timestamp,
12031
+ data: this._data,
12032
+ });
12033
+ }
12034
+ // Delete clients that haven't pinged in a while
12035
+ for (const connectionId in this.proxy) {
12036
+ const { lastPing } = this.proxy[connectionId];
12037
+ if (timestamp - lastPing > pingTimeout * 2) {
12038
+ t.delete(this.root[connectionId]);
12039
+ }
12040
+ }
12041
+ });
12042
+ }
12043
+ catch (e) { }
12044
+ }));
12045
+ }, pingTimeout);
12243
12046
  }
12244
- KokimokiSchema.Nullable = Nullable;
12245
- function nullable(schema, defaultValue = null) {
12246
- return new Nullable(schema, defaultValue);
12047
+ async onJoin(client) {
12048
+ this._kmClients.add(client);
12049
+ await client.transact((t) => {
12050
+ t.set(this.root[client.connectionId], {
12051
+ clientId: client.id,
12052
+ lastPing: client.serverTimestamp(),
12053
+ data: this._data,
12054
+ });
12055
+ });
12247
12056
  }
12248
- KokimokiSchema.nullable = nullable;
12249
- class StringEnum extends Generic {
12250
- options;
12251
- defaultValue;
12252
- constructor(options, defaultValue) {
12253
- super();
12254
- this.options = options;
12255
- this.defaultValue = defaultValue;
12256
- }
12057
+ async onBeforeLeave(client) {
12058
+ await client.transact((t) => {
12059
+ t.delete(this.root[client.connectionId]);
12060
+ });
12257
12061
  }
12258
- KokimokiSchema.StringEnum = StringEnum;
12259
- function stringEnum(options, defaultValue) {
12260
- return new StringEnum(options, defaultValue);
12062
+ async onLeave(client) {
12063
+ this._kmClients.delete(client);
12261
12064
  }
12262
- KokimokiSchema.stringEnum = stringEnum;
12263
- class StringConst extends Generic {
12264
- defaultValue;
12265
- constructor(defaultValue) {
12266
- super();
12267
- this.defaultValue = defaultValue;
12065
+ getClients() {
12066
+ const clients = {};
12067
+ for (const connectionId in this.proxy) {
12068
+ clients[this.proxy[connectionId].clientId] =
12069
+ this.proxy[connectionId].data;
12268
12070
  }
12071
+ return clients;
12269
12072
  }
12270
- KokimokiSchema.StringConst = StringConst;
12271
- function stringConst(defaultValue) {
12272
- return new StringConst(defaultValue);
12073
+ async setData(data) {
12074
+ this._data = data;
12075
+ const kmClients = Array.from(this._kmClients);
12076
+ await Promise.all(kmClients.map(async (client) => {
12077
+ await client.transact((t) => {
12078
+ t.set(this.root[client.connectionId].data, this._data);
12079
+ });
12080
+ }));
12273
12081
  }
12274
- KokimokiSchema.stringConst = stringConst;
12275
- class AnyOf extends Generic {
12276
- schemas;
12277
- defaultValue;
12278
- constructor(schemas, defaultValue) {
12279
- super();
12280
- this.schemas = schemas;
12281
- this.defaultValue = defaultValue;
12282
- }
12082
+ }
12083
+
12084
+ class KokimokiReqRes {
12085
+ kmClient;
12086
+ serviceName;
12087
+ reqSchema;
12088
+ resSchema;
12089
+ handleRequest;
12090
+ _reqQueueSchema;
12091
+ _resQueueSchema;
12092
+ // Request queues are linked to client ids
12093
+ _reqQueues = {};
12094
+ // Response queues are linked to specific connection ids
12095
+ _resQueues = {};
12096
+ _reqPromises = {};
12097
+ async _getReqQueue(clientId, mode = RoomSubscriptionMode.Write) {
12098
+ if (!this._reqQueues.hasOwnProperty(clientId)) {
12099
+ this._reqQueues[clientId] = this.kmClient.queue(`/r/${clientId}/req/${this.serviceName}`, this._reqQueueSchema, mode, false);
12100
+ await this.kmClient.join(this._reqQueues[clientId]);
12101
+ }
12102
+ return this._reqQueues[clientId];
12103
+ }
12104
+ async _getResQueue(connectionId, mode = RoomSubscriptionMode.Write) {
12105
+ if (!this._resQueues.hasOwnProperty(connectionId)) {
12106
+ this._resQueues[connectionId] = this.kmClient.queue(`/r/${connectionId}/res/${this.serviceName}`, this._resQueueSchema, mode, false);
12107
+ await this.kmClient.join(this._resQueues[connectionId]);
12108
+ }
12109
+ return this._resQueues[connectionId];
12110
+ }
12111
+ constructor(kmClient, serviceName, reqSchema, resSchema, handleRequest) {
12112
+ this.kmClient = kmClient;
12113
+ this.serviceName = serviceName;
12114
+ this.reqSchema = reqSchema;
12115
+ this.resSchema = resSchema;
12116
+ this.handleRequest = handleRequest;
12117
+ this._reqQueueSchema = KokimokiSchema.struct({
12118
+ connectionId: KokimokiSchema.string(),
12119
+ // roomHash: S.number(),
12120
+ payload: reqSchema,
12121
+ });
12122
+ this._resQueueSchema = KokimokiSchema.struct({
12123
+ reqId: KokimokiSchema.string(),
12124
+ error: KokimokiSchema.optional(KokimokiSchema.string()),
12125
+ payload: resSchema,
12126
+ });
12127
+ // Set up queues on connect
12128
+ kmClient.on("connected", async () => {
12129
+ // Handle responses to self
12130
+ const resQueue = await this._getResQueue(kmClient.connectionId, RoomSubscriptionMode.ReadWrite);
12131
+ let handlingResponses = false;
12132
+ let rehandleResponses = false;
12133
+ resQueue.subscribe(async (messages) => {
12134
+ // console.log("RES", Object.keys(messages));
12135
+ if (handlingResponses) {
12136
+ rehandleResponses = true;
12137
+ return;
12138
+ }
12139
+ handlingResponses = true;
12140
+ await this.handleResponses(resQueue, messages);
12141
+ if (rehandleResponses) {
12142
+ rehandleResponses = false;
12143
+ await this.handleResponses(resQueue, messages);
12144
+ }
12145
+ handlingResponses = false;
12146
+ });
12147
+ // Handle requests to self
12148
+ const reqQueue = await this._getReqQueue(kmClient.id, RoomSubscriptionMode.ReadWrite);
12149
+ let handlingRequests = false;
12150
+ let rehandleRequests = false;
12151
+ reqQueue.subscribe(async (messages) => {
12152
+ // console.log("REQ", Object.keys(messages));
12153
+ if (handlingRequests) {
12154
+ rehandleRequests = true;
12155
+ return;
12156
+ }
12157
+ handlingRequests = true;
12158
+ await this.handleRequests(reqQueue, messages);
12159
+ if (rehandleRequests) {
12160
+ rehandleRequests = false;
12161
+ await this.handleRequests(reqQueue, messages);
12162
+ }
12163
+ handlingRequests = false;
12164
+ });
12165
+ });
12283
12166
  }
12284
- KokimokiSchema.AnyOf = AnyOf;
12285
- function anyOf(schemas, defaultValue) {
12286
- return new AnyOf(schemas, defaultValue);
12167
+ async handleRequests(reqQueue, messages) {
12168
+ // Send responses
12169
+ await this.kmClient.transact(async (t) => {
12170
+ for (const reqId in messages) {
12171
+ const { connectionId, payload } = messages[reqId];
12172
+ const resQueue = await this._getResQueue(connectionId);
12173
+ t.consumeMessage(reqQueue, reqId);
12174
+ try {
12175
+ t.queueMessage(resQueue, {
12176
+ reqId,
12177
+ error: undefined,
12178
+ payload: await this.handleRequest(payload),
12179
+ });
12180
+ }
12181
+ catch (e) {
12182
+ t.queueMessage(resQueue, {
12183
+ reqId,
12184
+ error: e.toString(),
12185
+ payload: null,
12186
+ });
12187
+ }
12188
+ }
12189
+ });
12287
12190
  }
12288
- KokimokiSchema.anyOf = anyOf;
12289
- class Optional extends Generic {
12290
- schema;
12291
- defaultValue;
12292
- constructor(schema, defaultValue = undefined) {
12293
- super();
12294
- this.schema = schema;
12295
- this.defaultValue = defaultValue;
12296
- }
12191
+ async handleResponses(resQueue, messages) {
12192
+ // Handle responses
12193
+ await this.kmClient.transact(async (t) => {
12194
+ for (const resId in messages) {
12195
+ const { reqId, error, payload } = messages[resId];
12196
+ const promise = this._reqPromises[reqId];
12197
+ t.consumeMessage(resQueue, resId);
12198
+ if (promise) {
12199
+ if (error) {
12200
+ promise.reject(error);
12201
+ }
12202
+ else {
12203
+ promise.resolve(payload);
12204
+ }
12205
+ delete this._reqPromises[reqId];
12206
+ }
12207
+ }
12208
+ });
12297
12209
  }
12298
- KokimokiSchema.Optional = Optional;
12299
- function optional(schema, defaultValue = undefined) {
12300
- return new Optional(schema, defaultValue);
12210
+ async send(toClientId, payload, timeout) {
12211
+ const toReqQueue = await this._getReqQueue(toClientId);
12212
+ // Create a promise for the response
12213
+ return await new Promise(async (resolve, reject) => {
12214
+ await this.kmClient.transact((t) => {
12215
+ const reqId = t.queueMessage(toReqQueue, {
12216
+ connectionId: this.kmClient.connectionId,
12217
+ payload,
12218
+ // roomHash,
12219
+ });
12220
+ this._reqPromises[reqId] = { resolve, reject };
12221
+ });
12222
+ });
12301
12223
  }
12302
- KokimokiSchema.optional = optional;
12303
- })(KokimokiSchema || (KokimokiSchema = {}));
12304
-
12305
- function parsePath(text) {
12306
- return text.split('.')
12307
- }
12308
-
12309
- function push(arr, el) {
12310
- const newArr = arr.slice();
12311
- newArr.push(el);
12312
- return newArr;
12313
12224
  }
12314
12225
 
12315
- // names of the traps that can be registered with ES6's Proxy object
12316
- const trapNames = [
12317
- 'apply',
12318
- 'construct',
12319
- 'defineProperty',
12320
- 'deleteProperty',
12321
- 'enumerate', // deprecated
12322
- 'get',
12323
- 'getOwnPropertyDescriptor',
12324
- 'getPrototypeOf',
12325
- 'has',
12326
- 'isExtensible',
12327
- 'ownKeys',
12328
- 'preventExtensions',
12329
- 'set',
12330
- 'setPrototypeOf',
12331
- ];
12332
-
12333
- // a list of paramer indexes that indicate that the a recieves a key at that parameter
12334
- // this information will be used to update the path accordingly
12335
- const keys = {
12336
- get: 1,
12337
- set: 1,
12338
- deleteProperty: 1,
12339
- has: 1,
12340
- defineProperty: 1,
12341
- getOwnPropertyDescriptor: 1,
12342
- };
12343
-
12344
- function DeepProxy(rootTarget, traps, options) {
12345
-
12346
- let path = [];
12347
- let userData = {};
12348
-
12349
- if (options !== undefined && typeof options.path !== 'undefined') {
12350
- path = parsePath(options.path);
12351
- }
12352
- if (options !== undefined && typeof options.userData !== 'undefined') {
12353
- userData = options.userData;
12354
- }
12355
-
12356
- function createProxy(target, path) {
12357
-
12358
- // avoid creating a new object between two traps
12359
- const context = { rootTarget, path };
12360
- Object.assign(context, userData);
12361
-
12362
- const realTraps = {};
12363
-
12364
- for (const trapName of trapNames) {
12365
- const keyParamIdx = keys[trapName]
12366
- , trap = traps[trapName];
12367
-
12368
- if (typeof trap !== 'undefined') {
12369
-
12370
- if (typeof keyParamIdx !== 'undefined') {
12371
-
12372
- realTraps[trapName] = function () {
12373
-
12374
- const key = arguments[keyParamIdx];
12375
-
12376
- // update context for this trap
12377
- context.nest = function (nestedTarget) {
12378
- if (nestedTarget === undefined)
12379
- nestedTarget = rootTarget;
12380
- return createProxy(nestedTarget, push(path, key));
12226
+ class KokimokiClient extends EventEmitter$1 {
12227
+ host;
12228
+ appId;
12229
+ code;
12230
+ _wsUrl;
12231
+ _apiUrl;
12232
+ _id;
12233
+ _connectionId;
12234
+ _token;
12235
+ _apiHeaders;
12236
+ _serverTimeOffset = 0;
12237
+ _clientContext;
12238
+ _ws;
12239
+ _subscriptionsByName = new Map();
12240
+ _subscriptionsByHash = new Map();
12241
+ _subscribeReqPromises = new Map();
12242
+ _unsubscribeReqPromises = new Map();
12243
+ _transactionPromises = new Map();
12244
+ _connected = false;
12245
+ _connectPromise;
12246
+ _messageId = 0;
12247
+ _autoReconnect = true;
12248
+ _reconnectTimeout = 0;
12249
+ _pingInterval;
12250
+ constructor(host, appId, code = "") {
12251
+ super();
12252
+ this.host = host;
12253
+ this.appId = appId;
12254
+ this.code = code;
12255
+ // Set up the URLs
12256
+ const secure = this.host.indexOf(":") === -1;
12257
+ this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
12258
+ this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
12259
+ // Set up ping interval
12260
+ const pingMsg = new WsMessageWriter();
12261
+ pingMsg.writeInt32(WsMessageType.Ping);
12262
+ const pingBuffer = pingMsg.getBuffer();
12263
+ this._pingInterval = setInterval(() => {
12264
+ if (this.connected) {
12265
+ this.ws.send(pingBuffer);
12266
+ }
12267
+ }, 5000);
12268
+ }
12269
+ get id() {
12270
+ if (!this._id) {
12271
+ throw new Error("Client not connected");
12272
+ }
12273
+ return this._id;
12274
+ }
12275
+ get connectionId() {
12276
+ if (!this._connectionId) {
12277
+ throw new Error("Client not connected");
12278
+ }
12279
+ return this._connectionId;
12280
+ }
12281
+ get token() {
12282
+ if (!this._token) {
12283
+ throw new Error("Client not connected");
12284
+ }
12285
+ return this._token;
12286
+ }
12287
+ get apiUrl() {
12288
+ if (!this._apiUrl) {
12289
+ throw new Error("Client not connected");
12290
+ }
12291
+ return this._apiUrl;
12292
+ }
12293
+ get apiHeaders() {
12294
+ if (!this._apiHeaders) {
12295
+ throw new Error("Client not connected");
12296
+ }
12297
+ return this._apiHeaders;
12298
+ }
12299
+ get clientContext() {
12300
+ if (this._clientContext === undefined) {
12301
+ throw new Error("Client not connected");
12302
+ }
12303
+ return this._clientContext;
12304
+ }
12305
+ get connected() {
12306
+ return this._connected;
12307
+ }
12308
+ get ws() {
12309
+ if (!this._ws) {
12310
+ throw new Error("Not connected");
12311
+ }
12312
+ return this._ws;
12313
+ }
12314
+ async connect() {
12315
+ if (this._connectPromise) {
12316
+ return await this._connectPromise;
12317
+ }
12318
+ this._ws = new WebSocket(`${this._wsUrl}/apps/${this.appId}?clientVersion=${KOKIMOKI_APP_VERSION}`);
12319
+ this._ws.binaryType = "arraybuffer";
12320
+ // Close previous connection in hot-reload scenarios
12321
+ if (window) {
12322
+ if (!window.__KOKIMOKI_WS__) {
12323
+ window.__KOKIMOKI_WS__ = {};
12324
+ }
12325
+ if (this.appId in window.__KOKIMOKI_WS__) {
12326
+ console.log(`[Kokimoki] Closing previous connection for ${this.appId}`);
12327
+ window.__KOKIMOKI_WS__[this.appId].close();
12328
+ }
12329
+ window.__KOKIMOKI_WS__[this.appId] = this;
12330
+ }
12331
+ // Wait for connection
12332
+ this._connectPromise = new Promise((onInit) => {
12333
+ // Fetch the auth token
12334
+ let clientToken = localStorage.getItem("KM_TOKEN");
12335
+ // Send the app token on first connect
12336
+ this.ws.onopen = () => {
12337
+ this.ws.send(JSON.stringify({ type: "auth", code: this.code, token: clientToken }));
12381
12338
  };
12382
-
12383
- return trap.apply(context, arguments);
12384
- };
12385
- } else {
12386
-
12387
- realTraps[trapName] = function () {
12388
-
12389
- // update context for this trap
12390
- context.nest = function (nestedTarget) {
12391
- if (nestedTarget === undefined)
12392
- nestedTarget = {};
12393
- return createProxy(nestedTarget, path);
12339
+ this.ws.onclose = () => {
12340
+ this._connected = false;
12341
+ this._connectPromise = undefined;
12342
+ this._ws.onmessage = null;
12343
+ this._ws = undefined;
12344
+ if (window && window.__KOKIMOKI_WS__) {
12345
+ delete window.__KOKIMOKI_WS__[this.appId];
12346
+ }
12347
+ // Clean up
12348
+ this._subscribeReqPromises.clear();
12349
+ this._transactionPromises.clear();
12350
+ // Attempt to reconnect
12351
+ if (!this._autoReconnect) {
12352
+ return;
12353
+ }
12354
+ console.log(`[Kokimoki] Connection lost, attempting to reconnect in ${this._reconnectTimeout} seconds...`);
12355
+ setTimeout(async () => await this.connect(), this._reconnectTimeout * 1000);
12356
+ this._reconnectTimeout = Math.min(3, this._reconnectTimeout + 1);
12357
+ // Emit disconnected event
12358
+ this.emit("disconnected");
12394
12359
  };
12395
-
12396
- return trap.apply(context, arguments);
12397
- };
12360
+ this.ws.onmessage = (e) => {
12361
+ // console.log(`Received WS message: ${e.data}`);
12362
+ // Handle JSON messages
12363
+ if (typeof e.data === "string") {
12364
+ const message = JSON.parse(e.data);
12365
+ switch (message.type) {
12366
+ case "init":
12367
+ this.handleInitMessage(message);
12368
+ onInit();
12369
+ break;
12370
+ }
12371
+ return;
12372
+ }
12373
+ // Handle binary messages
12374
+ this.handleBinaryMessage(e.data);
12375
+ };
12376
+ });
12377
+ await this._connectPromise;
12378
+ this._connected = true;
12379
+ // Connection established
12380
+ console.log(`[Kokimoki] Client id: ${this.id}`);
12381
+ console.log(`[Kokimoki] Client context:`, this.clientContext);
12382
+ // Restore subscriptions if reconnected
12383
+ const roomNames = Array.from(this._subscriptionsByName.keys()).map((name) => `"${name}"`);
12384
+ if (roomNames.length) {
12385
+ console.log(`[Kokimoki] Restoring subscriptions: ${roomNames}`);
12398
12386
  }
12399
- }
12387
+ for (const subscription of this._subscriptionsByName.values()) {
12388
+ try {
12389
+ await this.join(subscription.store);
12390
+ }
12391
+ catch (err) {
12392
+ console.error(`[Kokimoki] Failed to restore subscription for "${subscription.roomName}":`, err);
12393
+ }
12394
+ }
12395
+ // Emit connected event
12396
+ this._reconnectTimeout = 0;
12397
+ this.emit("connected");
12400
12398
  }
12401
-
12402
- return new Proxy(target, realTraps);
12403
- }
12404
-
12405
- return createProxy(rootTarget, path);
12406
-
12407
- }
12408
-
12409
- class KokimokiStore {
12410
- roomName;
12411
- mode;
12412
- doc;
12413
- proxy;
12414
- root;
12415
- defaultValue;
12416
- docRoot;
12417
- constructor(roomName, schema, mode = RoomSubscriptionMode.ReadWrite) {
12418
- this.roomName = roomName;
12419
- this.mode = mode;
12420
- // Construct Y doc
12421
- this.doc = new Doc();
12422
- this.docRoot = this.doc.getMap("root");
12423
- // Construct proxy object
12424
- this.proxy = proxy();
12425
- // @ts-ignore
12426
- f(this.proxy, this.docRoot);
12427
- // Create root proxy
12428
- const store = this;
12429
- // @ts-ignore
12430
- this.root = new DeepProxy(this.proxy, {
12431
- get() {
12432
- return this.nest(function () { });
12433
- },
12434
- apply() {
12435
- let obj = store.proxy;
12436
- for (let i = 0; i < this.path.length - 1; i++) {
12437
- obj = obj[this.path[i]];
12438
- }
12439
- return {
12440
- roomName: store.roomName,
12441
- doc: store.doc,
12442
- path: this.path,
12443
- obj,
12444
- key: this.path[this.path.length - 1],
12445
- };
12446
- },
12399
+ handleInitMessage(message) {
12400
+ localStorage.setItem("KM_TOKEN", message.clientToken);
12401
+ this._id = message.clientId;
12402
+ this._connectionId = message.id;
12403
+ this._token = message.appToken;
12404
+ this._clientContext = message.clientContext;
12405
+ // Set up the auth headers
12406
+ this._apiHeaders = new Headers({
12407
+ Authorization: `Bearer ${this.token}`,
12408
+ "Content-Type": "application/json",
12409
+ });
12410
+ }
12411
+ handleBinaryMessage(data) {
12412
+ const reader = new WsMessageReader(data);
12413
+ const type = reader.readInt32();
12414
+ switch (type) {
12415
+ case WsMessageType.SubscribeRes:
12416
+ this.handleSubscribeResMessage(reader);
12417
+ break;
12418
+ case WsMessageType.UnsubscribeRes:
12419
+ this.handleUnsubscribeResMessage(reader);
12420
+ break;
12421
+ case WsMessageType.RoomUpdate:
12422
+ this.handleRoomUpdateMessage(reader);
12423
+ break;
12424
+ case WsMessageType.Error:
12425
+ this.handleErrorMessage(reader);
12426
+ break;
12427
+ case WsMessageType.Pong: {
12428
+ const s = reader.readInt32();
12429
+ const ms = reader.readInt32();
12430
+ this._serverTimeOffset = Date.now() - s * 1000 - ms;
12431
+ break;
12432
+ }
12433
+ }
12434
+ }
12435
+ handleErrorMessage(msg) {
12436
+ const reqId = msg.readInt32();
12437
+ const error = msg.readString();
12438
+ const subscribeReqPromise = this._subscribeReqPromises.get(reqId);
12439
+ const transactionPromise = this._transactionPromises.get(reqId);
12440
+ if (subscribeReqPromise) {
12441
+ this._subscribeReqPromises.delete(reqId);
12442
+ subscribeReqPromise.reject(error);
12443
+ }
12444
+ else if (transactionPromise) {
12445
+ this._transactionPromises.delete(reqId);
12446
+ transactionPromise.reject(error);
12447
+ }
12448
+ else {
12449
+ console.warn(`Received error for unknown request ${reqId}: ${error}`);
12450
+ }
12451
+ }
12452
+ handleSubscribeResMessage(msg) {
12453
+ const reqId = msg.readInt32();
12454
+ const roomHash = msg.readUint32();
12455
+ const promise = this._subscribeReqPromises.get(reqId);
12456
+ if (promise) {
12457
+ this._subscribeReqPromises.delete(reqId);
12458
+ // In Write mode, no initial state is sent
12459
+ if (!msg.end) {
12460
+ promise.resolve(roomHash, msg.readUint8Array());
12461
+ }
12462
+ else {
12463
+ promise.resolve(roomHash);
12464
+ }
12465
+ }
12466
+ }
12467
+ handleUnsubscribeResMessage(msg) {
12468
+ const reqId = msg.readInt32();
12469
+ const promise = this._unsubscribeReqPromises.get(reqId);
12470
+ if (promise) {
12471
+ this._unsubscribeReqPromises.delete(reqId);
12472
+ promise.resolve();
12473
+ }
12474
+ }
12475
+ handleRoomUpdateMessage(msg) {
12476
+ const appliedId = msg.readInt32();
12477
+ const roomHash = msg.readUint32();
12478
+ // Apply update if not in Write mode
12479
+ if (!msg.end) {
12480
+ const subscription = this._subscriptionsByHash.get(roomHash);
12481
+ if (subscription) {
12482
+ applyUpdate(subscription.store.doc, msg.readUint8Array(), this);
12483
+ }
12484
+ else {
12485
+ console.warn(`Received update for unknown room ${roomHash}`);
12486
+ }
12487
+ }
12488
+ // Check transaction resolves
12489
+ for (const [transactionId, { resolve },] of this._transactionPromises.entries()) {
12490
+ if (appliedId >= transactionId) {
12491
+ this._transactionPromises.delete(transactionId);
12492
+ resolve();
12493
+ }
12494
+ }
12495
+ }
12496
+ serverTimestamp() {
12497
+ return Date.now() - this._serverTimeOffset;
12498
+ }
12499
+ // Send Y update to room
12500
+ async patchRoomState(room, update) {
12501
+ const res = await fetch(`${this._apiUrl}/rooms/${room}`, {
12502
+ method: "PATCH",
12503
+ headers: this.apiHeaders,
12504
+ body: update,
12505
+ });
12506
+ return await res.json();
12507
+ }
12508
+ // Storage
12509
+ async createUpload(name, blob, tags) {
12510
+ const res = await fetch(`${this._apiUrl}/uploads`, {
12511
+ method: "POST",
12512
+ headers: this.apiHeaders,
12513
+ body: JSON.stringify({
12514
+ name,
12515
+ size: blob.size,
12516
+ mimeType: blob.type,
12517
+ tags,
12518
+ }),
12519
+ });
12520
+ return await res.json();
12521
+ }
12522
+ async uploadChunks(blob, chunkSize, signedUrls) {
12523
+ return await Promise.all(signedUrls.map(async (url, index) => {
12524
+ const start = index * chunkSize;
12525
+ const end = Math.min(start + chunkSize, blob.size);
12526
+ const chunk = blob.slice(start, end);
12527
+ const res = await fetch(url, { method: "PUT", body: chunk });
12528
+ return JSON.parse(res.headers.get("ETag") || '""');
12529
+ }));
12530
+ }
12531
+ async completeUpload(id, etags) {
12532
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
12533
+ method: "PUT",
12534
+ headers: this.apiHeaders,
12535
+ body: JSON.stringify({ etags }),
12536
+ });
12537
+ return await res.json();
12538
+ }
12539
+ async upload(name, blob, tags = []) {
12540
+ const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
12541
+ const etags = await this.uploadChunks(blob, chunkSize, urls);
12542
+ return await this.completeUpload(id, etags);
12543
+ }
12544
+ async updateUpload(id, update) {
12545
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
12546
+ method: "PUT",
12547
+ headers: this.apiHeaders,
12548
+ body: JSON.stringify(update),
12447
12549
  });
12448
- // Set default value
12449
- this.defaultValue = schema.defaultValue;
12550
+ return await res.json();
12450
12551
  }
12451
- get() {
12452
- return this.proxy;
12552
+ async listUploads(filter = {}, skip = 0, limit = 100) {
12553
+ const url = new URL("/uploads", this._apiUrl);
12554
+ url.searchParams.set("skip", skip.toString());
12555
+ url.searchParams.set("limit", limit.toString());
12556
+ if (filter.clientId) {
12557
+ url.searchParams.set("clientId", filter.clientId);
12558
+ }
12559
+ if (filter.mimeTypes) {
12560
+ url.searchParams.set("mimeTypes", filter.mimeTypes.join());
12561
+ }
12562
+ if (filter.tags) {
12563
+ url.searchParams.set("tags", filter.tags.join());
12564
+ }
12565
+ const res = await fetch(url.href, {
12566
+ headers: this.apiHeaders,
12567
+ });
12568
+ return await res.json();
12453
12569
  }
12454
- subscribe(set) {
12570
+ async deleteUpload(id) {
12571
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
12572
+ method: "DELETE",
12573
+ headers: this.apiHeaders,
12574
+ });
12575
+ return await res.json();
12576
+ }
12577
+ async exposeScriptingContext(context) {
12455
12578
  // @ts-ignore
12456
- const handler = () => set(this.proxy);
12457
- this.doc.on("update", handler);
12579
+ window.KM_SCRIPTING_CONTEXT = context;
12458
12580
  // @ts-ignore
12459
- set(this.proxy);
12460
- return () => this.doc.off("update", handler);
12581
+ window.dispatchEvent(new CustomEvent("km:scriptingContextExposed"));
12461
12582
  }
12462
- async onJoin(client) { }
12463
- async onBeforeLeave(client) { }
12464
- async onLeave(client) { }
12465
- }
12466
-
12467
- class KokimokiQueue extends KokimokiStore {
12468
- payloadSchema;
12469
- _emitter = new EventEmitter$1();
12470
- on = this._emitter.on.bind(this._emitter);
12471
- off = this._emitter.off.bind(this._emitter);
12472
- constructor(roomName, payloadSchema, mode) {
12473
- super(`/q/${roomName}`, KokimokiSchema.dict(KokimokiSchema.struct({
12474
- timestamp: KokimokiSchema.number(),
12475
- payload: payloadSchema,
12476
- })), mode);
12477
- this.payloadSchema = payloadSchema;
12478
- const emittedMessageIds = new Set();
12479
- this.doc.on("update", () => {
12480
- const newMessages = [];
12481
- for (const id in this.proxy) {
12482
- if (emittedMessageIds.has(id)) {
12483
- continue;
12484
- }
12485
- emittedMessageIds.add(id);
12486
- newMessages.push({ id, payload: this.proxy[id].payload });
12487
- }
12488
- if (newMessages.length > 0) {
12489
- this._emitter.emit("messages", newMessages);
12490
- }
12583
+ async sendSubscribeReq(roomName, mode) {
12584
+ // Set up sync resolver
12585
+ const reqId = ++this._messageId;
12586
+ return await new Promise((resolve, reject) => {
12587
+ this._subscribeReqPromises.set(reqId, {
12588
+ resolve: (roomHash, initialUpdate) => {
12589
+ resolve({ roomHash, initialUpdate });
12590
+ },
12591
+ reject,
12592
+ });
12593
+ // Send subscription request
12594
+ const msg = new WsMessageWriter();
12595
+ msg.writeInt32(WsMessageType.SubscribeReq);
12596
+ msg.writeInt32(reqId);
12597
+ msg.writeString(roomName);
12598
+ msg.writeChar(mode);
12599
+ this.ws.send(msg.getBuffer());
12491
12600
  });
12492
12601
  }
12493
- }
12494
-
12495
- class KokimokiAwareness extends KokimokiStore {
12496
- dataSchema;
12497
- _data;
12498
- _pingInterval = null;
12499
- _kmClients = new Set();
12500
- constructor(roomName, dataSchema, _data, mode = RoomSubscriptionMode.ReadWrite, pingTimeout = 3000) {
12501
- super(`/a/${roomName}`, KokimokiSchema.dict(KokimokiSchema.struct({
12502
- clientId: KokimokiSchema.string(),
12503
- lastPing: KokimokiSchema.number(),
12504
- data: dataSchema,
12505
- })), mode);
12506
- this.dataSchema = dataSchema;
12507
- this._data = _data;
12508
- this._pingInterval = setInterval(async () => {
12509
- const kmClients = Array.from(this._kmClients);
12510
- await Promise.all(kmClients.map(async (client) => {
12511
- try {
12512
- await client.transact((t) => {
12513
- const timestamp = client.serverTimestamp();
12514
- // Update self
12515
- if (this.proxy[client.connectionId]) {
12516
- t.set(this.root[client.connectionId].lastPing, timestamp);
12517
- }
12518
- else {
12519
- t.set(this.root[client.connectionId], {
12520
- clientId: client.id,
12521
- lastPing: timestamp,
12522
- data: this._data,
12523
- });
12524
- }
12525
- // Delete clients that haven't pinged in a while
12526
- for (const connectionId in this.proxy) {
12527
- const { lastPing } = this.proxy[connectionId];
12528
- if (timestamp - lastPing > pingTimeout * 2) {
12529
- t.delete(this.root[connectionId]);
12530
- }
12531
- }
12532
- });
12533
- }
12534
- catch (e) { }
12535
- }));
12536
- }, pingTimeout);
12537
- }
12538
- async onJoin(client) {
12539
- this._kmClients.add(client);
12540
- await client.transact((t) => {
12541
- t.set(this.root[client.connectionId], {
12542
- clientId: client.id,
12543
- lastPing: client.serverTimestamp(),
12544
- data: this._data,
12602
+ async sendUnsubscribeReq(roomHash) {
12603
+ const reqId = ++this._messageId;
12604
+ return await new Promise((resolve, reject) => {
12605
+ this._unsubscribeReqPromises.set(reqId, {
12606
+ resolve,
12607
+ reject,
12545
12608
  });
12609
+ // Send unsubscribe request
12610
+ const msg = new WsMessageWriter();
12611
+ msg.writeInt32(WsMessageType.UnsubscribeReq);
12612
+ msg.writeInt32(reqId);
12613
+ msg.writeUint32(roomHash);
12614
+ this.ws.send(msg.getBuffer());
12546
12615
  });
12547
12616
  }
12548
- async onBeforeLeave(client) {
12549
- await client.transact((t) => {
12550
- t.delete(this.root[client.connectionId]);
12551
- });
12617
+ async join(store) {
12618
+ let subscription = this._subscriptionsByName.get(store.roomName);
12619
+ if (!subscription) {
12620
+ subscription = new RoomSubscription(this, store);
12621
+ this._subscriptionsByName.set(store.roomName, subscription);
12622
+ }
12623
+ // Send subscription request if connected to server
12624
+ if (!subscription.joined) {
12625
+ const res = await this.sendSubscribeReq(store.roomName, store.mode);
12626
+ this._subscriptionsByHash.set(res.roomHash, subscription);
12627
+ await subscription.applyInitialResponse(res.roomHash, res.initialUpdate);
12628
+ // Trigger onJoin event
12629
+ store.onJoin(this);
12630
+ }
12552
12631
  }
12553
- async onLeave(client) {
12554
- this._kmClients.delete(client);
12632
+ async leave(store) {
12633
+ const subscription = this._subscriptionsByName.get(store.roomName);
12634
+ if (subscription) {
12635
+ await store.onBeforeLeave(this);
12636
+ await this.sendUnsubscribeReq(subscription.roomHash);
12637
+ this._subscriptionsByName.delete(store.roomName);
12638
+ this._subscriptionsByHash.delete(subscription.roomHash);
12639
+ subscription.close();
12640
+ // Trigger onLeave event
12641
+ store.onLeave(this);
12642
+ }
12555
12643
  }
12556
- getClients() {
12557
- const clients = {};
12558
- for (const connectionId in this.proxy) {
12559
- clients[this.proxy[connectionId].clientId] =
12560
- this.proxy[connectionId].data;
12644
+ async transact(handler) {
12645
+ if (!this._connected) {
12646
+ throw new Error("Client not connected");
12561
12647
  }
12562
- return clients;
12648
+ const transaction = new KokimokiTransaction(this);
12649
+ await handler(transaction);
12650
+ const { updates, consumedMessages } = await transaction.getUpdates();
12651
+ if (!updates.length) {
12652
+ return;
12653
+ }
12654
+ // Construct buffer
12655
+ const writer = new WsMessageWriter();
12656
+ // Write message type
12657
+ writer.writeInt32(WsMessageType.Transaction);
12658
+ // Update and write transaction ID
12659
+ const transactionId = ++this._messageId;
12660
+ writer.writeInt32(transactionId);
12661
+ // Write room hashes where messages were consumed
12662
+ writer.writeInt32(consumedMessages.size);
12663
+ for (const roomName of consumedMessages) {
12664
+ const subscription = this._subscriptionsByName.get(roomName);
12665
+ if (!subscription) {
12666
+ throw new Error(`Cannot consume message in "${roomName}" because it hasn't been joined`);
12667
+ }
12668
+ writer.writeUint32(subscription.roomHash);
12669
+ }
12670
+ // Write updates
12671
+ for (const { roomName, update } of updates) {
12672
+ const subscription = this._subscriptionsByName.get(roomName);
12673
+ if (!subscription) {
12674
+ throw new Error(`Cannot send update to "${roomName}" because it hasn't been joined`);
12675
+ }
12676
+ writer.writeUint32(subscription.roomHash);
12677
+ writer.writeUint8Array(update);
12678
+ }
12679
+ const buffer = writer.getBuffer();
12680
+ // Wait for server to apply transaction
12681
+ await new Promise((resolve, reject) => {
12682
+ this._transactionPromises.set(transactionId, { resolve, reject });
12683
+ // Send update to server
12684
+ try {
12685
+ this.ws.send(buffer);
12686
+ }
12687
+ catch (e) {
12688
+ // Not connected
12689
+ console.log("Failed to send update to server:", e);
12690
+ // TODO: merge updates or something
12691
+ throw e;
12692
+ }
12693
+ });
12563
12694
  }
12564
- async setData(data) {
12565
- this._data = data;
12566
- const kmClients = Array.from(this._kmClients);
12567
- await Promise.all(kmClients.map(async (client) => {
12568
- await client.transact((t) => {
12569
- t.set(this.root[client.connectionId].data, this._data);
12570
- });
12571
- }));
12695
+ async close() {
12696
+ this._autoReconnect = false;
12697
+ if (this._ws) {
12698
+ this._ws.close();
12699
+ }
12700
+ if (this._pingInterval) {
12701
+ clearInterval(this._pingInterval);
12702
+ }
12703
+ }
12704
+ // Get room hash value for store
12705
+ getRoomHash(store) {
12706
+ const subscription = this._subscriptionsByName.get(store.roomName);
12707
+ if (!subscription) {
12708
+ throw new Error(`Store "${store.roomName}" not joined`);
12709
+ }
12710
+ return subscription.roomHash;
12711
+ }
12712
+ /** Initializers */
12713
+ // store
12714
+ store(name, schema, autoJoin = true) {
12715
+ const store = new KokimokiStore(name, schema);
12716
+ if (autoJoin) {
12717
+ this.join(store)
12718
+ .then(() => { })
12719
+ .catch(() => { });
12720
+ }
12721
+ return store;
12722
+ }
12723
+ // queue
12724
+ queue(name, schema, mode, autoJoin = true) {
12725
+ const queue = new KokimokiQueue(name, schema, mode);
12726
+ if (autoJoin) {
12727
+ this.join(queue)
12728
+ .then(() => { })
12729
+ .catch(() => { });
12730
+ }
12731
+ return queue;
12732
+ }
12733
+ // awareness
12734
+ awareness(name, dataSchema, initialData = dataSchema.defaultValue, autoJoin = true) {
12735
+ const awareness = new KokimokiAwareness(name, dataSchema, initialData);
12736
+ if (autoJoin) {
12737
+ this.join(awareness)
12738
+ .then(() => { })
12739
+ .catch(() => { });
12740
+ }
12741
+ return awareness;
12742
+ }
12743
+ // req-res
12744
+ reqRes(serviceName, reqSchema, resSchema, handleRequest) {
12745
+ return new KokimokiReqRes(this, serviceName, reqSchema, resSchema, handleRequest);
12572
12746
  }
12573
12747
  }
12574
12748