@inditextech/weave-store-azure-web-pubsub 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WeaveStore } from "@inditextech/weave-sdk";
2
- import { WeaveStoreOptions } from "@inditextech/weave-types";
2
+ import { DeepPartial, WeaveStoreOptions } from "@inditextech/weave-types";
3
3
  import ReconnectingWebSocket from "reconnecting-websocket";
4
4
  import { Doc } from "yjs";
5
5
  import Emittery from "emittery";
@@ -100,13 +100,13 @@ declare const WEAVE_STORE_AZURE_WEB_PUBSUB_DEFAULT_CONFIG: {
100
100
  //#region src/types.d.ts
101
101
  type WeaveStoreAzureWebPubsubConfig = {
102
102
  endpoint: string;
103
+ persistIntervalMs?: number;
103
104
  hubName: string;
104
105
  auth?: {
105
106
  key?: string;
106
107
  custom?: TokenCredential;
107
108
  };
108
- connectionHandlers?: Pick<WeaveAzureWebPubsubSyncHandlerOptions, "onConnect" | "onConnected" | "removeConnection" | "getConnectionRoom" | "getRoomConnections">;
109
- persistIntervalMs?: number;
109
+ connectionHandlers?: DeepPartial<WeaveAzureWebPubsubSyncHandlerOptions>;
110
110
  };
111
111
  type WeaveAzureWebPubsubSyncHandlerOptions = {
112
112
  onConnect?: (connectionId: string, queries: Record<string, string[]> | undefined) => Promise<void>;
package/dist/server.d.ts CHANGED
@@ -9,7 +9,7 @@ import ReconnectingWebSocket from "reconnecting-websocket";
9
9
  import * as awarenessProtocol$1 from "y-protocols/awareness";
10
10
  import * as awarenessProtocol from "y-protocols/awareness";
11
11
  import { WeaveStore } from "@inditextech/weave-sdk";
12
- import { WeaveStoreOptions } from "@inditextech/weave-types";
12
+ import { DeepPartial, WeaveStoreOptions } from "@inditextech/weave-types";
13
13
  import { Encoder } from "lib0/encoding";
14
14
  import { Decoder } from "lib0/decoding";
15
15
  import express from "express-serve-static-core";
@@ -108,13 +108,13 @@ declare class WeaveStoreAzureWebPubSubSyncClient extends Emittery {
108
108
  //#region src/types.d.ts
109
109
  type WeaveStoreAzureWebPubsubConfig = {
110
110
  endpoint: string;
111
+ persistIntervalMs?: number;
111
112
  hubName: string;
112
113
  auth?: {
113
114
  key?: string;
114
115
  custom?: TokenCredential;
115
116
  };
116
- connectionHandlers?: Pick<WeaveAzureWebPubsubSyncHandlerOptions, "onConnect" | "onConnected" | "removeConnection" | "getConnectionRoom" | "getRoomConnections">;
117
- persistIntervalMs?: number;
117
+ connectionHandlers?: DeepPartial<WeaveAzureWebPubsubSyncHandlerOptions>;
118
118
  };
119
119
  type WeaveAzureWebPubsubSyncHandlerOptions = {
120
120
  onConnect?: (connectionId: string, queries: Record<string, string[]> | undefined) => Promise<void>;
@@ -1000,6 +1000,7 @@ declare class WebPubSubEventHandler {
1000
1000
  //#region src/server/azure-web-pubsub-host.d.ts
1001
1001
  declare class WeaveStoreAzureWebPubSubSyncHost {
1002
1002
  private readonly server;
1003
+ private readonly syncHandler;
1003
1004
  doc: Y.Doc;
1004
1005
  topic: string;
1005
1006
  topicAwarenessChannel: string;
@@ -1010,7 +1011,7 @@ declare class WeaveStoreAzureWebPubSubSyncHost {
1010
1011
  private _awareness;
1011
1012
  private _updateHandler;
1012
1013
  private _awarenessUpdateHandler;
1013
- constructor(server: WeaveAzureWebPubsubServer, client: WebPubSubServiceClient, topic: string, doc: Y.Doc);
1014
+ constructor(server: WeaveAzureWebPubsubServer, syncHandler: WeaveAzureWebPubsubSyncHandler, client: WebPubSubServiceClient, topic: string, doc: Y.Doc);
1014
1015
  get awareness(): awarenessProtocol.Awareness;
1015
1016
  sendInitAwarenessInfo(origin: string): void;
1016
1017
  createWebSocket(): Promise<void>;
@@ -1036,11 +1037,13 @@ declare class WeaveAzureWebPubsubSyncHandler extends WebPubSubEventHandler {
1036
1037
  private readonly syncOptions?;
1037
1038
  private initialState;
1038
1039
  private readonly server;
1040
+ private readonly roomsLastState;
1039
1041
  constructor(hub: string, server: WeaveAzureWebPubsubServer, client: WebPubSubServiceClient, initialState: FetchInitialState, syncHandlerOptions?: WeaveAzureWebPubsubSyncHandlerOptions, eventHandlerOptions?: WebPubSubEventHandlerOptions);
1040
1042
  private getNewYDoc;
1041
1043
  private setupRoomInstance;
1042
- private persistRoomTask;
1044
+ isPersistingOnInterval(): boolean;
1043
1045
  private setupRoomInstancePersistence;
1046
+ persistRoomTask(roomId: string): Promise<void>;
1044
1047
  private handleConnectionDisconnection;
1045
1048
  destroyRoomInstance(roomId: string): Promise<void>;
1046
1049
  private getHostConnection;
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import { URL as URL$1 } from "node:url";
8
8
  import { EOL } from "node:os";
9
9
  import process$1 from "node:process";
10
10
  import { WebSocket } from "ws";
11
+ import crypto from "node:crypto";
11
12
  import { defaultInitialState } from "@inditextech/weave-sdk/server";
12
13
 
13
14
  //#region rolldown:runtime
@@ -18609,6 +18610,12 @@ var require_response = __commonJS({ "../../node_modules/koa/lib/response.js"(exp
18609
18610
  set body(val) {
18610
18611
  const original = this._body;
18611
18612
  this._body = val;
18613
+ const cleanupPreviousStream = () => {
18614
+ if (original && isStream$1(original)) {
18615
+ original.once("error", () => {});
18616
+ if (!isStream$1(val)) destroy$1(original);
18617
+ }
18618
+ };
18612
18619
  if (val == null) {
18613
18620
  if (!statuses$3.empty[this.status]) {
18614
18621
  if (this.type === "application/json") {
@@ -18621,6 +18628,7 @@ var require_response = __commonJS({ "../../node_modules/koa/lib/response.js"(exp
18621
18628
  this.remove("Content-Type");
18622
18629
  this.remove("Content-Length");
18623
18630
  this.remove("Transfer-Encoding");
18631
+ cleanupPreviousStream();
18624
18632
  return;
18625
18633
  }
18626
18634
  if (!this._explicitStatus) this.status = 200;
@@ -18628,29 +18636,33 @@ var require_response = __commonJS({ "../../node_modules/koa/lib/response.js"(exp
18628
18636
  if (typeof val === "string") {
18629
18637
  if (setType) this.type = /^\s*</.test(val) ? "html" : "text";
18630
18638
  this.length = Buffer.byteLength(val);
18639
+ cleanupPreviousStream();
18631
18640
  return;
18632
18641
  }
18633
18642
  if (Buffer.isBuffer(val)) {
18634
18643
  if (setType) this.type = "bin";
18635
18644
  this.length = val.length;
18645
+ cleanupPreviousStream();
18636
18646
  return;
18637
18647
  }
18638
18648
  if (isStream$1(val)) {
18639
18649
  onFinish(this.res, destroy$1.bind(null, val));
18640
18650
  if (original !== val) {
18641
- val.once("error", (err) => this.ctx.onerror(err));
18642
18651
  if (original != null) this.remove("Content-Length");
18652
+ cleanupPreviousStream();
18643
18653
  }
18644
18654
  if (setType) this.type = "bin";
18645
18655
  return;
18646
18656
  }
18647
18657
  if (val instanceof ReadableStream) {
18648
18658
  if (setType) this.type = "bin";
18659
+ cleanupPreviousStream();
18649
18660
  return;
18650
18661
  }
18651
18662
  if (val instanceof Blob) {
18652
18663
  if (setType) this.type = "bin";
18653
18664
  this.length = val.size;
18665
+ cleanupPreviousStream();
18654
18666
  return;
18655
18667
  }
18656
18668
  if (val instanceof Response) {
@@ -18658,10 +18670,12 @@ var require_response = __commonJS({ "../../node_modules/koa/lib/response.js"(exp
18658
18670
  if (setType) this.type = "bin";
18659
18671
  const headers = val.headers;
18660
18672
  for (const key of headers.keys()) this.set(key, headers.get(key));
18673
+ cleanupPreviousStream();
18661
18674
  return;
18662
18675
  }
18663
18676
  this.remove("Content-Length");
18664
18677
  if (!this.type || !/\bjson\b/i.test(this.type)) this.type = "json";
18678
+ cleanupPreviousStream();
18665
18679
  },
18666
18680
  set length(n) {
18667
18681
  if (!this.has("Transfer-Encoding")) this.set("Content-Length", n);
@@ -19818,19 +19832,19 @@ var require_delegates = __commonJS({ "../../node_modules/delegates/index.js"(exp
19818
19832
  //#endregion
19819
19833
  //#region ../../node_modules/tsscmp/lib/index.js
19820
19834
  var require_lib = __commonJS({ "../../node_modules/tsscmp/lib/index.js"(exports, module) {
19821
- var crypto$1 = __require("crypto");
19835
+ var crypto$2 = __require("crypto");
19822
19836
  function bufferEqual(a, b) {
19823
19837
  if (a.length !== b.length) return false;
19824
- if (crypto$1.timingSafeEqual) return crypto$1.timingSafeEqual(a, b);
19838
+ if (crypto$2.timingSafeEqual) return crypto$2.timingSafeEqual(a, b);
19825
19839
  for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
19826
19840
  return true;
19827
19841
  }
19828
19842
  function timeSafeCompare(a, b) {
19829
19843
  var sa = String(a);
19830
19844
  var sb = String(b);
19831
- var key = crypto$1.pseudoRandomBytes(32);
19832
- var ah = crypto$1.createHmac("sha256", key).update(sa).digest();
19833
- var bh = crypto$1.createHmac("sha256", key).update(sb).digest();
19845
+ var key = crypto$2.pseudoRandomBytes(32);
19846
+ var ah = crypto$2.createHmac("sha256", key).update(sa).digest();
19847
+ var bh = crypto$2.createHmac("sha256", key).update(sb).digest();
19834
19848
  return bufferEqual(ah, bh) && a === b;
19835
19849
  }
19836
19850
  module.exports = timeSafeCompare;
@@ -19840,14 +19854,14 @@ var require_lib = __commonJS({ "../../node_modules/tsscmp/lib/index.js"(exports,
19840
19854
  //#region ../../node_modules/keygrip/index.js
19841
19855
  var require_keygrip = __commonJS({ "../../node_modules/keygrip/index.js"(exports, module) {
19842
19856
  var compare = require_lib();
19843
- var crypto = __require("crypto");
19857
+ var crypto$1 = __require("crypto");
19844
19858
  function Keygrip$1(keys$1, algorithm, encoding) {
19845
19859
  if (!algorithm) algorithm = "sha1";
19846
19860
  if (!encoding) encoding = "base64";
19847
19861
  if (!(this instanceof Keygrip$1)) return new Keygrip$1(keys$1, algorithm, encoding);
19848
19862
  if (!keys$1 || !(0 in keys$1)) throw new Error("Keys must be provided.");
19849
19863
  function sign(data, key) {
19850
- return crypto.createHmac(algorithm, key).update(data).digest(encoding).replace(/\/|\+|=/g, function(x) {
19864
+ return crypto$1.createHmac(algorithm, key).update(data).digest(encoding).replace(/\/|\+|=/g, function(x) {
19851
19865
  return {
19852
19866
  "/": "_",
19853
19867
  "+": "-",
@@ -20389,10 +20403,14 @@ var require_application = __commonJS({ "../../node_modules/koa/lib/application.j
20389
20403
  }
20390
20404
  if (Buffer.isBuffer(body)) return res.end(body);
20391
20405
  if (typeof body === "string") return res.end(body);
20392
- if (body instanceof Blob) return Stream.Readable.from(body.stream()).pipe(res);
20393
- if (body instanceof ReadableStream) return Stream.Readable.from(body).pipe(res);
20394
- if (body instanceof Response) return Stream.Readable.from(body?.body || "").pipe(res);
20395
- if (isStream(body)) return body.pipe(res);
20406
+ let stream = null;
20407
+ if (body instanceof Blob) stream = Stream.Readable.from(body.stream());
20408
+ else if (body instanceof ReadableStream) stream = Stream.Readable.from(body);
20409
+ else if (body instanceof Response) stream = Stream.Readable.from(body?.body || "");
20410
+ else if (isStream(body)) stream = body;
20411
+ if (stream) return Stream.pipeline(stream, res, (err) => {
20412
+ if (err && ctx.app.listenerCount("error")) ctx.onerror(err);
20413
+ });
20396
20414
  body = JSON.stringify(body);
20397
20415
  if (!res.headersSent) ctx.length = Buffer.byteLength(body);
20398
20416
  res.end(body);
@@ -22840,8 +22858,9 @@ const HostUserId = "host";
22840
22858
  var WeaveStoreAzureWebPubSubSyncHost = class {
22841
22859
  _reconnectAttempts = 0;
22842
22860
  _forceClose = false;
22843
- constructor(server, client, topic, doc) {
22861
+ constructor(server, syncHandler, client, topic, doc) {
22844
22862
  this.server = server;
22863
+ this.syncHandler = syncHandler;
22845
22864
  this.doc = doc;
22846
22865
  this.topic = topic;
22847
22866
  this.topicAwarenessChannel = `${topic}-awareness`;
@@ -22869,6 +22888,7 @@ var WeaveStoreAzureWebPubSubSyncHost = class {
22869
22888
  writeUpdate(encoder, update);
22870
22889
  const u8 = toUint8Array(encoder);
22871
22890
  this.broadcast(this.topic, origin, u8);
22891
+ if (!this.syncHandler.isPersistingOnInterval()) this.syncHandler.persistRoomTask(this.topic);
22872
22892
  } catch (err) {
22873
22893
  console.error("Error in document update handler:", err);
22874
22894
  }
@@ -23061,12 +23081,27 @@ var WeaveStoreAzureWebPubSubSyncHost = class {
23061
23081
  }
23062
23082
  };
23063
23083
 
23084
+ //#endregion
23085
+ //#region src/server/utils.ts
23086
+ function getStateAsJson(actualState) {
23087
+ const document = new yjs_default.Doc();
23088
+ yjs_default.applyUpdate(document, actualState);
23089
+ const actualStateString = JSON.stringify(document.getMap("weave").toJSON());
23090
+ const actualStateJson = JSON.parse(actualStateString);
23091
+ return actualStateJson;
23092
+ }
23093
+ function hashJson(obj) {
23094
+ const jsonString = JSON.stringify(obj);
23095
+ return crypto.createHash("sha256").update(jsonString).digest("hex");
23096
+ }
23097
+
23064
23098
  //#endregion
23065
23099
  //#region src/server/azure-web-pubsub-sync-handler.ts
23066
23100
  var WeaveAzureWebPubsubSyncHandler = class extends WebPubSubEventHandler {
23067
23101
  _rooms = new Map();
23068
23102
  _roomsSyncHost = new Map();
23069
23103
  _store_persistence = new Map();
23104
+ roomsLastState = new Map();
23070
23105
  constructor(hub, server, client, initialState, syncHandlerOptions, eventHandlerOptions) {
23071
23106
  super(hub, {
23072
23107
  ...eventHandlerOptions,
@@ -23091,6 +23126,7 @@ var WeaveAzureWebPubsubSyncHandler = class extends WebPubSubEventHandler {
23091
23126
  this.server = server;
23092
23127
  this.initialState = initialState;
23093
23128
  this._client = client;
23129
+ if (this.isPersistingOnInterval()) console.warn(`Room persistence defined via interval, be aware that this can lead to data loss. Consider persisting on document updates instead.`);
23094
23130
  }
23095
23131
  getNewYDoc() {
23096
23132
  return new yjs_default.Doc();
@@ -23102,29 +23138,44 @@ var WeaveAzureWebPubsubSyncHandler = class extends WebPubSubEventHandler {
23102
23138
  if (this.server?.fetchRoom) documentData = await this.server.fetchRoom(roomId);
23103
23139
  if (documentData) yjs_default.applyUpdate(doc, documentData);
23104
23140
  else this.initialState(doc);
23105
- this._roomsSyncHost.set(roomId, new WeaveStoreAzureWebPubSubSyncHost(this.server, this._client, roomId, doc));
23141
+ this._roomsSyncHost.set(roomId, new WeaveStoreAzureWebPubSubSyncHost(this.server, this, this._client, roomId, doc));
23106
23142
  const connection = this._roomsSyncHost.get(roomId);
23107
23143
  await connection.start();
23108
- await this.setupRoomInstancePersistence(roomId);
23144
+ if (this.isPersistingOnInterval()) this.setupRoomInstancePersistence(roomId);
23145
+ }
23146
+ isPersistingOnInterval() {
23147
+ return this.syncOptions?.persistIntervalMs !== void 0;
23148
+ }
23149
+ async setupRoomInstancePersistence(roomId) {
23150
+ if (!this._store_persistence.has(roomId)) {
23151
+ const intervalId = setInterval(async () => {
23152
+ await this.persistRoomTask(roomId);
23153
+ }, this.syncOptions?.persistIntervalMs ?? 5e3);
23154
+ this._store_persistence.set(roomId, intervalId);
23155
+ }
23109
23156
  }
23110
23157
  async persistRoomTask(roomId) {
23111
23158
  try {
23112
23159
  const doc = this._rooms.get(roomId);
23113
23160
  if (!doc) return;
23114
23161
  const actualState = yjs_default.encodeStateAsUpdate(doc);
23162
+ if (!this.isPersistingOnInterval()) {
23163
+ const savedRoomData = this.roomsLastState.get(roomId);
23164
+ if (savedRoomData) {
23165
+ const savedStateJson = getStateAsJson(savedRoomData);
23166
+ const savedHash = hashJson(savedStateJson);
23167
+ const actualStateJson = getStateAsJson(actualState);
23168
+ const actualHash = hashJson(actualStateJson);
23169
+ const same = savedHash === actualHash;
23170
+ if (same) return;
23171
+ this.roomsLastState.set(roomId, actualState);
23172
+ }
23173
+ }
23115
23174
  if (this.server?.persistRoom) await this.server.persistRoom(roomId, actualState);
23116
23175
  } catch (ex) {
23117
23176
  console.error(ex);
23118
23177
  }
23119
23178
  }
23120
- async setupRoomInstancePersistence(roomId) {
23121
- if (!this._store_persistence.has(roomId)) {
23122
- const intervalId = setInterval(async () => {
23123
- await this.persistRoomTask(roomId);
23124
- }, this.syncOptions?.persistIntervalMs ?? 5e3);
23125
- this._store_persistence.set(roomId, intervalId);
23126
- }
23127
- }
23128
23179
  async handleConnectionDisconnection(connectionId) {
23129
23180
  const connectionRoom = await this.syncOptions?.getConnectionRoom?.(connectionId);
23130
23181
  if (connectionRoom) await this.syncOptions?.removeConnection?.(connectionId);
@@ -23132,10 +23183,13 @@ var WeaveAzureWebPubsubSyncHandler = class extends WebPubSubEventHandler {
23132
23183
  if (connectionRoom && roomConnections?.length === 0) await this.destroyRoomInstance(connectionRoom);
23133
23184
  }
23134
23185
  async destroyRoomInstance(roomId) {
23135
- const intervalId = this._store_persistence.get(roomId);
23136
- if (intervalId) clearInterval(intervalId);
23137
- this._store_persistence.delete(roomId);
23186
+ if (this.isPersistingOnInterval()) {
23187
+ const intervalId = this._store_persistence.get(roomId);
23188
+ if (intervalId) clearInterval(intervalId);
23189
+ this._store_persistence.delete(roomId);
23190
+ }
23138
23191
  await this.persistRoomTask(roomId);
23192
+ if (!this.isPersistingOnInterval()) this.roomsLastState.delete(roomId);
23139
23193
  const syncHost = this._roomsSyncHost.get(roomId);
23140
23194
  if (syncHost) {
23141
23195
  await syncHost.stop();
@@ -23182,7 +23236,7 @@ var WeaveAzureWebPubsubServer = class extends Emittery {
23182
23236
  credentials ??= new DefaultAzureCredential();
23183
23237
  this.syncClient = new WebPubSubServiceClient(pubSubConfig.endpoint, credentials, pubSubConfig.hubName);
23184
23238
  this.syncHandler = new WeaveAzureWebPubsubSyncHandler(pubSubConfig.hubName, this, this.syncClient, initialState, {
23185
- persistIntervalMs: pubSubConfig.persistIntervalMs,
23239
+ ...pubSubConfig.persistIntervalMs && { persistIntervalMs: pubSubConfig.persistIntervalMs },
23186
23240
  ...pubSubConfig.connectionHandlers
23187
23241
  }, eventsHandlerConfig);
23188
23242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inditextech/weave-store-azure-web-pubsub",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Jesus Manuel Piñeiro Cid <jesusmpc@inditex.com>",
@@ -57,8 +57,8 @@
57
57
  "dependencies": {
58
58
  "@azure/identity": "4.10.2",
59
59
  "@azure/web-pubsub": "1.2.0",
60
- "@inditextech/weave-types": "2.0.3",
61
- "@inditextech/weave-sdk": "2.0.3",
60
+ "@inditextech/weave-types": "2.1.1",
61
+ "@inditextech/weave-sdk": "2.1.1",
62
62
  "@syncedstore/core": "0.6.0",
63
63
  "buffer": "6.0.3",
64
64
  "reconnecting-websocket": "4.4.0",