@kinotic-ai/core 1.2.2 → 1.3.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/index.cjs CHANGED
@@ -68,6 +68,7 @@ __export(exports_src, {
68
68
  Version: () => Version,
69
69
  TextEventFactory: () => TextEventFactory,
70
70
  Sort: () => Sort,
71
+ SessionKeepAliveMode: () => SessionKeepAliveMode,
71
72
  ServiceRegistry: () => ServiceRegistry,
72
73
  ServerInfo: () => ServerInfo,
73
74
  Scope: () => Scope,
@@ -95,7 +96,6 @@ __export(exports_src, {
95
96
  Context: () => Context,
96
97
  ConnectionInfo: () => ConnectionInfo,
97
98
  ConnectedInfo: () => ConnectedInfo,
98
- ConnectHeaders: () => ConnectHeaders,
99
99
  CONTEXT_METADATA_KEY: () => CONTEXT_METADATA_KEY,
100
100
  AuthorizationError: () => AuthorizationError,
101
101
  AuthenticationError: () => AuthenticationError,
@@ -104,19 +104,22 @@ __export(exports_src, {
104
104
  module.exports = __toCommonJS(exports_src);
105
105
 
106
106
  // packages/core/src/api/ConnectionInfo.ts
107
- class ConnectHeaders {
108
- }
109
-
110
107
  class ServerInfo {
111
108
  host;
112
109
  port;
113
110
  useSSL;
114
111
  }
112
+ var SessionKeepAliveMode;
113
+ ((SessionKeepAliveMode2) => {
114
+ SessionKeepAliveMode2["NONE"] = "NONE";
115
+ SessionKeepAliveMode2["ACTIVITY"] = "ACTIVITY";
116
+ SessionKeepAliveMode2["CONNECTION"] = "CONNECTION";
117
+ })(SessionKeepAliveMode ||= {});
115
118
 
116
119
  class ConnectionInfo extends ServerInfo {
117
- connectHeaders;
120
+ webSocketFactory;
118
121
  maxConnectionAttempts;
119
- disableStickySession;
122
+ sessionKeepAlive = "ACTIVITY" /* ACTIVITY */;
120
123
  }
121
124
  // packages/core/src/api/errors/KinoticError.ts
122
125
  class KinoticError extends Error {
@@ -132,10 +135,8 @@ var EventConstants;
132
135
  EventConstants2["CONTENT_TYPE_HEADER"] = "content-type";
133
136
  EventConstants2["CONTENT_LENGTH_HEADER"] = "content-length";
134
137
  EventConstants2["REPLY_TO_HEADER"] = "reply-to";
135
- EventConstants2["REPLY_TO_ID_HEADER"] = "reply-to-id";
136
- EventConstants2["SESSION_HEADER"] = "session";
137
138
  EventConstants2["CONNECTED_INFO_HEADER"] = "connected-info";
138
- EventConstants2["DISABLE_STICKY_SESSION_HEADER"] = "disable-sticky-session";
139
+ EventConstants2["SESSION_KEEP_ALIVE_HEADER"] = "session-keep-alive";
139
140
  EventConstants2["CORRELATION_ID_HEADER"] = "__correlation-id";
140
141
  EventConstants2["ERROR_HEADER"] = "error";
141
142
  EventConstants2["COMPLETE_HEADER"] = "complete";
@@ -148,6 +149,8 @@ var EventConstants;
148
149
  EventConstants2["SERVICE_DESTINATION_SCHEME"] = "srv";
149
150
  EventConstants2["STREAM_DESTINATION_PREFIX"] = "stream://";
150
151
  EventConstants2["STREAM_DESTINATION_SCHEME"] = "stream";
152
+ EventConstants2["REPLY_DESTINATION_PREFIX"] = "reply://";
153
+ EventConstants2["REPLY_DESTINATION_SCHEME"] = "reply";
151
154
  EventConstants2["CONTENT_JSON"] = "application/json";
152
155
  EventConstants2["CONTENT_TEXT"] = "text/plain";
153
156
  EventConstants2["TRACEPARENT_HEADER"] = "traceparent";
@@ -157,8 +160,8 @@ var EventConstants;
157
160
  // packages/core/src/internal/api/event/StompConnectionManager.ts
158
161
  var import_rx_stomp = require("@stomp/rx-stomp");
159
162
  var import_stompjs = require("@stomp/stompjs");
160
- var import_uuid = require("uuid");
161
163
  var import_debug = __toESM(require("debug"));
164
+ var import_uuid = require("uuid");
162
165
 
163
166
  class StompConnectionManager {
164
167
  lastWebsocketError = null;
@@ -171,9 +174,10 @@ class StompConnectionManager {
171
174
  initialConnectionSuccessful = false;
172
175
  debugLogger = import_debug.default("kinoitc:stomp");
173
176
  uuidv4 = import_uuid.v4();
174
- replyToId = import_uuid.v4();
175
- _replyToCri = "srv://" /* SERVICE_DESTINATION_PREFIX */ + this.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
177
+ _replyToCri = null;
178
+ serverHeadersSubscription = null;
176
179
  deactivationHandler = null;
180
+ replyToCriChangedHandler = null;
177
181
  get active() {
178
182
  return !!this.rxStomp;
179
183
  }
@@ -201,31 +205,23 @@ class StompConnectionManager {
201
205
  this.initialConnectionSuccessful = false;
202
206
  this.lastWebsocketError = null;
203
207
  this.maxConnectionAttemptsReached = false;
208
+ this._replyToCri = null;
209
+ this.serverHeadersSubscription?.unsubscribe();
210
+ this.serverHeadersSubscription = null;
204
211
  const url = "ws" + (connectionInfo.useSSL ? "s" : "") + "://" + connectionInfo.host + (connectionInfo.port ? ":" + connectionInfo.port : "") + "/v1";
205
212
  this.rxStomp = new import_rx_stomp.RxStomp;
206
- let connectHeadersInternal = typeof connectionInfo.connectHeaders !== "function" && connectionInfo.connectHeaders != null ? connectionInfo.connectHeaders : {};
213
+ let preparedSocket = null;
214
+ const userWebSocketFactory = connectionInfo.webSocketFactory;
207
215
  const stompConfig = {
208
216
  brokerURL: url,
209
- connectHeaders: connectHeadersInternal,
217
+ connectHeaders: {
218
+ ["session-keep-alive" /* SESSION_KEEP_ALIVE_HEADER */]: connectionInfo.sessionKeepAlive
219
+ },
210
220
  heartbeatIncoming: 120000,
211
221
  heartbeatOutgoing: 30000,
212
222
  reconnectDelay: this.INITIAL_RECONNECT_DELAY,
223
+ webSocketFactory: userWebSocketFactory ? () => preparedSocket : undefined,
213
224
  beforeConnect: async () => {
214
- if (typeof connectionInfo.connectHeaders === "function") {
215
- const headers = await connectionInfo.connectHeaders();
216
- for (const key in headers) {
217
- connectHeadersInternal[key] = headers[key];
218
- }
219
- }
220
- if (connectionInfo.disableStickySession) {
221
- connectHeadersInternal["disable-sticky-session" /* DISABLE_STICKY_SESSION_HEADER */] = "true";
222
- }
223
- if (connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */]) {
224
- this.replyToId = connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */];
225
- this._replyToCri = "srv://" /* SERVICE_DESTINATION_PREFIX */ + this.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
226
- } else {
227
- connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */] = this.replyToId;
228
- }
229
225
  if (connectionInfo?.maxConnectionAttempts) {
230
226
  this.connectionAttempts++;
231
227
  if (this.connectionAttempts > connectionInfo.maxConnectionAttempts) {
@@ -235,12 +231,23 @@ class StompConnectionManager {
235
231
  let message = this.lastWebsocketError?.message ? this.lastWebsocketError?.message : "UNKNOWN";
236
232
  reject(`Max number of reconnection attempts reached. Last WS Error ${message}`);
237
233
  }
234
+ return;
238
235
  } else {
239
236
  await this.connectionJitterDelay();
240
237
  }
241
238
  } else {
242
239
  await this.connectionJitterDelay();
243
240
  }
241
+ if (userWebSocketFactory) {
242
+ try {
243
+ preparedSocket = await userWebSocketFactory();
244
+ } catch (e) {
245
+ await this.deactivate();
246
+ if (!this.initialConnectionSuccessful) {
247
+ reject(e);
248
+ }
249
+ }
250
+ }
244
251
  }
245
252
  };
246
253
  if (this.debugLogger.enabled) {
@@ -267,36 +274,29 @@ class StompConnectionManager {
267
274
  this.rxStomp = null;
268
275
  reject(message);
269
276
  });
270
- const serverHeadersSubscription = this.rxStomp.serverHeaders$.subscribe((value) => {
271
- let connectedInfoJson = value["connected-info" /* CONNECTED_INFO_HEADER */];
272
- if (connectedInfoJson != null) {
273
- const connectedInfo = JSON.parse(connectedInfoJson);
274
- if (!connectionInfo.disableStickySession) {
275
- serverHeadersSubscription.unsubscribe();
276
- if (connectedInfo.sessionId != null && connectedInfo.replyToId != null) {
277
- if (connectionInfo.connectHeaders != null) {
278
- for (let key in connectHeadersInternal) {
279
- delete connectHeadersInternal[key];
280
- }
281
- }
282
- connectHeadersInternal["session" /* SESSION_HEADER */] = connectedInfo.sessionId;
283
- resolve(connectedInfo);
284
- } else {
285
- reject("Server did not return proper data for successful login");
286
- }
287
- } else if (typeof connectionInfo.connectHeaders === "function") {
288
- for (let key in connectHeadersInternal) {
289
- delete connectHeadersInternal[key];
290
- }
291
- if (!this.initialConnectionSuccessful) {
292
- resolve(connectedInfo);
293
- }
294
- } else if (typeof connectionInfo.connectHeaders === "object") {
295
- serverHeadersSubscription.unsubscribe();
296
- resolve(connectedInfo);
277
+ this.serverHeadersSubscription = this.rxStomp.serverHeaders$.subscribe((value) => {
278
+ const connectedInfoJson = value["connected-info" /* CONNECTED_INFO_HEADER */];
279
+ const firstConnect = this._replyToCri == null;
280
+ if (connectedInfoJson == null) {
281
+ if (firstConnect) {
282
+ reject("Server did not return proper data for successful login");
297
283
  }
298
- } else {
299
- reject("Server did not return proper data for successful login");
284
+ return;
285
+ }
286
+ const connectedInfo = JSON.parse(connectedInfoJson);
287
+ if (connectedInfo.replyToId == null) {
288
+ if (firstConnect) {
289
+ reject("Server did not return a replyToId for successful login");
290
+ }
291
+ return;
292
+ }
293
+ const newReplyToCri = "reply://" /* REPLY_DESTINATION_PREFIX */ + connectedInfo.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
294
+ if (firstConnect) {
295
+ this._replyToCri = newReplyToCri;
296
+ resolve(connectedInfo);
297
+ } else if (this._replyToCri !== newReplyToCri) {
298
+ this._replyToCri = newReplyToCri;
299
+ this.replyToCriChangedHandler?.(newReplyToCri);
300
300
  }
301
301
  });
302
302
  this.rxStomp.activate();
@@ -305,6 +305,8 @@ class StompConnectionManager {
305
305
  async deactivate(force) {
306
306
  if (this.rxStomp) {
307
307
  await this.rxStomp.deactivate({ force });
308
+ this.serverHeadersSubscription?.unsubscribe();
309
+ this.serverHeadersSubscription = null;
308
310
  if (this.deactivationHandler) {
309
311
  this.deactivationHandler();
310
312
  }
@@ -385,6 +387,10 @@ class EventBus {
385
387
  this.stompConnectionManager.deactivationHandler = () => {
386
388
  this.cleanup();
387
389
  };
390
+ this.stompConnectionManager.replyToCriChangedHandler = (replyToCri) => {
391
+ this.replyToCri = replyToCri;
392
+ this.resetRequestReplies("Reply destination changed");
393
+ };
388
394
  }
389
395
  isConnectionActive() {
390
396
  return this.stompConnectionManager.active;
@@ -452,7 +458,7 @@ class EventBus {
452
458
  })).subscribe({
453
459
  next(value) {
454
460
  if (value.hasHeader("control" /* CONTROL_HEADER */)) {
455
- if (value.headers.get("control" /* CONTROL_HEADER */) === "complete") {
461
+ if (value.headers.get("control" /* CONTROL_HEADER */) === "complete" /* CONTROL_VALUE_COMPLETE */) {
456
462
  serverSignaledCompletion = true;
457
463
  subscriber.complete();
458
464
  } else {
@@ -496,8 +502,16 @@ class EventBus {
496
502
  return this._observe(cri);
497
503
  }
498
504
  cleanup() {
505
+ this.resetRequestReplies("Connection disconnected");
506
+ if (this.errorSubjectSubscription) {
507
+ this.errorSubjectSubscription.unsubscribe();
508
+ this.errorSubjectSubscription = null;
509
+ }
510
+ this.serverInfo = null;
511
+ }
512
+ resetRequestReplies(reason) {
499
513
  if (this.requestRepliesSubject != null) {
500
- this.requestRepliesSubject.error(new Error("Connection disconnected"));
514
+ this.requestRepliesSubject.error(new Error(reason));
501
515
  if (this.requestRepliesSubscription != null) {
502
516
  this.requestRepliesSubscription.unsubscribe();
503
517
  this.requestRepliesSubscription = null;
@@ -505,11 +519,6 @@ class EventBus {
505
519
  this.requestRepliesSubject = null;
506
520
  this.requestRepliesObservable = null;
507
521
  }
508
- if (this.errorSubjectSubscription) {
509
- this.errorSubjectSubscription.unsubscribe();
510
- this.errorSubjectSubscription = null;
511
- }
512
- this.serverInfo = null;
513
522
  }
514
523
  createSendUnavailableError() {
515
524
  let ret = "You must call connect on the event bus before sending any request";
@@ -993,7 +1002,7 @@ var import_operators2 = require("rxjs/operators");
993
1002
  // packages/core/package.json
994
1003
  var package_default = {
995
1004
  name: "@kinotic-ai/core",
996
- version: "1.2.2",
1005
+ version: "1.3.1",
997
1006
  type: "module",
998
1007
  files: [
999
1008
  "dist"
@@ -1487,7 +1496,6 @@ class AuthorizationError extends KinoticError {
1487
1496
  }
1488
1497
  // packages/core/src/api/security/ConnectedInfo.ts
1489
1498
  class ConnectedInfo {
1490
- sessionId;
1491
1499
  replyToId;
1492
1500
  participant;
1493
1501
  }
package/dist/index.d.cts CHANGED
@@ -1,26 +1,54 @@
1
1
  /**
2
- * ConnectHeaders to use during connection to the kinoitc server
3
- * These headers will be sent as part of the STOMP CONNECT frame
4
- * This is typically used for authentication information, but any data can be sent
2
+ * Structural shape of a WebSocket used by the underlying STOMP client.
3
+ * Copied from the WebSocket interface to avoid pulling in the DOM typelib,
4
+ * so this type stays usable in Node environments where `lib: dom` is not set.
5
5
  */
6
- declare class ConnectHeaders {
7
- [key: string]: string;
6
+ interface IWebSocket {
7
+ url: string;
8
+ binaryType?: string;
9
+ readyState: number;
10
+ onopen: ((ev?: any) => any) | undefined | null;
11
+ onclose: ((ev?: any) => any) | undefined | null;
12
+ onerror: ((ev: any) => any) | undefined | null;
13
+ onmessage: ((ev: any) => any) | undefined | null;
14
+ close(code?: number, reason?: string): void;
15
+ send(data: string | ArrayBuffer): void;
8
16
  }
17
+ /**
18
+ * Factory invoked on every (re)connect to produce the WebSocket the STOMP
19
+ * client will use. Supply this in Node when you need to set headers on the
20
+ * upgrade request (for example, an Authorization header). It may be async —
21
+ * for example, to refresh a short-lived access token before each connect.
22
+ * Browser callers normally leave this unset and rely on the session cookie
23
+ * established by a prior REST login.
24
+ */
25
+ type WebSocketFactory = () => IWebSocket | Promise<IWebSocket>;
9
26
  declare class ServerInfo {
10
27
  host: string;
11
28
  port?: number | null;
12
29
  useSSL?: boolean | null;
13
30
  }
31
+ declare enum SessionKeepAliveMode {
32
+ NONE = "NONE",
33
+ ACTIVITY = "ACTIVITY",
34
+ CONNECTION = "CONNECTION"
35
+ }
14
36
  /**
15
- * ConnectionInfo provides the information needed to connect to the kinoitc server
37
+ * ConnectionInfo provides the information needed to connect to the kinoitc server.
38
+ *
39
+ * Authentication is performed during the WebSocket upgrade (handshake), not in
40
+ * the STOMP CONNECT frame. In the browser, log in via the REST endpoints first
41
+ * and the established session cookie will be used. In Node, supply a
42
+ * {@link WebSocketFactory} that attaches the required upgrade headers.
16
43
  */
17
44
  declare class ConnectionInfo extends ServerInfo {
18
45
  /**
19
- * The headers to send during the connection to the kinoitc server.
20
- * If a function is provided, it will be called to get the headers each time the connection is established.
21
- * This is useful for providing dynamic headers, such as a JWT token that expires.
46
+ * Optional factory used to create the underlying WebSocket. Use this in
47
+ * Node to attach custom headers (such as Authorization) to the upgrade
48
+ * request. If omitted, a default WebSocket is created and authentication
49
+ * is expected to come from the session cookie.
22
50
  */
23
- connectHeaders?: ConnectHeaders | (() => Promise<ConnectHeaders>);
51
+ webSocketFactory?: WebSocketFactory;
24
52
  /**
25
53
  * The maximum number of connection attempts to make during the {@link IEventBus} initial connection request.
26
54
  * If the limit is reached the {@link IEventBus} will return an error to the caller of {@link IEventBus#connect}
@@ -28,10 +56,11 @@ declare class ConnectionInfo extends ServerInfo {
28
56
  */
29
57
  maxConnectionAttempts?: number | null;
30
58
  /**
31
- * If true, the session will not be kept alive after the connection is established and then disrupted.
32
- * If false, the session will be kept alive after the connection is established and then disrupted, for a period of time.
59
+ * Controls whether session expiration is extended by gateway activity or by an active websocket connection.
60
+ * Defaults to {@link SessionKeepAliveMode.ACTIVITY}.
61
+ * Use {@link SessionKeepAliveMode.NONE} to remove the session when the websocket connection closes.
33
62
  */
34
- disableStickySession?: boolean | null;
63
+ sessionKeepAlive: SessionKeepAliveMode;
35
64
  }
36
65
  import { Optional } from "typescript-optional";
37
66
  import { Observable } from "rxjs";
@@ -100,7 +129,6 @@ declare class Participant implements IParticipant {
100
129
  * Contains information about the connection that was established
101
130
  */
102
131
  declare class ConnectedInfo {
103
- sessionId: string;
104
132
  replyToId: string;
105
133
  participant: Participant;
106
134
  }
@@ -253,22 +281,13 @@ declare enum EventConstants {
253
281
  CONTENT_LENGTH_HEADER = "content-length",
254
282
  REPLY_TO_HEADER = "reply-to",
255
283
  /**
256
- * This is the replyToId that will be supplied by the client, which will be used when sending replies to the client.
257
- */
258
- REPLY_TO_ID_HEADER = "reply-to-id",
259
- /**
260
- * Header provided by the sever on connection to represent the user's session id
261
- */
262
- SESSION_HEADER = "session",
263
- /**
264
284
  * Header provided by the server on connection to provide the {@link ConnectionInfo} as a JSON string
265
285
  */
266
286
  CONNECTED_INFO_HEADER = "connected-info",
267
287
  /**
268
- * Header provided by the client on connection request to represent that the server
269
- * should not keep the session alive after any network disconnection.
288
+ * Header provided by the client on connection request to choose how the session is kept alive.
270
289
  */
271
- DISABLE_STICKY_SESSION_HEADER = "disable-sticky-session",
290
+ SESSION_KEEP_ALIVE_HEADER = "session-keep-alive",
272
291
  /**
273
292
  * Correlates a response with a given request
274
293
  * Headers that start with __ will always be persisted between messages
@@ -297,6 +316,8 @@ declare enum EventConstants {
297
316
  SERVICE_DESTINATION_SCHEME = "srv",
298
317
  STREAM_DESTINATION_PREFIX = "stream://",
299
318
  STREAM_DESTINATION_SCHEME = "stream",
319
+ REPLY_DESTINATION_PREFIX = "reply://",
320
+ REPLY_DESTINATION_SCHEME = "reply",
300
321
  CONTENT_JSON = "application/json",
301
322
  CONTENT_TEXT = "text/plain",
302
323
  /**
@@ -1094,6 +1115,12 @@ declare class EventBus implements IEventBus {
1094
1115
  observe(cri: string): Observable3<IEvent>;
1095
1116
  private cleanup;
1096
1117
  /**
1118
+ * Tears down the shared request-replies stream so the next request rebuilds it against the
1119
+ * current {@link replyToCri}. Any in-flight requests are failed with the given reason since
1120
+ * their replies can no longer be delivered.
1121
+ */
1122
+ private resetRequestReplies;
1123
+ /**
1097
1124
  * Creates the proper error to return if this.stompConnectionManager?.rxStomp is not available on a send request
1098
1125
  */
1099
1126
  private createSendUnavailableError;
@@ -1117,4 +1144,4 @@ declare class ParticipantConstants {
1117
1144
  static readonly PARTICIPANT_TYPE_NODE: string;
1118
1145
  static readonly CLI_PARTICIPANT_ID: string;
1119
1146
  }
1120
- export { createCRI, Version, TextEventFactory, Sort, ServiceRegistry, ServiceContext, ServerInfo, Scope, Publish, ParticipantConstants, Participant, Pageable, Page, Order, OffsetPageable, NullHandling, KinoticSingleton, KinoticPlugin, KinoticError, Kinotic, JsonEventFactory, IterablePage, Identifiable, IServiceRegistry, IServiceProxy, IParticipant, IKinotic, IEventFactory, IEventBus, IEvent, IEditableDataSource, IDataSource, ICrudServiceProxyFactory, ICrudServiceProxy, FunctionalIterablePage, EventConstants, EventBus, Event, Direction, DefaultCRI, DataSourceUtils, CursorPageable, CrudServiceProxyFactory, CrudServiceProxy, ContextInterceptor, Context, ConnectionInfo, ConnectedInfo, ConnectHeaders, CRI, CONTEXT_METADATA_KEY, AuthorizationError, AuthenticationError, AbstractIterablePage };
1147
+ export { createCRI, WebSocketFactory, Version, TextEventFactory, Sort, SessionKeepAliveMode, ServiceRegistry, ServiceContext, ServerInfo, Scope, Publish, ParticipantConstants, Participant, Pageable, Page, Order, OffsetPageable, NullHandling, KinoticSingleton, KinoticPlugin, KinoticError, Kinotic, JsonEventFactory, IterablePage, Identifiable, IWebSocket, IServiceRegistry, IServiceProxy, IParticipant, IKinotic, IEventFactory, IEventBus, IEvent, IEditableDataSource, IDataSource, ICrudServiceProxyFactory, ICrudServiceProxy, FunctionalIterablePage, EventConstants, EventBus, Event, Direction, DefaultCRI, DataSourceUtils, CursorPageable, CrudServiceProxyFactory, CrudServiceProxy, ContextInterceptor, Context, ConnectionInfo, ConnectedInfo, CRI, CONTEXT_METADATA_KEY, AuthorizationError, AuthenticationError, AbstractIterablePage };
package/dist/index.d.ts CHANGED
@@ -1,26 +1,54 @@
1
1
  /**
2
- * ConnectHeaders to use during connection to the kinoitc server
3
- * These headers will be sent as part of the STOMP CONNECT frame
4
- * This is typically used for authentication information, but any data can be sent
2
+ * Structural shape of a WebSocket used by the underlying STOMP client.
3
+ * Copied from the WebSocket interface to avoid pulling in the DOM typelib,
4
+ * so this type stays usable in Node environments where `lib: dom` is not set.
5
5
  */
6
- declare class ConnectHeaders {
7
- [key: string]: string;
6
+ interface IWebSocket {
7
+ url: string;
8
+ binaryType?: string;
9
+ readyState: number;
10
+ onopen: ((ev?: any) => any) | undefined | null;
11
+ onclose: ((ev?: any) => any) | undefined | null;
12
+ onerror: ((ev: any) => any) | undefined | null;
13
+ onmessage: ((ev: any) => any) | undefined | null;
14
+ close(code?: number, reason?: string): void;
15
+ send(data: string | ArrayBuffer): void;
8
16
  }
17
+ /**
18
+ * Factory invoked on every (re)connect to produce the WebSocket the STOMP
19
+ * client will use. Supply this in Node when you need to set headers on the
20
+ * upgrade request (for example, an Authorization header). It may be async —
21
+ * for example, to refresh a short-lived access token before each connect.
22
+ * Browser callers normally leave this unset and rely on the session cookie
23
+ * established by a prior REST login.
24
+ */
25
+ type WebSocketFactory = () => IWebSocket | Promise<IWebSocket>;
9
26
  declare class ServerInfo {
10
27
  host: string;
11
28
  port?: number | null;
12
29
  useSSL?: boolean | null;
13
30
  }
31
+ declare enum SessionKeepAliveMode {
32
+ NONE = "NONE",
33
+ ACTIVITY = "ACTIVITY",
34
+ CONNECTION = "CONNECTION"
35
+ }
14
36
  /**
15
- * ConnectionInfo provides the information needed to connect to the kinoitc server
37
+ * ConnectionInfo provides the information needed to connect to the kinoitc server.
38
+ *
39
+ * Authentication is performed during the WebSocket upgrade (handshake), not in
40
+ * the STOMP CONNECT frame. In the browser, log in via the REST endpoints first
41
+ * and the established session cookie will be used. In Node, supply a
42
+ * {@link WebSocketFactory} that attaches the required upgrade headers.
16
43
  */
17
44
  declare class ConnectionInfo extends ServerInfo {
18
45
  /**
19
- * The headers to send during the connection to the kinoitc server.
20
- * If a function is provided, it will be called to get the headers each time the connection is established.
21
- * This is useful for providing dynamic headers, such as a JWT token that expires.
46
+ * Optional factory used to create the underlying WebSocket. Use this in
47
+ * Node to attach custom headers (such as Authorization) to the upgrade
48
+ * request. If omitted, a default WebSocket is created and authentication
49
+ * is expected to come from the session cookie.
22
50
  */
23
- connectHeaders?: ConnectHeaders | (() => Promise<ConnectHeaders>);
51
+ webSocketFactory?: WebSocketFactory;
24
52
  /**
25
53
  * The maximum number of connection attempts to make during the {@link IEventBus} initial connection request.
26
54
  * If the limit is reached the {@link IEventBus} will return an error to the caller of {@link IEventBus#connect}
@@ -28,10 +56,11 @@ declare class ConnectionInfo extends ServerInfo {
28
56
  */
29
57
  maxConnectionAttempts?: number | null;
30
58
  /**
31
- * If true, the session will not be kept alive after the connection is established and then disrupted.
32
- * If false, the session will be kept alive after the connection is established and then disrupted, for a period of time.
59
+ * Controls whether session expiration is extended by gateway activity or by an active websocket connection.
60
+ * Defaults to {@link SessionKeepAliveMode.ACTIVITY}.
61
+ * Use {@link SessionKeepAliveMode.NONE} to remove the session when the websocket connection closes.
33
62
  */
34
- disableStickySession?: boolean | null;
63
+ sessionKeepAlive: SessionKeepAliveMode;
35
64
  }
36
65
  import { Optional } from "typescript-optional";
37
66
  import { Observable } from "rxjs";
@@ -100,7 +129,6 @@ declare class Participant implements IParticipant {
100
129
  * Contains information about the connection that was established
101
130
  */
102
131
  declare class ConnectedInfo {
103
- sessionId: string;
104
132
  replyToId: string;
105
133
  participant: Participant;
106
134
  }
@@ -253,22 +281,13 @@ declare enum EventConstants {
253
281
  CONTENT_LENGTH_HEADER = "content-length",
254
282
  REPLY_TO_HEADER = "reply-to",
255
283
  /**
256
- * This is the replyToId that will be supplied by the client, which will be used when sending replies to the client.
257
- */
258
- REPLY_TO_ID_HEADER = "reply-to-id",
259
- /**
260
- * Header provided by the sever on connection to represent the user's session id
261
- */
262
- SESSION_HEADER = "session",
263
- /**
264
284
  * Header provided by the server on connection to provide the {@link ConnectionInfo} as a JSON string
265
285
  */
266
286
  CONNECTED_INFO_HEADER = "connected-info",
267
287
  /**
268
- * Header provided by the client on connection request to represent that the server
269
- * should not keep the session alive after any network disconnection.
288
+ * Header provided by the client on connection request to choose how the session is kept alive.
270
289
  */
271
- DISABLE_STICKY_SESSION_HEADER = "disable-sticky-session",
290
+ SESSION_KEEP_ALIVE_HEADER = "session-keep-alive",
272
291
  /**
273
292
  * Correlates a response with a given request
274
293
  * Headers that start with __ will always be persisted between messages
@@ -297,6 +316,8 @@ declare enum EventConstants {
297
316
  SERVICE_DESTINATION_SCHEME = "srv",
298
317
  STREAM_DESTINATION_PREFIX = "stream://",
299
318
  STREAM_DESTINATION_SCHEME = "stream",
319
+ REPLY_DESTINATION_PREFIX = "reply://",
320
+ REPLY_DESTINATION_SCHEME = "reply",
300
321
  CONTENT_JSON = "application/json",
301
322
  CONTENT_TEXT = "text/plain",
302
323
  /**
@@ -1094,6 +1115,12 @@ declare class EventBus implements IEventBus {
1094
1115
  observe(cri: string): Observable3<IEvent>;
1095
1116
  private cleanup;
1096
1117
  /**
1118
+ * Tears down the shared request-replies stream so the next request rebuilds it against the
1119
+ * current {@link replyToCri}. Any in-flight requests are failed with the given reason since
1120
+ * their replies can no longer be delivered.
1121
+ */
1122
+ private resetRequestReplies;
1123
+ /**
1097
1124
  * Creates the proper error to return if this.stompConnectionManager?.rxStomp is not available on a send request
1098
1125
  */
1099
1126
  private createSendUnavailableError;
@@ -1117,4 +1144,4 @@ declare class ParticipantConstants {
1117
1144
  static readonly PARTICIPANT_TYPE_NODE: string;
1118
1145
  static readonly CLI_PARTICIPANT_ID: string;
1119
1146
  }
1120
- export { createCRI, Version, TextEventFactory, Sort, ServiceRegistry, ServiceContext, ServerInfo, Scope, Publish, ParticipantConstants, Participant, Pageable, Page, Order, OffsetPageable, NullHandling, KinoticSingleton, KinoticPlugin, KinoticError, Kinotic, JsonEventFactory, IterablePage, Identifiable, IServiceRegistry, IServiceProxy, IParticipant, IKinotic, IEventFactory, IEventBus, IEvent, IEditableDataSource, IDataSource, ICrudServiceProxyFactory, ICrudServiceProxy, FunctionalIterablePage, EventConstants, EventBus, Event, Direction, DefaultCRI, DataSourceUtils, CursorPageable, CrudServiceProxyFactory, CrudServiceProxy, ContextInterceptor, Context, ConnectionInfo, ConnectedInfo, ConnectHeaders, CRI, CONTEXT_METADATA_KEY, AuthorizationError, AuthenticationError, AbstractIterablePage };
1147
+ export { createCRI, WebSocketFactory, Version, TextEventFactory, Sort, SessionKeepAliveMode, ServiceRegistry, ServiceContext, ServerInfo, Scope, Publish, ParticipantConstants, Participant, Pageable, Page, Order, OffsetPageable, NullHandling, KinoticSingleton, KinoticPlugin, KinoticError, Kinotic, JsonEventFactory, IterablePage, Identifiable, IWebSocket, IServiceRegistry, IServiceProxy, IParticipant, IKinotic, IEventFactory, IEventBus, IEvent, IEditableDataSource, IDataSource, ICrudServiceProxyFactory, ICrudServiceProxy, FunctionalIterablePage, EventConstants, EventBus, Event, Direction, DefaultCRI, DataSourceUtils, CursorPageable, CrudServiceProxyFactory, CrudServiceProxy, ContextInterceptor, Context, ConnectionInfo, ConnectedInfo, CRI, CONTEXT_METADATA_KEY, AuthorizationError, AuthenticationError, AbstractIterablePage };
package/dist/index.js CHANGED
@@ -2,19 +2,22 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // packages/core/src/api/ConnectionInfo.ts
5
- class ConnectHeaders {
6
- }
7
-
8
5
  class ServerInfo {
9
6
  host;
10
7
  port;
11
8
  useSSL;
12
9
  }
10
+ var SessionKeepAliveMode;
11
+ ((SessionKeepAliveMode2) => {
12
+ SessionKeepAliveMode2["NONE"] = "NONE";
13
+ SessionKeepAliveMode2["ACTIVITY"] = "ACTIVITY";
14
+ SessionKeepAliveMode2["CONNECTION"] = "CONNECTION";
15
+ })(SessionKeepAliveMode ||= {});
13
16
 
14
17
  class ConnectionInfo extends ServerInfo {
15
- connectHeaders;
18
+ webSocketFactory;
16
19
  maxConnectionAttempts;
17
- disableStickySession;
20
+ sessionKeepAlive = "ACTIVITY" /* ACTIVITY */;
18
21
  }
19
22
  // packages/core/src/api/errors/KinoticError.ts
20
23
  class KinoticError extends Error {
@@ -30,10 +33,8 @@ var EventConstants;
30
33
  EventConstants2["CONTENT_TYPE_HEADER"] = "content-type";
31
34
  EventConstants2["CONTENT_LENGTH_HEADER"] = "content-length";
32
35
  EventConstants2["REPLY_TO_HEADER"] = "reply-to";
33
- EventConstants2["REPLY_TO_ID_HEADER"] = "reply-to-id";
34
- EventConstants2["SESSION_HEADER"] = "session";
35
36
  EventConstants2["CONNECTED_INFO_HEADER"] = "connected-info";
36
- EventConstants2["DISABLE_STICKY_SESSION_HEADER"] = "disable-sticky-session";
37
+ EventConstants2["SESSION_KEEP_ALIVE_HEADER"] = "session-keep-alive";
37
38
  EventConstants2["CORRELATION_ID_HEADER"] = "__correlation-id";
38
39
  EventConstants2["ERROR_HEADER"] = "error";
39
40
  EventConstants2["COMPLETE_HEADER"] = "complete";
@@ -46,6 +47,8 @@ var EventConstants;
46
47
  EventConstants2["SERVICE_DESTINATION_SCHEME"] = "srv";
47
48
  EventConstants2["STREAM_DESTINATION_PREFIX"] = "stream://";
48
49
  EventConstants2["STREAM_DESTINATION_SCHEME"] = "stream";
50
+ EventConstants2["REPLY_DESTINATION_PREFIX"] = "reply://";
51
+ EventConstants2["REPLY_DESTINATION_SCHEME"] = "reply";
49
52
  EventConstants2["CONTENT_JSON"] = "application/json";
50
53
  EventConstants2["CONTENT_TEXT"] = "text/plain";
51
54
  EventConstants2["TRACEPARENT_HEADER"] = "traceparent";
@@ -55,8 +58,8 @@ var EventConstants;
55
58
  // packages/core/src/internal/api/event/StompConnectionManager.ts
56
59
  import { RxStomp } from "@stomp/rx-stomp";
57
60
  import { ReconnectionTimeMode } from "@stomp/stompjs";
58
- import { v4 as uuidv4 } from "uuid";
59
61
  import debug from "debug";
62
+ import { v4 as uuidv4 } from "uuid";
60
63
 
61
64
  class StompConnectionManager {
62
65
  lastWebsocketError = null;
@@ -69,9 +72,10 @@ class StompConnectionManager {
69
72
  initialConnectionSuccessful = false;
70
73
  debugLogger = debug("kinoitc:stomp");
71
74
  uuidv4 = uuidv4();
72
- replyToId = uuidv4();
73
- _replyToCri = "srv://" /* SERVICE_DESTINATION_PREFIX */ + this.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
75
+ _replyToCri = null;
76
+ serverHeadersSubscription = null;
74
77
  deactivationHandler = null;
78
+ replyToCriChangedHandler = null;
75
79
  get active() {
76
80
  return !!this.rxStomp;
77
81
  }
@@ -99,31 +103,23 @@ class StompConnectionManager {
99
103
  this.initialConnectionSuccessful = false;
100
104
  this.lastWebsocketError = null;
101
105
  this.maxConnectionAttemptsReached = false;
106
+ this._replyToCri = null;
107
+ this.serverHeadersSubscription?.unsubscribe();
108
+ this.serverHeadersSubscription = null;
102
109
  const url = "ws" + (connectionInfo.useSSL ? "s" : "") + "://" + connectionInfo.host + (connectionInfo.port ? ":" + connectionInfo.port : "") + "/v1";
103
110
  this.rxStomp = new RxStomp;
104
- let connectHeadersInternal = typeof connectionInfo.connectHeaders !== "function" && connectionInfo.connectHeaders != null ? connectionInfo.connectHeaders : {};
111
+ let preparedSocket = null;
112
+ const userWebSocketFactory = connectionInfo.webSocketFactory;
105
113
  const stompConfig = {
106
114
  brokerURL: url,
107
- connectHeaders: connectHeadersInternal,
115
+ connectHeaders: {
116
+ ["session-keep-alive" /* SESSION_KEEP_ALIVE_HEADER */]: connectionInfo.sessionKeepAlive
117
+ },
108
118
  heartbeatIncoming: 120000,
109
119
  heartbeatOutgoing: 30000,
110
120
  reconnectDelay: this.INITIAL_RECONNECT_DELAY,
121
+ webSocketFactory: userWebSocketFactory ? () => preparedSocket : undefined,
111
122
  beforeConnect: async () => {
112
- if (typeof connectionInfo.connectHeaders === "function") {
113
- const headers = await connectionInfo.connectHeaders();
114
- for (const key in headers) {
115
- connectHeadersInternal[key] = headers[key];
116
- }
117
- }
118
- if (connectionInfo.disableStickySession) {
119
- connectHeadersInternal["disable-sticky-session" /* DISABLE_STICKY_SESSION_HEADER */] = "true";
120
- }
121
- if (connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */]) {
122
- this.replyToId = connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */];
123
- this._replyToCri = "srv://" /* SERVICE_DESTINATION_PREFIX */ + this.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
124
- } else {
125
- connectHeadersInternal["reply-to-id" /* REPLY_TO_ID_HEADER */] = this.replyToId;
126
- }
127
123
  if (connectionInfo?.maxConnectionAttempts) {
128
124
  this.connectionAttempts++;
129
125
  if (this.connectionAttempts > connectionInfo.maxConnectionAttempts) {
@@ -133,12 +129,23 @@ class StompConnectionManager {
133
129
  let message = this.lastWebsocketError?.message ? this.lastWebsocketError?.message : "UNKNOWN";
134
130
  reject(`Max number of reconnection attempts reached. Last WS Error ${message}`);
135
131
  }
132
+ return;
136
133
  } else {
137
134
  await this.connectionJitterDelay();
138
135
  }
139
136
  } else {
140
137
  await this.connectionJitterDelay();
141
138
  }
139
+ if (userWebSocketFactory) {
140
+ try {
141
+ preparedSocket = await userWebSocketFactory();
142
+ } catch (e) {
143
+ await this.deactivate();
144
+ if (!this.initialConnectionSuccessful) {
145
+ reject(e);
146
+ }
147
+ }
148
+ }
142
149
  }
143
150
  };
144
151
  if (this.debugLogger.enabled) {
@@ -165,36 +172,29 @@ class StompConnectionManager {
165
172
  this.rxStomp = null;
166
173
  reject(message);
167
174
  });
168
- const serverHeadersSubscription = this.rxStomp.serverHeaders$.subscribe((value) => {
169
- let connectedInfoJson = value["connected-info" /* CONNECTED_INFO_HEADER */];
170
- if (connectedInfoJson != null) {
171
- const connectedInfo = JSON.parse(connectedInfoJson);
172
- if (!connectionInfo.disableStickySession) {
173
- serverHeadersSubscription.unsubscribe();
174
- if (connectedInfo.sessionId != null && connectedInfo.replyToId != null) {
175
- if (connectionInfo.connectHeaders != null) {
176
- for (let key in connectHeadersInternal) {
177
- delete connectHeadersInternal[key];
178
- }
179
- }
180
- connectHeadersInternal["session" /* SESSION_HEADER */] = connectedInfo.sessionId;
181
- resolve(connectedInfo);
182
- } else {
183
- reject("Server did not return proper data for successful login");
184
- }
185
- } else if (typeof connectionInfo.connectHeaders === "function") {
186
- for (let key in connectHeadersInternal) {
187
- delete connectHeadersInternal[key];
188
- }
189
- if (!this.initialConnectionSuccessful) {
190
- resolve(connectedInfo);
191
- }
192
- } else if (typeof connectionInfo.connectHeaders === "object") {
193
- serverHeadersSubscription.unsubscribe();
194
- resolve(connectedInfo);
175
+ this.serverHeadersSubscription = this.rxStomp.serverHeaders$.subscribe((value) => {
176
+ const connectedInfoJson = value["connected-info" /* CONNECTED_INFO_HEADER */];
177
+ const firstConnect = this._replyToCri == null;
178
+ if (connectedInfoJson == null) {
179
+ if (firstConnect) {
180
+ reject("Server did not return proper data for successful login");
195
181
  }
196
- } else {
197
- reject("Server did not return proper data for successful login");
182
+ return;
183
+ }
184
+ const connectedInfo = JSON.parse(connectedInfoJson);
185
+ if (connectedInfo.replyToId == null) {
186
+ if (firstConnect) {
187
+ reject("Server did not return a replyToId for successful login");
188
+ }
189
+ return;
190
+ }
191
+ const newReplyToCri = "reply://" /* REPLY_DESTINATION_PREFIX */ + connectedInfo.replyToId + ":" + this.uuidv4 + "@kinoitc.js.EventBus/replyHandler";
192
+ if (firstConnect) {
193
+ this._replyToCri = newReplyToCri;
194
+ resolve(connectedInfo);
195
+ } else if (this._replyToCri !== newReplyToCri) {
196
+ this._replyToCri = newReplyToCri;
197
+ this.replyToCriChangedHandler?.(newReplyToCri);
198
198
  }
199
199
  });
200
200
  this.rxStomp.activate();
@@ -203,6 +203,8 @@ class StompConnectionManager {
203
203
  async deactivate(force) {
204
204
  if (this.rxStomp) {
205
205
  await this.rxStomp.deactivate({ force });
206
+ this.serverHeadersSubscription?.unsubscribe();
207
+ this.serverHeadersSubscription = null;
206
208
  if (this.deactivationHandler) {
207
209
  this.deactivationHandler();
208
210
  }
@@ -283,6 +285,10 @@ class EventBus {
283
285
  this.stompConnectionManager.deactivationHandler = () => {
284
286
  this.cleanup();
285
287
  };
288
+ this.stompConnectionManager.replyToCriChangedHandler = (replyToCri) => {
289
+ this.replyToCri = replyToCri;
290
+ this.resetRequestReplies("Reply destination changed");
291
+ };
286
292
  }
287
293
  isConnectionActive() {
288
294
  return this.stompConnectionManager.active;
@@ -350,7 +356,7 @@ class EventBus {
350
356
  })).subscribe({
351
357
  next(value) {
352
358
  if (value.hasHeader("control" /* CONTROL_HEADER */)) {
353
- if (value.headers.get("control" /* CONTROL_HEADER */) === "complete") {
359
+ if (value.headers.get("control" /* CONTROL_HEADER */) === "complete" /* CONTROL_VALUE_COMPLETE */) {
354
360
  serverSignaledCompletion = true;
355
361
  subscriber.complete();
356
362
  } else {
@@ -394,8 +400,16 @@ class EventBus {
394
400
  return this._observe(cri);
395
401
  }
396
402
  cleanup() {
403
+ this.resetRequestReplies("Connection disconnected");
404
+ if (this.errorSubjectSubscription) {
405
+ this.errorSubjectSubscription.unsubscribe();
406
+ this.errorSubjectSubscription = null;
407
+ }
408
+ this.serverInfo = null;
409
+ }
410
+ resetRequestReplies(reason) {
397
411
  if (this.requestRepliesSubject != null) {
398
- this.requestRepliesSubject.error(new Error("Connection disconnected"));
412
+ this.requestRepliesSubject.error(new Error(reason));
399
413
  if (this.requestRepliesSubscription != null) {
400
414
  this.requestRepliesSubscription.unsubscribe();
401
415
  this.requestRepliesSubscription = null;
@@ -403,11 +417,6 @@ class EventBus {
403
417
  this.requestRepliesSubject = null;
404
418
  this.requestRepliesObservable = null;
405
419
  }
406
- if (this.errorSubjectSubscription) {
407
- this.errorSubjectSubscription.unsubscribe();
408
- this.errorSubjectSubscription = null;
409
- }
410
- this.serverInfo = null;
411
420
  }
412
421
  createSendUnavailableError() {
413
422
  let ret = "You must call connect on the event bus before sending any request";
@@ -894,7 +903,7 @@ import { first, map as map2 } from "rxjs/operators";
894
903
  // packages/core/package.json
895
904
  var package_default = {
896
905
  name: "@kinotic-ai/core",
897
- version: "1.2.2",
906
+ version: "1.3.1",
898
907
  type: "module",
899
908
  files: [
900
909
  "dist"
@@ -1388,7 +1397,6 @@ class AuthorizationError extends KinoticError {
1388
1397
  }
1389
1398
  // packages/core/src/api/security/ConnectedInfo.ts
1390
1399
  class ConnectedInfo {
1391
- sessionId;
1392
1400
  replyToId;
1393
1401
  participant;
1394
1402
  }
@@ -1423,6 +1431,7 @@ export {
1423
1431
  Version,
1424
1432
  TextEventFactory,
1425
1433
  Sort,
1434
+ SessionKeepAliveMode,
1426
1435
  ServiceRegistry,
1427
1436
  ServerInfo,
1428
1437
  Scope,
@@ -1450,7 +1459,6 @@ export {
1450
1459
  Context,
1451
1460
  ConnectionInfo,
1452
1461
  ConnectedInfo,
1453
- ConnectHeaders,
1454
1462
  CONTEXT_METADATA_KEY,
1455
1463
  AuthorizationError,
1456
1464
  AuthenticationError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinotic-ai/core",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"