@kadoa/node-sdk 0.24.0 → 0.24.2

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.mjs CHANGED
@@ -6035,7 +6035,7 @@ var NotificationSetupService = class {
6035
6035
  (s) => s.eventType === eventType
6036
6036
  );
6037
6037
  if (existing?.id) {
6038
- const existingChannelIds = (existing.channels || []).map((c) => c.id).filter(Boolean);
6038
+ const existingChannelIds = (existing.channels || []).map((channel) => channel.id).filter(Boolean);
6039
6039
  const mergedChannelIds = [
6040
6040
  .../* @__PURE__ */ new Set([...existingChannelIds, ...channelIds])
6041
6041
  ];
@@ -6201,7 +6201,7 @@ process.env.KADOA_WSS_NEO_API_URI ?? "wss://events.kadoa.com/events/ws";
6201
6201
  var REALTIME_API_URI = process.env.KADOA_REALTIME_API_URI ?? "https://realtime.kadoa.com";
6202
6202
 
6203
6203
  // src/version.ts
6204
- var SDK_VERSION = "0.24.0";
6204
+ var SDK_VERSION = "0.24.2";
6205
6205
  var SDK_NAME = "kadoa-node-sdk";
6206
6206
  var SDK_LANGUAGE = "node";
6207
6207
 
@@ -6210,89 +6210,256 @@ var debug5 = logger.wss;
6210
6210
  if (typeof WebSocket === "undefined") {
6211
6211
  global.WebSocket = __require("ws");
6212
6212
  }
6213
- var Realtime = class {
6213
+ var isDrainControlMessage = (message) => message.type === "control.draining";
6214
+ var isRealtimeEvent = (message) => message.type !== "heartbeat" && message.type !== "control.draining";
6215
+ var _Realtime = class _Realtime {
6214
6216
  constructor(config) {
6217
+ this.drainingSockets = /* @__PURE__ */ new Set();
6215
6218
  this.lastHeartbeat = Date.now();
6216
6219
  this.isConnecting = false;
6217
6220
  this.eventListeners = /* @__PURE__ */ new Set();
6218
6221
  this.connectionListeners = /* @__PURE__ */ new Set();
6219
6222
  this.errorListeners = /* @__PURE__ */ new Set();
6223
+ this.isClosed = false;
6224
+ this.hasConnectedOnce = false;
6225
+ this.recentEventIds = /* @__PURE__ */ new Set();
6226
+ this.recentEventIdQueue = [];
6227
+ this.maxRecentEventIds = 1e3;
6220
6228
  this.apiKey = config.apiKey;
6221
6229
  this.heartbeatInterval = config.heartbeatInterval || 1e4;
6222
- this.reconnectDelay = config.reconnectDelay || 5e3;
6230
+ this.reconnectDelay = this.normalizeReconnectDelay(config.reconnectDelay);
6223
6231
  this.missedHeartbeatsLimit = config.missedHeartbeatsLimit || 3e4;
6224
6232
  }
6225
6233
  async connect() {
6226
- if (this.isConnecting) return;
6234
+ if (this.isClosed || this.isConnecting || this.activeSocket) {
6235
+ return;
6236
+ }
6227
6237
  this.isConnecting = true;
6228
6238
  try {
6229
- const response = await fetch(`${PUBLIC_API_URI}/v4/oauth2/token`, {
6230
- method: "POST",
6231
- headers: {
6232
- "Content-Type": "application/json",
6233
- "x-api-key": `${this.apiKey}`,
6234
- "x-sdk-version": SDK_VERSION
6239
+ const { access_token, team_id } = await this.getOAuthToken();
6240
+ await this.openSocket(access_token, team_id, "active");
6241
+ this.hasConnectedOnce = true;
6242
+ } catch (err) {
6243
+ debug5("Failed to connect: %O", err);
6244
+ this.isConnecting = false;
6245
+ this.notifyErrorListeners(err);
6246
+ if (!this.hasConnectedOnce) {
6247
+ throw err;
6248
+ }
6249
+ this.scheduleReconnect();
6250
+ }
6251
+ }
6252
+ async getOAuthToken() {
6253
+ const response = await fetch(`${PUBLIC_API_URI}/v4/oauth2/token`, {
6254
+ method: "POST",
6255
+ headers: {
6256
+ "Content-Type": "application/json",
6257
+ "x-api-key": `${this.apiKey}`,
6258
+ "x-sdk-version": SDK_VERSION
6259
+ }
6260
+ });
6261
+ return await response.json();
6262
+ }
6263
+ async openSocket(accessToken, teamId, role) {
6264
+ await new Promise((resolve, reject) => {
6265
+ const socket = new WebSocket(
6266
+ `${WSS_API_URI}?access_token=${accessToken}`
6267
+ );
6268
+ let settled = false;
6269
+ socket.onopen = () => {
6270
+ const subscribeMessage = {
6271
+ action: "subscribe",
6272
+ channel: teamId
6273
+ };
6274
+ if (this.lastCursor) {
6275
+ subscribeMessage.lastCursor = this.lastCursor;
6235
6276
  }
6236
- });
6237
- const { access_token, team_id } = await response.json();
6238
- await new Promise((resolve, reject) => {
6239
- this.socket = new WebSocket(
6240
- `${WSS_API_URI}?access_token=${access_token}`
6241
- );
6242
- this.socket.onopen = () => {
6243
- this.isConnecting = false;
6244
- this.lastHeartbeat = Date.now();
6245
- if (this.socket?.readyState === WebSocket.OPEN) {
6246
- this.socket.send(
6247
- JSON.stringify({
6248
- action: "subscribe",
6249
- channel: team_id
6250
- })
6251
- );
6252
- debug5("Connected to WebSocket");
6253
- this.notifyConnectionListeners(true);
6254
- }
6255
- this.startHeartbeatCheck();
6277
+ socket.send(JSON.stringify(subscribeMessage));
6278
+ this.promoteSocket(socket, role);
6279
+ this.isConnecting = false;
6280
+ this.lastHeartbeat = Date.now();
6281
+ this.startHeartbeatCheck();
6282
+ debug5("Connected to WebSocket");
6283
+ if (!settled) {
6284
+ settled = true;
6256
6285
  resolve();
6257
- };
6258
- this.socket.onmessage = (event) => {
6259
- try {
6260
- const data = JSON.parse(event.data);
6261
- if (data.type === "heartbeat") {
6262
- this.handleHeartbeat();
6263
- } else {
6264
- if (data?.id) {
6265
- fetch(`${REALTIME_API_URI}/api/v1/events/ack`, {
6266
- method: "POST",
6267
- headers: { "Content-Type": "application/json" },
6268
- body: JSON.stringify({ id: data.id })
6269
- });
6270
- }
6271
- this.notifyEventListeners(data);
6272
- }
6273
- } catch (err) {
6274
- debug5("Failed to parse incoming message: %O", err);
6275
- }
6276
- };
6277
- this.socket.onclose = () => {
6278
- debug5("WebSocket disconnected. Attempting to reconnect...");
6279
- this.isConnecting = false;
6280
- this.stopHeartbeatCheck();
6281
- this.notifyConnectionListeners(false, "Connection closed");
6282
- setTimeout(() => this.connect(), this.reconnectDelay);
6283
- };
6284
- this.socket.onerror = (error) => {
6285
- debug5("WebSocket error: %O", error);
6286
- this.isConnecting = false;
6287
- this.notifyErrorListeners(error);
6286
+ }
6287
+ };
6288
+ socket.onmessage = (event) => {
6289
+ this.handleSocketMessage(socket, event.data);
6290
+ };
6291
+ socket.onclose = () => {
6292
+ this.handleSocketClose(socket);
6293
+ if (!settled) {
6294
+ settled = true;
6295
+ reject(new Error("WebSocket closed before opening"));
6296
+ }
6297
+ };
6298
+ socket.onerror = (error) => {
6299
+ this.notifyErrorListeners(error);
6300
+ if (!settled) {
6301
+ settled = true;
6288
6302
  reject(error);
6289
- };
6290
- });
6303
+ return;
6304
+ }
6305
+ if (socket === this.activeSocket) {
6306
+ this.handleUnexpectedDisconnect("Socket error");
6307
+ }
6308
+ };
6309
+ });
6310
+ }
6311
+ promoteSocket(socket, role) {
6312
+ if (role === "replacement" && this.activeSocket && this.activeSocket !== socket) {
6313
+ this.drainingSockets.add(this.activeSocket);
6314
+ }
6315
+ this.activeSocket = socket;
6316
+ this.drainingSockets.delete(socket);
6317
+ if (role === "active" || !this.hasConnectedOnce) {
6318
+ this.notifyConnectionListeners(true);
6319
+ }
6320
+ }
6321
+ handleSocketMessage(socket, rawData) {
6322
+ try {
6323
+ const payload = typeof rawData === "string" ? rawData : rawData.toString?.() ?? "";
6324
+ const data = JSON.parse(payload);
6325
+ if (data.type === "heartbeat") {
6326
+ if (socket === this.activeSocket) {
6327
+ this.handleHeartbeat();
6328
+ }
6329
+ return;
6330
+ }
6331
+ if (isDrainControlMessage(data)) {
6332
+ this.handleDrainSignal(socket, data);
6333
+ return;
6334
+ }
6335
+ if (!isRealtimeEvent(data)) {
6336
+ return;
6337
+ }
6338
+ if (typeof data._cursor === "string") {
6339
+ this.lastCursor = data._cursor;
6340
+ }
6341
+ if (typeof data.id === "string") {
6342
+ fetch(`${REALTIME_API_URI}/api/v1/events/ack`, {
6343
+ method: "POST",
6344
+ headers: { "Content-Type": "application/json" },
6345
+ body: JSON.stringify({ id: data.id })
6346
+ }).catch((error) => {
6347
+ debug5("Failed to acknowledge event %s: %O", data.id, error);
6348
+ });
6349
+ }
6350
+ if (this.isDuplicateEvent(data.id)) {
6351
+ return;
6352
+ }
6353
+ this.notifyEventListeners(data);
6291
6354
  } catch (err) {
6292
- debug5("Failed to connect: %O", err);
6293
- this.isConnecting = false;
6294
- setTimeout(() => this.connect(), this.reconnectDelay);
6355
+ debug5("Failed to parse incoming message: %O", err);
6356
+ }
6357
+ }
6358
+ handleDrainSignal(socket, message) {
6359
+ if (socket !== this.activeSocket || this.isClosed) {
6360
+ return;
6361
+ }
6362
+ debug5("Received drain signal, preparing replacement socket");
6363
+ this.drainingSockets.add(socket);
6364
+ this.scheduleDrainReconnect(message.retryAfterMs);
6365
+ }
6366
+ handleSocketClose(socket) {
6367
+ const wasActiveSocket = socket === this.activeSocket;
6368
+ this.drainingSockets.delete(socket);
6369
+ if (!wasActiveSocket) {
6370
+ return;
6371
+ }
6372
+ this.activeSocket = void 0;
6373
+ this.stopHeartbeatCheck();
6374
+ if (this.isClosed) {
6375
+ return;
6376
+ }
6377
+ if (this.drainingSockets.size > 0) {
6378
+ debug5("Draining socket closed after replacement was scheduled");
6379
+ return;
6380
+ }
6381
+ this.handleUnexpectedDisconnect("Connection closed");
6382
+ }
6383
+ handleUnexpectedDisconnect(reason) {
6384
+ this.isConnecting = false;
6385
+ this.notifyConnectionListeners(false, reason);
6386
+ this.scheduleReconnect();
6387
+ }
6388
+ scheduleReconnect(replacement = false) {
6389
+ if (this.isClosed || this.reconnectTimer) {
6390
+ return;
6391
+ }
6392
+ this.reconnectTimer = setTimeout(async () => {
6393
+ this.reconnectTimer = void 0;
6394
+ if (this.isClosed || this.isConnecting || !replacement && this.activeSocket) {
6395
+ return;
6396
+ }
6397
+ this.isConnecting = true;
6398
+ try {
6399
+ const { access_token, team_id } = await this.getOAuthToken();
6400
+ await this.openSocket(
6401
+ access_token,
6402
+ team_id,
6403
+ replacement ? "replacement" : "active"
6404
+ );
6405
+ } catch (err) {
6406
+ debug5("Reconnect failed: %O", err);
6407
+ this.isConnecting = false;
6408
+ this.notifyErrorListeners(err);
6409
+ this.scheduleReconnect(replacement);
6410
+ }
6411
+ }, this.reconnectDelay);
6412
+ }
6413
+ scheduleDrainReconnect(retryAfterMs) {
6414
+ if (this.isClosed || this.reconnectTimer) {
6415
+ return;
6295
6416
  }
6417
+ let safeDelayMs = this.reconnectDelay;
6418
+ if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 && retryAfterMs <= _Realtime.MAX_RECONNECT_DELAY_MS) {
6419
+ safeDelayMs = Math.trunc(retryAfterMs);
6420
+ }
6421
+ this.reconnectTimer = setTimeout(async () => {
6422
+ this.reconnectTimer = void 0;
6423
+ if (this.isClosed || this.isConnecting) {
6424
+ return;
6425
+ }
6426
+ this.isConnecting = true;
6427
+ try {
6428
+ const { access_token, team_id } = await this.getOAuthToken();
6429
+ await this.openSocket(access_token, team_id, "replacement");
6430
+ } catch (err) {
6431
+ debug5("Reconnect failed: %O", err);
6432
+ this.isConnecting = false;
6433
+ this.notifyErrorListeners(err);
6434
+ this.scheduleReconnect(true);
6435
+ }
6436
+ }, safeDelayMs);
6437
+ }
6438
+ normalizeReconnectDelay(delay) {
6439
+ if (typeof delay !== "number" || !Number.isFinite(delay)) {
6440
+ return _Realtime.DEFAULT_RECONNECT_DELAY_MS;
6441
+ }
6442
+ return Math.min(
6443
+ Math.max(0, Math.trunc(delay)),
6444
+ _Realtime.MAX_RECONNECT_DELAY_MS
6445
+ );
6446
+ }
6447
+ isDuplicateEvent(eventId) {
6448
+ if (!eventId) {
6449
+ return false;
6450
+ }
6451
+ if (this.recentEventIds.has(eventId)) {
6452
+ return true;
6453
+ }
6454
+ this.recentEventIds.add(eventId);
6455
+ this.recentEventIdQueue.push(eventId);
6456
+ if (this.recentEventIdQueue.length > this.maxRecentEventIds) {
6457
+ const expiredId = this.recentEventIdQueue.shift();
6458
+ if (expiredId) {
6459
+ this.recentEventIds.delete(expiredId);
6460
+ }
6461
+ }
6462
+ return false;
6296
6463
  }
6297
6464
  handleHeartbeat() {
6298
6465
  debug5("Heartbeat received");
@@ -6320,22 +6487,24 @@ var Realtime = class {
6320
6487
  this.errorListeners.forEach((listener) => {
6321
6488
  try {
6322
6489
  listener(error);
6323
- } catch (error2) {
6324
- debug5("Error in error listener: %O", error2);
6490
+ } catch (listenerError) {
6491
+ debug5("Error in error listener: %O", listenerError);
6325
6492
  }
6326
6493
  });
6327
6494
  }
6328
6495
  startHeartbeatCheck() {
6496
+ this.stopHeartbeatCheck();
6329
6497
  this.missedHeartbeatCheckTimer = setInterval(() => {
6330
- if (Date.now() - this.lastHeartbeat > this.missedHeartbeatsLimit) {
6498
+ if (this.activeSocket && Date.now() - this.lastHeartbeat > this.missedHeartbeatsLimit) {
6331
6499
  debug5("No heartbeat received in 30 seconds! Closing connection.");
6332
- this.socket?.close();
6500
+ this.activeSocket.close();
6333
6501
  }
6334
6502
  }, this.heartbeatInterval);
6335
6503
  }
6336
6504
  stopHeartbeatCheck() {
6337
6505
  if (this.missedHeartbeatCheckTimer) {
6338
6506
  clearInterval(this.missedHeartbeatCheckTimer);
6507
+ this.missedHeartbeatCheckTimer = void 0;
6339
6508
  }
6340
6509
  }
6341
6510
  /**
@@ -6356,6 +6525,9 @@ var Realtime = class {
6356
6525
  */
6357
6526
  onConnection(listener) {
6358
6527
  this.connectionListeners.add(listener);
6528
+ if (this.isConnected()) {
6529
+ listener(true);
6530
+ }
6359
6531
  return () => {
6360
6532
  this.connectionListeners.delete(listener);
6361
6533
  };
@@ -6372,19 +6544,30 @@ var Realtime = class {
6372
6544
  };
6373
6545
  }
6374
6546
  close() {
6375
- if (this.socket) {
6376
- this.stopHeartbeatCheck();
6377
- this.socket.close();
6378
- this.socket = void 0;
6547
+ this.isClosed = true;
6548
+ if (this.reconnectTimer) {
6549
+ clearTimeout(this.reconnectTimer);
6550
+ this.reconnectTimer = void 0;
6379
6551
  }
6552
+ this.stopHeartbeatCheck();
6553
+ this.activeSocket?.close();
6554
+ this.activeSocket = void 0;
6555
+ this.drainingSockets.forEach((socket) => {
6556
+ socket.close();
6557
+ });
6558
+ this.drainingSockets.clear();
6559
+ this.isConnecting = false;
6380
6560
  this.eventListeners.clear();
6381
6561
  this.connectionListeners.clear();
6382
6562
  this.errorListeners.clear();
6383
6563
  }
6384
6564
  isConnected() {
6385
- return this.socket?.readyState === WebSocket.OPEN;
6565
+ return this.activeSocket?.readyState === WebSocket.OPEN;
6386
6566
  }
6387
6567
  };
6568
+ _Realtime.DEFAULT_RECONNECT_DELAY_MS = 5e3;
6569
+ _Realtime.MAX_RECONNECT_DELAY_MS = 6e4;
6570
+ var Realtime = _Realtime;
6388
6571
 
6389
6572
  // src/domains/user/user.service.ts
6390
6573
  var UserService = class {