@raisindb/client 0.1.5 → 0.1.17

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.
Files changed (44) hide show
  1. package/README.md +281 -70
  2. package/dist/chunk-DJDP5CX6.mjs +866 -0
  3. package/dist/chunk-DJDP5CX6.mjs.map +1 -0
  4. package/dist/chunk-JRFBP4WK.mjs +170 -0
  5. package/dist/chunk-JRFBP4WK.mjs.map +1 -0
  6. package/dist/chunk-OLJOZIIU.mjs +1348 -0
  7. package/dist/chunk-OLJOZIIU.mjs.map +1 -0
  8. package/dist/chunk-T2ZSNYFG.mjs +248 -0
  9. package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
  10. package/dist/chunk-VZX3ACSO.mjs +190 -0
  11. package/dist/chunk-VZX3ACSO.mjs.map +1 -0
  12. package/dist/chunk-WFBAWOVM.mjs +532 -0
  13. package/dist/chunk-WFBAWOVM.mjs.map +1 -0
  14. package/dist/conversation-list-store-CNcVagmw.d.mts +6120 -0
  15. package/dist/conversation-list-store-CNcVagmw.d.ts +6120 -0
  16. package/dist/index-DQjE-4Oj.d.ts +301 -0
  17. package/dist/index-FkFH4HHQ.d.mts +301 -0
  18. package/dist/index.d.mts +66 -6108
  19. package/dist/index.d.ts +66 -6108
  20. package/dist/index.js +859 -109
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +639 -3108
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/react.d.mts +4 -0
  25. package/dist/react.d.ts +4 -0
  26. package/dist/react.js +2500 -0
  27. package/dist/react.js.map +1 -0
  28. package/dist/react.mjs +9 -0
  29. package/dist/react.mjs.map +1 -0
  30. package/dist/svelte-sql-62EaYoeM.d.ts +52 -0
  31. package/dist/svelte-sql-CgT-Ij-P.d.mts +52 -0
  32. package/dist/svelte.d.mts +288 -0
  33. package/dist/svelte.d.ts +288 -0
  34. package/dist/svelte.js +2413 -0
  35. package/dist/svelte.js.map +1 -0
  36. package/dist/svelte.mjs +25 -0
  37. package/dist/svelte.mjs.map +1 -0
  38. package/dist/vue.d.mts +197 -0
  39. package/dist/vue.d.ts +197 -0
  40. package/dist/vue.js +1441 -0
  41. package/dist/vue.js.map +1 -0
  42. package/dist/vue.mjs +9 -0
  43. package/dist/vue.mjs.map +1 -0
  44. package/package.json +18 -3
package/dist/index.mjs CHANGED
@@ -1,366 +1,54 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ import {
2
+ createRaisinReact,
3
+ useConversation,
4
+ useConversationList,
5
+ useFlow
6
+ } from "./chunk-WFBAWOVM.mjs";
7
+ import {
8
+ RAISIN_CONTEXT_KEY,
9
+ createConversationAdapter,
10
+ createConversationListAdapter,
11
+ createFlowAdapter
12
+ } from "./chunk-VZX3ACSO.mjs";
13
+ import {
14
+ FlowClient,
15
+ RaisinAbortError,
16
+ RaisinAuthError,
17
+ RaisinConnectionError,
18
+ RaisinError,
19
+ RaisinFlowError,
20
+ RaisinTimeoutError,
21
+ SSEClient,
22
+ classifyHttpError
23
+ } from "./chunk-DJDP5CX6.mjs";
24
+ import {
25
+ createRaisinVue
26
+ } from "./chunk-JRFBP4WK.mjs";
27
+ import {
28
+ createAuthAdapter,
29
+ createConnectionAdapter,
30
+ createSqlAdapter,
31
+ createSubscriptionAdapter
32
+ } from "./chunk-T2ZSNYFG.mjs";
33
+ import {
34
+ Connection,
35
+ ConnectionState,
36
+ ConversationListStore,
37
+ ConversationStore,
38
+ LogLevel,
39
+ ReconnectManager,
40
+ configureLogger,
41
+ getLogLevel,
42
+ getLoggerConfig,
43
+ isRecoveredDoneEvent,
44
+ logger,
45
+ normalizeHomePath,
46
+ projectPlansFromMessages,
47
+ setLogLevel
48
+ } from "./chunk-OLJOZIIU.mjs";
7
49
 
8
50
  // src/client.ts
9
- import { EventEmitter as EventEmitter3 } from "events";
10
-
11
- // src/connection.ts
12
- import { EventEmitter } from "events";
13
-
14
- // src/utils/reconnect.ts
15
- var ReconnectManager = class {
16
- constructor(options = {}) {
17
- this.attempts = 0;
18
- this.isReconnecting = false;
19
- this.initialDelay = options.initialDelay ?? 1e3;
20
- this.maxDelay = options.maxDelay ?? 3e4;
21
- this.backoffMultiplier = options.backoffMultiplier ?? 2;
22
- this.maxAttempts = options.maxAttempts ?? Infinity;
23
- this.jitter = options.jitter ?? true;
24
- this.currentDelay = this.initialDelay;
25
- }
26
- /**
27
- * Schedule a reconnection attempt
28
- *
29
- * @param callback - Function to call when it's time to reconnect
30
- * @returns true if reconnection was scheduled, false if max attempts reached
31
- */
32
- scheduleReconnect(callback) {
33
- if (this.attempts >= this.maxAttempts) {
34
- return false;
35
- }
36
- this.cancelReconnect();
37
- this.isReconnecting = true;
38
- this.attempts++;
39
- let delay = this.currentDelay;
40
- if (this.jitter) {
41
- const jitterRange = delay * 0.25;
42
- delay += (Math.random() * 2 - 1) * jitterRange;
43
- delay = Math.max(0, Math.round(delay));
44
- }
45
- this.reconnectTimeout = setTimeout(async () => {
46
- try {
47
- await callback();
48
- this.reset();
49
- } catch (error) {
50
- this.currentDelay = Math.min(
51
- this.currentDelay * this.backoffMultiplier,
52
- this.maxDelay
53
- );
54
- }
55
- }, delay);
56
- return true;
57
- }
58
- /**
59
- * Cancel any pending reconnection attempt
60
- */
61
- cancelReconnect() {
62
- if (this.reconnectTimeout) {
63
- clearTimeout(this.reconnectTimeout);
64
- this.reconnectTimeout = void 0;
65
- }
66
- this.isReconnecting = false;
67
- }
68
- /**
69
- * Reset the reconnection state (call after successful connection)
70
- */
71
- reset() {
72
- this.cancelReconnect();
73
- this.currentDelay = this.initialDelay;
74
- this.attempts = 0;
75
- }
76
- /**
77
- * Check if currently in reconnection mode
78
- */
79
- isActive() {
80
- return this.isReconnecting;
81
- }
82
- /**
83
- * Get the current number of reconnection attempts
84
- */
85
- getAttempts() {
86
- return this.attempts;
87
- }
88
- /**
89
- * Get the current delay in milliseconds
90
- */
91
- getCurrentDelay() {
92
- return this.currentDelay;
93
- }
94
- /**
95
- * Check if max attempts have been reached
96
- */
97
- hasReachedMaxAttempts() {
98
- return this.attempts >= this.maxAttempts;
99
- }
100
- };
101
-
102
- // src/logger.ts
103
- var LogLevel = /* @__PURE__ */ ((LogLevel3) => {
104
- LogLevel3[LogLevel3["Silent"] = 0] = "Silent";
105
- LogLevel3[LogLevel3["Error"] = 1] = "Error";
106
- LogLevel3[LogLevel3["Warn"] = 2] = "Warn";
107
- LogLevel3[LogLevel3["Info"] = 3] = "Info";
108
- LogLevel3[LogLevel3["Debug"] = 4] = "Debug";
109
- return LogLevel3;
110
- })(LogLevel || {});
111
- var config = {
112
- level: 1 /* Error */,
113
- prefix: "[RaisinDB]"
114
- };
115
- function configureLogger(options) {
116
- config = { ...config, ...options };
117
- }
118
- function getLoggerConfig() {
119
- return { ...config };
120
- }
121
- function setLogLevel(level) {
122
- config.level = level;
123
- }
124
- function getLogLevel() {
125
- return config.level;
126
- }
127
- var logger = {
128
- /**
129
- * Log debug message (only shown when level >= Debug)
130
- */
131
- debug(...args) {
132
- if (config.level >= 4 /* Debug */) {
133
- console.log(config.prefix, ...args);
134
- }
135
- },
136
- /**
137
- * Log info message (only shown when level >= Info)
138
- */
139
- info(...args) {
140
- if (config.level >= 3 /* Info */) {
141
- console.log(config.prefix, ...args);
142
- }
143
- },
144
- /**
145
- * Log warning message (only shown when level >= Warn)
146
- */
147
- warn(...args) {
148
- if (config.level >= 2 /* Warn */) {
149
- console.warn(config.prefix, ...args);
150
- }
151
- },
152
- /**
153
- * Log error message (only shown when level >= Error)
154
- */
155
- error(...args) {
156
- if (config.level >= 1 /* Error */) {
157
- console.error(config.prefix, ...args);
158
- }
159
- }
160
- };
161
- if (typeof globalThis !== "undefined") {
162
- globalThis.__raisinDebug = (enable = true) => {
163
- setLogLevel(enable ? 4 /* Debug */ : 1 /* Error */);
164
- console.log(`[RaisinDB] Debug logging ${enable ? "ENABLED" : "DISABLED"} (includes server handler logs)`);
165
- };
166
- }
167
-
168
- // src/connection.ts
169
- var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
170
- ConnectionState2["Disconnected"] = "disconnected";
171
- ConnectionState2["Connecting"] = "connecting";
172
- ConnectionState2["Connected"] = "connected";
173
- ConnectionState2["Reconnecting"] = "reconnecting";
174
- ConnectionState2["Closed"] = "closed";
175
- return ConnectionState2;
176
- })(ConnectionState || {});
177
- function getWebSocketImpl() {
178
- if (typeof WebSocket !== "undefined") {
179
- return WebSocket;
180
- }
181
- try {
182
- const ws = __require("ws");
183
- return ws.WebSocket || ws.default || ws;
184
- } catch (error) {
185
- throw new Error(
186
- 'WebSocket implementation not found. In Node.js, please install the "ws" package: npm install ws'
187
- );
188
- }
189
- }
190
- var Connection = class extends EventEmitter {
191
- constructor(url, options = {}) {
192
- super();
193
- this.state = "disconnected" /* Disconnected */;
194
- this.manualClose = false;
195
- this.url = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
196
- this.options = {
197
- autoReconnect: options.autoReconnect ?? true,
198
- heartbeatInterval: options.heartbeatInterval ?? 3e4,
199
- heartbeatTimeout: options.heartbeatTimeout ?? 5e3,
200
- protocols: options.protocols
201
- };
202
- this.reconnectManager = new ReconnectManager(options.reconnectOptions);
203
- this.WebSocketImpl = getWebSocketImpl();
204
- }
205
- /**
206
- * Connect to the WebSocket server
207
- */
208
- async connect() {
209
- if (this.state === "connected" /* Connected */ || this.state === "connecting" /* Connecting */) {
210
- return;
211
- }
212
- this.manualClose = false;
213
- this.setState("connecting" /* Connecting */);
214
- return new Promise((resolve, reject) => {
215
- try {
216
- this.ws = new this.WebSocketImpl(
217
- this.url,
218
- this.options.protocols
219
- );
220
- this.ws.binaryType = "arraybuffer";
221
- this.ws.onopen = () => {
222
- this.setState("connected" /* Connected */);
223
- this.reconnectManager.reset();
224
- this.startHeartbeat();
225
- resolve();
226
- };
227
- this.ws.onmessage = (event) => {
228
- logger.debug(`WebSocket onmessage fired - data type: ${typeof event.data}, is ArrayBuffer: ${event.data instanceof ArrayBuffer}`);
229
- this.resetHeartbeat();
230
- const data = event.data;
231
- logger.debug(`Received binary message - size: ${data.byteLength} bytes`);
232
- logger.debug(`Emitting 'message' event to client handlers`);
233
- this.emit("message", data);
234
- };
235
- this.ws.onerror = () => {
236
- const error = new Error("WebSocket error occurred");
237
- this.emit("error", error);
238
- if (this.state === "connecting" /* Connecting */) {
239
- reject(error);
240
- }
241
- };
242
- this.ws.onclose = (event) => {
243
- this.stopHeartbeat();
244
- this.emit("close", event);
245
- if (!this.manualClose && this.options.autoReconnect) {
246
- this.setState("reconnecting" /* Reconnecting */);
247
- this.reconnect();
248
- } else {
249
- this.setState("disconnected" /* Disconnected */);
250
- }
251
- if (this.state === "connecting" /* Connecting */) {
252
- reject(new Error(`Connection closed: ${event.reason || "Unknown reason"}`));
253
- }
254
- };
255
- } catch (error) {
256
- this.setState("disconnected" /* Disconnected */);
257
- reject(error);
258
- }
259
- });
260
- }
261
- /**
262
- * Disconnect from the WebSocket server
263
- */
264
- disconnect() {
265
- this.manualClose = true;
266
- this.reconnectManager.cancelReconnect();
267
- this.stopHeartbeat();
268
- if (this.ws) {
269
- this.ws.close(1e3, "Client disconnect");
270
- this.ws = void 0;
271
- }
272
- this.setState("closed" /* Closed */);
273
- }
274
- /**
275
- * Send data through the WebSocket
276
- *
277
- * @param data - Data to send (Uint8Array for MessagePack)
278
- */
279
- send(data) {
280
- if (this.state !== "connected" /* Connected */ || !this.ws) {
281
- throw new Error("Cannot send data: not connected");
282
- }
283
- if (this.ws.readyState !== 1) {
284
- logger.error(`WebSocket not open - readyState: ${this.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`);
285
- throw new Error(`Cannot send data: WebSocket not open (readyState: ${this.ws.readyState})`);
286
- }
287
- logger.debug(`Sending message - size: ${data.length} bytes, readyState: ${this.ws.readyState}`);
288
- try {
289
- this.ws.send(data);
290
- logger.debug(`Message sent successfully`);
291
- } catch (error) {
292
- logger.error(`Failed to send message:`, error);
293
- throw new Error(`Failed to send data: ${error}`);
294
- }
295
- }
296
- /**
297
- * Get current connection state
298
- */
299
- getState() {
300
- return this.state;
301
- }
302
- /**
303
- * Check if connected
304
- */
305
- isConnected() {
306
- return this.state === "connected" /* Connected */;
307
- }
308
- /**
309
- * Reconnect to the server
310
- */
311
- reconnect() {
312
- const success = this.reconnectManager.scheduleReconnect(async () => {
313
- try {
314
- await this.connect();
315
- } catch (error) {
316
- }
317
- });
318
- if (!success) {
319
- this.setState("disconnected" /* Disconnected */);
320
- this.emit("error", new Error("Max reconnection attempts reached"));
321
- }
322
- }
323
- /**
324
- * Set connection state and emit event
325
- */
326
- setState(state) {
327
- if (this.state !== state) {
328
- this.state = state;
329
- this.emit("stateChange", state);
330
- }
331
- }
332
- /**
333
- * Start heartbeat mechanism
334
- */
335
- startHeartbeat() {
336
- if (this.options.heartbeatInterval <= 0) {
337
- return;
338
- }
339
- this.stopHeartbeat();
340
- }
341
- /**
342
- * Stop heartbeat mechanism
343
- */
344
- stopHeartbeat() {
345
- if (this.heartbeatTimer) {
346
- clearInterval(this.heartbeatTimer);
347
- this.heartbeatTimer = void 0;
348
- }
349
- if (this.heartbeatTimeoutTimer) {
350
- clearTimeout(this.heartbeatTimeoutTimer);
351
- this.heartbeatTimeoutTimer = void 0;
352
- }
353
- }
354
- /**
355
- * Reset heartbeat timeout (called when message received)
356
- */
357
- resetHeartbeat() {
358
- if (this.heartbeatTimeoutTimer) {
359
- clearTimeout(this.heartbeatTimeoutTimer);
360
- this.heartbeatTimeoutTimer = void 0;
361
- }
362
- }
363
- };
51
+ import { EventEmitter as EventEmitter2 } from "events";
364
52
 
365
53
  // src/utils/request-tracker.ts
366
54
  import { v4 as uuidv4 } from "uuid";
@@ -881,6 +569,14 @@ var EventHandler = class {
881
569
  * If a subscription with identical filters already exists, the callback
882
570
  * is added to the existing subscription instead of creating a new one.
883
571
  *
572
+ * Path filter semantics (matched server-side):
573
+ * - A plain path matches **only that exact node** — there is no implicit
574
+ * prefix matching.
575
+ * - `*` matches exactly one path segment (e.g. `/inbox/*` matches
576
+ * `/inbox/a` but not `/inbox/a/b`).
577
+ * - `**` matches recursively (e.g. `/inbox/**` matches all descendants
578
+ * of `/inbox`, but not `/inbox` itself).
579
+ *
884
580
  * @param filters - Event filters
885
581
  * @param callback - Callback function for events
886
582
  * @returns Subscription object
@@ -1071,14 +767,29 @@ var EventHandler = class {
1071
767
  * Restore subscriptions after reconnect
1072
768
  *
1073
769
  * Re-subscribes to the server with the original filters for all active
1074
- * subscription entries.
770
+ * subscription entries. Failed entries are kept (with their callbacks)
771
+ * and marked pending so a retry call can re-attempt them without
772
+ * duplicating already-restored subscriptions.
773
+ *
774
+ * @param options.retry - When true, only re-attempts entries that failed
775
+ * in a previous call (instead of re-subscribing everything)
776
+ * @throws Error if any subscription could not be restored
1075
777
  */
1076
- async restoreSubscriptions() {
1077
- const entriesToRestore = Array.from(this.filterSubscriptions.entries());
1078
- this.subscriptions.clear();
1079
- this.filterSubscriptions.clear();
778
+ async restoreSubscriptions(options) {
779
+ if (!options?.retry) {
780
+ for (const entry of this.filterSubscriptions.values()) {
781
+ this.subscriptions.delete(entry.serverId);
782
+ entry.restorePending = true;
783
+ }
784
+ }
785
+ const entriesToRestore = Array.from(this.filterSubscriptions.entries()).filter(([, entry]) => entry.restorePending);
786
+ let failureCount = 0;
787
+ let lastError;
1080
788
  for (const [hash, entry] of entriesToRestore) {
1081
- if (entry.callbacks.size === 0) continue;
789
+ if (entry.callbacks.size === 0) {
790
+ entry.restorePending = false;
791
+ continue;
792
+ }
1082
793
  try {
1083
794
  const payload = { filters: entry.filters };
1084
795
  const response = await this.sendRequest(
@@ -1086,22 +797,24 @@ var EventHandler = class {
1086
797
  "subscribe" /* Subscribe */
1087
798
  );
1088
799
  const newServerId = response.subscription_id;
1089
- this.filterSubscriptions.set(hash, {
1090
- serverId: newServerId,
1091
- filters: entry.filters,
1092
- callbacks: entry.callbacks
1093
- });
800
+ entry.serverId = newServerId;
801
+ entry.restorePending = false;
1094
802
  this.subscriptions.set(newServerId, hash);
1095
803
  for (const callback of entry.callbacks) {
1096
804
  this.callbackToHash.set(callback, hash);
1097
805
  }
1098
806
  } catch (error) {
1099
807
  logger.error("Failed to restore subscription:", error);
1100
- for (const callback of entry.callbacks) {
1101
- this.callbackToHash.delete(callback);
1102
- }
808
+ failureCount++;
809
+ lastError = error;
1103
810
  }
1104
811
  }
812
+ if (failureCount > 0) {
813
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
814
+ throw new Error(
815
+ `Failed to restore ${failureCount} subscription(s) after reconnect: ${message}`
816
+ );
817
+ }
1105
818
  }
1106
819
  };
1107
820
  var EventSubscriptions = class {
@@ -1136,7 +849,15 @@ var EventSubscriptions = class {
1136
849
  /**
1137
850
  * Subscribe to events for a specific path
1138
851
  *
1139
- * @param path - Path pattern (supports wildcards like "/folder/*")
852
+ * Path pattern semantics (matched server-side):
853
+ * - A plain path matches **only that exact node** — there is no implicit
854
+ * prefix matching, so `/folder` does NOT match `/folder/child`.
855
+ * - `*` matches exactly one path segment: `/folder/*` matches
856
+ * `/folder/child` but not `/folder/child/grandchild`.
857
+ * - `**` matches recursively: `/folder/**` matches every descendant of
858
+ * `/folder` (but not `/folder` itself).
859
+ *
860
+ * @param path - Path pattern (e.g. "/folder", "/folder/*", "/folder/**")
1140
861
  * @param callback - Callback function for events
1141
862
  * @param options - Optional subscription options
1142
863
  * @param options.includeNode - Include full node data in event payload (default: false)
@@ -1768,7 +1489,17 @@ var NodeFileSource = class {
1768
1489
  return;
1769
1490
  }
1770
1491
  if (!this._fsPromises) {
1771
- this._fsPromises = await import("fs/promises");
1492
+ if (!isNodeEnvironment()) {
1493
+ throw new Error(
1494
+ "NodeFileSource requires Node.js (fs/promises is not available in browsers). In the browser, use a File or Blob object instead."
1495
+ );
1496
+ }
1497
+ const specifier = "fs/promises";
1498
+ this._fsPromises = await import(
1499
+ /* @vite-ignore */
1500
+ /* webpackIgnore: true */
1501
+ specifier
1502
+ );
1772
1503
  }
1773
1504
  const stats = await this._fsPromises.stat(this._filePath);
1774
1505
  this._size = stats.size;
@@ -3024,398 +2755,40 @@ var HttpFunctionsApi = class {
3024
2755
  }
3025
2756
  };
3026
2757
 
3027
- // src/errors.ts
3028
- var RaisinError = class extends Error {
3029
- constructor(message, code, details) {
3030
- super(message);
3031
- this.name = "RaisinError";
3032
- this.code = code;
3033
- this.details = details;
3034
- if (Error.captureStackTrace) {
3035
- Error.captureStackTrace(this, this.constructor);
3036
- }
3037
- }
3038
- };
3039
- var RaisinConnectionError = class extends RaisinError {
3040
- constructor(message, code, options) {
3041
- super(message, code, options?.details);
3042
- this.name = "RaisinConnectionError";
3043
- this.status = options?.status;
3044
- }
3045
- };
3046
- var RaisinAuthError = class extends RaisinError {
3047
- constructor(message, code, status, details) {
3048
- super(message, code, details);
3049
- this.name = "RaisinAuthError";
3050
- this.status = status;
3051
- }
3052
- };
3053
- var RaisinFlowError = class extends RaisinError {
3054
- constructor(message, code, instanceId, details) {
3055
- super(message, code, details);
3056
- this.name = "RaisinFlowError";
3057
- this.instanceId = instanceId;
3058
- }
3059
- };
3060
- var RaisinTimeoutError = class extends RaisinError {
3061
- constructor(message, code, timeoutMs, details) {
3062
- super(message, code, details);
3063
- this.name = "RaisinTimeoutError";
3064
- this.timeoutMs = timeoutMs;
3065
- }
3066
- };
3067
- var RaisinAbortError = class extends RaisinError {
3068
- constructor(message = "Operation was aborted") {
3069
- super(message, "ABORTED");
3070
- this.name = "RaisinAbortError";
3071
- }
3072
- };
3073
- function classifyHttpError(status, message, details) {
3074
- if (status === 401) {
3075
- return new RaisinAuthError(message, "AUTH_UNAUTHORIZED", status, details);
3076
- }
3077
- if (status === 403) {
3078
- return new RaisinAuthError(message, "AUTH_FORBIDDEN", status, details);
3079
- }
3080
- if (status === 404) {
3081
- return new RaisinFlowError(message, "FLOW_NOT_FOUND", void 0, details);
3082
- }
3083
- return new RaisinConnectionError(message, "CONNECTION_FAILED", { status, details });
3084
- }
3085
-
3086
- // src/streaming/sse-client.ts
3087
- function parseSSEBlock(block) {
3088
- let event = "message";
3089
- let data = "";
3090
- let id;
3091
- const lines = block.split("\n");
3092
- for (const line of lines) {
3093
- if (line.startsWith("event:")) {
3094
- event = line.slice(6).trim();
3095
- } else if (line.startsWith("data:")) {
3096
- if (data) data += "\n";
3097
- data += line.slice(5).trim();
3098
- } else if (line.startsWith("id:")) {
3099
- id = line.slice(3).trim();
3100
- }
3101
- }
3102
- if (!data) return null;
3103
- return { event, data, id };
3104
- }
3105
- var SSEClient = class {
3106
- constructor(url, options = {}) {
3107
- this.abortController = null;
3108
- this._state = "disconnected";
3109
- this.url = url;
3110
- this.options = options;
2758
+ // src/conversations.ts
2759
+ var DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS = 12e4;
2760
+ var ConversationManager = class {
2761
+ constructor(baseUrl, repository, authManager, options = {}, executeSql) {
2762
+ this.baseUrl = baseUrl.replace(/\/$/, "");
2763
+ this.repository = repository;
2764
+ this.authManager = authManager;
3111
2765
  this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
3112
- this._lastEventId = options.lastEventId;
3113
- this.autoReconnect = options.reconnect?.enabled ?? true;
3114
- this.externalSignal = options.signal;
3115
- this.eventTypeSet = options.eventTypes?.length ? new Set(options.eventTypes) : null;
3116
- this.reconnectManager = new ReconnectManager({
3117
- initialDelay: options.reconnect?.initialDelay ?? 1e3,
3118
- maxDelay: options.reconnect?.maxDelay ?? 3e4,
3119
- backoffMultiplier: options.reconnect?.backoffMultiplier ?? 2,
3120
- maxAttempts: options.reconnect?.maxAttempts ?? Infinity
3121
- });
3122
- }
3123
- /** Current connection state */
3124
- get state() {
3125
- return this._state;
3126
- }
3127
- /**
3128
- * Returns a promise that resolves once the SSE connection is established.
3129
- *
3130
- * Useful for ensuring the connection is ready before triggering events
3131
- * on the server side (e.g., calling resume before consuming the stream).
3132
- * If the client is already connected, resolves immediately.
3133
- */
3134
- waitUntilConnected() {
3135
- if (this._state === "connected") return Promise.resolve();
3136
- return new Promise((resolve, reject) => {
3137
- const originalOnState = this.options.onStateChange;
3138
- this.options.onStateChange = (state) => {
3139
- originalOnState?.(state);
3140
- if (state === "connected") {
3141
- this.options.onStateChange = originalOnState;
3142
- resolve();
3143
- } else if (state === "disconnected") {
3144
- this.options.onStateChange = originalOnState;
3145
- reject(new Error("SSE connection failed"));
3146
- }
3147
- };
3148
- });
3149
- }
3150
- /** Last received event ID */
3151
- get lastEventId() {
3152
- return this._lastEventId;
2766
+ this.requestTimeout = options.requestTimeout ?? 6e4;
2767
+ this.executeSql = executeSql;
3153
2768
  }
3154
- setState(state) {
3155
- if (this._state !== state) {
3156
- this._state = state;
3157
- this.options.onStateChange?.(state);
2769
+ // ==========================================================================
2770
+ // User context
2771
+ // ==========================================================================
2772
+ async ensureCurrentUser() {
2773
+ if (this.cachedUserId && this.cachedUserHome) {
2774
+ return { userId: this.cachedUserId, userHome: this.cachedUserHome };
3158
2775
  }
2776
+ const result = await this.sqlQuery(
2777
+ `SELECT RAISIN_CURRENT_USER()->>'path'::String as home, RAISIN_CURRENT_USER()->>'id'::String as user_id`
2778
+ );
2779
+ if (!result?.rows?.[0]) throw new Error("User not authenticated");
2780
+ const row = result.rows[0];
2781
+ const userHome = normalizeHomePath(row.home);
2782
+ if (!userHome) throw new Error("User home path not found");
2783
+ this.cachedUserHome = userHome;
2784
+ this.cachedUserId = row.user_id || userHome;
2785
+ return { userId: this.cachedUserId, userHome };
3159
2786
  }
2787
+ // ==========================================================================
2788
+ // Conversation listing
2789
+ // ==========================================================================
3160
2790
  /**
3161
- * Connect and consume events via callback.
3162
- *
3163
- * @param onEvent - Called for each matching SSE event
3164
- * @param onError - Called when an error occurs (optional)
3165
- */
3166
- async connect(onEvent, onError) {
3167
- this.close();
3168
- await this.startStream(onEvent, onError);
3169
- }
3170
- /**
3171
- * Close the SSE connection and stop reconnection attempts.
3172
- */
3173
- close() {
3174
- this.reconnectManager.cancelReconnect();
3175
- if (this.abortController) {
3176
- this.abortController.abort();
3177
- this.abortController = null;
3178
- }
3179
- this.setState("disconnected");
3180
- }
3181
- /**
3182
- * AsyncIterable interface: allows `for await (const event of sseClient)`.
3183
- *
3184
- * The iterator ends when close() is called or the server closes the stream
3185
- * (and auto-reconnect is disabled or max attempts are exhausted).
3186
- */
3187
- async *[Symbol.asyncIterator]() {
3188
- const queue = [];
3189
- let resolve = null;
3190
- let finished = false;
3191
- const push = (item) => {
3192
- if (finished) return;
3193
- if (resolve) {
3194
- const r = resolve;
3195
- resolve = null;
3196
- r(item);
3197
- } else {
3198
- queue.push(item);
3199
- }
3200
- };
3201
- const pull = () => {
3202
- const queued = queue.shift();
3203
- if (queued) return Promise.resolve(queued);
3204
- return new Promise((r) => {
3205
- resolve = r;
3206
- });
3207
- };
3208
- this.startStream(
3209
- (event) => push({ done: false, value: event }),
3210
- (error) => push({ done: true, error })
3211
- ).then(() => {
3212
- push({ done: true });
3213
- }).catch((error) => {
3214
- push({ done: true, error });
3215
- });
3216
- try {
3217
- while (!finished) {
3218
- const item = await pull();
3219
- if (item.done) {
3220
- finished = true;
3221
- if (item.error) {
3222
- throw item.error;
3223
- }
3224
- return;
3225
- }
3226
- yield item.value;
3227
- }
3228
- } finally {
3229
- finished = true;
3230
- this.close();
3231
- }
3232
- }
3233
- /**
3234
- * Core streaming loop: connects, reads the SSE stream, and handles reconnection.
3235
- */
3236
- async startStream(onEvent, onError) {
3237
- while (true) {
3238
- try {
3239
- await this.readStream(onEvent);
3240
- if (!this.autoReconnect || this._state === "disconnected") {
3241
- return;
3242
- }
3243
- this.setState("reconnecting");
3244
- const scheduled = await this.waitForReconnect();
3245
- if (!scheduled) {
3246
- logger.warn("SSE max reconnection attempts reached");
3247
- this.setState("disconnected");
3248
- return;
3249
- }
3250
- } catch (error) {
3251
- if (this._state === "disconnected") {
3252
- return;
3253
- }
3254
- const err = error instanceof Error ? error : new Error(String(error));
3255
- if (err.name === "AbortError" || err instanceof RaisinAbortError) {
3256
- return;
3257
- }
3258
- logger.error("SSE connection error:", err.message);
3259
- onError?.(err);
3260
- if (!this.autoReconnect) {
3261
- this.setState("disconnected");
3262
- throw err;
3263
- }
3264
- this.setState("reconnecting");
3265
- const scheduled = await this.waitForReconnect();
3266
- if (!scheduled) {
3267
- logger.warn("SSE max reconnection attempts reached");
3268
- this.setState("disconnected");
3269
- throw err;
3270
- }
3271
- }
3272
- }
3273
- }
3274
- /**
3275
- * Perform a single fetch and read the response body as SSE stream.
3276
- */
3277
- async readStream(onEvent) {
3278
- this.abortController = new AbortController();
3279
- this.setState("connecting");
3280
- if (this.externalSignal) {
3281
- if (this.externalSignal.aborted) {
3282
- this.abortController.abort();
3283
- } else {
3284
- const ctrl = this.abortController;
3285
- this.externalSignal.addEventListener("abort", () => ctrl.abort(), { once: true });
3286
- }
3287
- }
3288
- const headers = {
3289
- Accept: "text/event-stream",
3290
- "Cache-Control": "no-cache",
3291
- ...this.options.headers
3292
- };
3293
- if (this._lastEventId) {
3294
- headers["Last-Event-ID"] = this._lastEventId;
3295
- }
3296
- const method = this.options.method ?? "GET";
3297
- const fetchOptions = {
3298
- method,
3299
- headers,
3300
- signal: this.abortController.signal
3301
- };
3302
- if (method === "POST" && this.options.body !== void 0) {
3303
- fetchOptions.headers["Content-Type"] = "application/json";
3304
- fetchOptions.body = JSON.stringify(this.options.body);
3305
- }
3306
- const response = await this.fetchImpl(this.url, fetchOptions);
3307
- if (!response.ok) {
3308
- const text = await response.text().catch(() => "");
3309
- const msg = `SSE connection failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`;
3310
- if (response.status === 401 || response.status === 403) {
3311
- throw new RaisinAuthError(
3312
- msg,
3313
- response.status === 401 ? "AUTH_UNAUTHORIZED" : "AUTH_FORBIDDEN",
3314
- response.status
3315
- );
3316
- }
3317
- throw new RaisinConnectionError(msg, "SSE_STREAM_ERROR", { status: response.status });
3318
- }
3319
- if (!response.body) {
3320
- throw new RaisinConnectionError("SSE response has no body", "SSE_STREAM_ERROR");
3321
- }
3322
- this.setState("connected");
3323
- this.reconnectManager.reset();
3324
- const reader = response.body.getReader();
3325
- const decoder = new TextDecoder();
3326
- let buffer = "";
3327
- try {
3328
- while (true) {
3329
- const { done, value } = await reader.read();
3330
- if (done) break;
3331
- buffer += decoder.decode(value, { stream: true });
3332
- const blocks = buffer.split("\n\n");
3333
- buffer = blocks.pop() ?? "";
3334
- for (const block of blocks) {
3335
- const trimmed = block.trim();
3336
- if (!trimmed) continue;
3337
- const parsed = parseSSEBlock(trimmed);
3338
- if (!parsed) continue;
3339
- if (parsed.id) {
3340
- this._lastEventId = parsed.id;
3341
- }
3342
- if (this.eventTypeSet && !this.eventTypeSet.has(parsed.event)) {
3343
- continue;
3344
- }
3345
- let data;
3346
- try {
3347
- data = JSON.parse(parsed.data);
3348
- } catch {
3349
- data = parsed.data;
3350
- }
3351
- const sseEvent = {
3352
- type: parsed.event,
3353
- data,
3354
- id: parsed.id
3355
- };
3356
- onEvent(sseEvent);
3357
- }
3358
- }
3359
- } finally {
3360
- reader.releaseLock();
3361
- }
3362
- }
3363
- /**
3364
- * Wait for the reconnect manager to schedule a reconnection attempt.
3365
- * Returns true if reconnection was scheduled, false if max attempts reached.
3366
- */
3367
- waitForReconnect() {
3368
- return new Promise((resolve) => {
3369
- const scheduled = this.reconnectManager.scheduleReconnect(() => {
3370
- resolve(true);
3371
- });
3372
- if (!scheduled) {
3373
- resolve(false);
3374
- }
3375
- });
3376
- }
3377
- };
3378
-
3379
- // src/utils/chat-events.ts
3380
- function isRecoveredDoneEvent(event) {
3381
- if (event.type !== "done") return false;
3382
- const done = event;
3383
- if (done.recovered !== true) return false;
3384
- return typeof done.content !== "string" || done.content.trim().length === 0;
3385
- }
3386
-
3387
- // src/conversations.ts
3388
- var ConversationManager = class {
3389
- constructor(baseUrl, repository, authManager, options = {}, executeSql) {
3390
- this.baseUrl = baseUrl.replace(/\/$/, "");
3391
- this.repository = repository;
3392
- this.authManager = authManager;
3393
- this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
3394
- this.requestTimeout = options.requestTimeout ?? 6e4;
3395
- this.executeSql = executeSql;
3396
- }
3397
- // ==========================================================================
3398
- // User context
3399
- // ==========================================================================
3400
- async ensureCurrentUser() {
3401
- if (this.cachedUserId && this.cachedUserHome) {
3402
- return { userId: this.cachedUserId, userHome: this.cachedUserHome };
3403
- }
3404
- const result = await this.sqlQuery(
3405
- `SELECT RAISIN_CURRENT_USER()->>'path'::String as home, RAISIN_CURRENT_USER()->>'id'::String as user_id`
3406
- );
3407
- if (!result?.rows?.[0]) throw new Error("User not authenticated");
3408
- const row = result.rows[0];
3409
- this.cachedUserHome = row.home;
3410
- this.cachedUserId = row.user_id || this.cachedUserHome;
3411
- if (!this.cachedUserHome) throw new Error("User home path not found");
3412
- return { userId: this.cachedUserId, userHome: this.cachedUserHome };
3413
- }
3414
- // ==========================================================================
3415
- // Conversation listing
3416
- // ==========================================================================
3417
- /**
3418
- * List conversations for the current user.
2791
+ * List conversations for the current user.
3419
2792
  */
3420
2793
  async list(options) {
3421
2794
  const workspace = "raisin:access_control";
@@ -3604,13 +2977,15 @@ var ConversationManager = class {
3604
2977
  fetch: this.fetchImpl,
3605
2978
  signal: options?.signal,
3606
2979
  method: "POST",
3607
- body: { channel: streamTarget.channel, path: streamTarget.path }
2980
+ body: { channel: streamTarget.channel, path: streamTarget.path },
2981
+ inactivityTimeoutMs: options?.inactivityTimeoutMs ?? DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS
3608
2982
  });
3609
2983
  const iterator = sse[Symbol.asyncIterator]();
3610
2984
  let pendingFirst = iterator.next();
3611
2985
  try {
3612
2986
  await sse.waitUntilConnected();
3613
2987
  } catch {
2988
+ pendingFirst?.catch(() => void 0);
3614
2989
  pendingFirst = null;
3615
2990
  sse.close();
3616
2991
  logger.debug("SSE connection failed, falling back to create-only");
@@ -3647,6 +3022,15 @@ var ConversationManager = class {
3647
3022
  return;
3648
3023
  }
3649
3024
  }
3025
+ } catch (error) {
3026
+ if (error instanceof RaisinTimeoutError && error.code === "SSE_INACTIVITY_TIMEOUT") {
3027
+ logger.warn(
3028
+ `[ConversationManager] SSE stream inactive for ${error.timeoutMs}ms, ending turn with synthetic waiting event`
3029
+ );
3030
+ yield { type: "waiting", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3031
+ return;
3032
+ }
3033
+ throw error;
3650
3034
  } finally {
3651
3035
  sse.close();
3652
3036
  }
@@ -4132,471 +3516,159 @@ var ConversationManager = class {
4132
3516
  }
4133
3517
  };
4134
3518
 
4135
- // src/flow-client.ts
4136
- var FlowClient = class _FlowClient {
4137
- /**
4138
- * Create a new FlowClient.
4139
- *
4140
- * @param baseUrl - Base HTTP URL of the RaisinDB server (e.g., "http://localhost:8081")
4141
- * @param repository - Repository name
4142
- * @param authManager - Auth manager for token access
4143
- * @param options - Additional options
4144
- * @param flowsApi - Optional FlowsApi for WebSocket-based flow operations
4145
- */
4146
- constructor(baseUrl, repository, authManager, options = {}, flowsApi) {
3519
+ // src/inbox.ts
3520
+ var InboxApi = class {
3521
+ constructor(baseUrl, repository, authManager, options = {}) {
4147
3522
  this.baseUrl = baseUrl.replace(/\/$/, "");
4148
3523
  this.repository = repository;
4149
3524
  this.authManager = authManager;
4150
3525
  this.fetchImpl = options.fetch ?? fetch;
4151
- this.requestTimeout = options.requestTimeout ?? 6e4;
4152
- this.flowsApi = flowsApi;
3526
+ this.requestTimeout = options.requestTimeout ?? 3e4;
4153
3527
  }
4154
3528
  /**
4155
- * Create a FlowClient from an existing RaisinHttpClient.
3529
+ * List the caller's inbox tasks.
4156
3530
  *
4157
- * @param httpClient - An authenticated RaisinHttpClient instance
4158
- * @param baseUrl - Base HTTP URL (same as the one used for the HTTP client)
4159
- * @param repository - Repository name
4160
- * @param options - Additional options
3531
+ * Tasks are sorted pending-first, then by priority (highest first) and
3532
+ * due time (earliest first).
4161
3533
  */
4162
- static fromHttpClient(httpClient, baseUrl, repository, options = {}, flowsApi) {
4163
- return new _FlowClient(
4164
- baseUrl,
4165
- repository,
4166
- httpClient.getAuthManager(),
4167
- options,
4168
- flowsApi
4169
- );
3534
+ async listTasks(options = {}) {
3535
+ const params = new URLSearchParams();
3536
+ if (options.status) params.set("status", options.status);
3537
+ if (options.assignee) params.set("assignee", options.assignee);
3538
+ const query = params.size > 0 ? `?${params.toString()}` : "";
3539
+ return this.request({
3540
+ method: "GET",
3541
+ path: `/api/inbox/${this.repository}${query}`,
3542
+ signal: options.signal
3543
+ });
4170
3544
  }
4171
- // ==========================================================================
4172
- // Flow Execution
4173
- // ==========================================================================
4174
3545
  /**
4175
- * Execute a flow by path.
4176
- *
4177
- * Creates a FlowInstance and queues it for execution. Returns immediately
4178
- * with the instance ID - use `streamEvents()` to monitor progress.
4179
- *
4180
- * @param flowPath - Path to the raisin:Flow node (e.g., "/flows/my-flow")
4181
- * @param input - Input data passed to the flow
4182
- * @returns Flow run response with instance_id and job_id
4183
- *
4184
- * @example
4185
- * ```typescript
4186
- * const result = await flows.run('/flows/process-order', {
4187
- * orderId: 'ord-123',
4188
- * items: [{ sku: 'ABC', qty: 2 }],
4189
- * });
4190
- * console.log('Instance:', result.instance_id);
4191
- * ```
3546
+ * Get a single inbox task by ID (or path).
4192
3547
  */
4193
- async run(flowPath, input = {}, options) {
4194
- if (this.flowsApi) {
4195
- return this.flowsApi.run(flowPath, input);
4196
- }
4197
- const response = await this.request({
4198
- method: "POST",
4199
- path: `/api/flows/${this.repository}/run`,
4200
- body: { flow_path: flowPath, input },
3548
+ async getTask(taskId, options) {
3549
+ return this.request({
3550
+ method: "GET",
3551
+ path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}`,
4201
3552
  signal: options?.signal
4202
3553
  });
4203
- return response;
4204
3554
  }
4205
- // ==========================================================================
4206
- // Instance Status
4207
- // ==========================================================================
4208
3555
  /**
4209
- * Get the current status of a flow instance.
3556
+ * Complete an inbox task.
4210
3557
  *
4211
- * Returns the instance status, variables, and metadata. Useful for
4212
- * polling-based approaches where SSE is not reliable (e.g., initial
4213
- * flow execution where events may be missed).
3558
+ * The caller must be the task's assignee (or an admin). Completion marks
3559
+ * the task done, records who decided, and resumes the owning flow.
4214
3560
  *
4215
- * @param instanceId - Flow instance ID (from `run()`)
4216
- * @returns Instance status including variables and flow path
3561
+ * @param taskId - Task node ID (or path)
3562
+ * @param response - The response payload:
3563
+ * - approval tasks: `{ action: '<option value>', comment?: string }`
3564
+ * - input tasks: the input value(s) matching the task's input_schema
3565
+ * - review/action tasks: any acknowledgement payload
4217
3566
  *
4218
3567
  * @example
4219
3568
  * ```typescript
4220
- * const status = await flows.getInstanceStatus('instance-123');
4221
- * console.log('Status:', status.status); // 'waiting', 'completed', etc.
3569
+ * await inbox.completeTask('task-123', { action: 'approve' });
4222
3570
  * ```
4223
3571
  */
4224
- async getInstanceStatus(instanceId, options) {
4225
- if (this.flowsApi) {
4226
- return this.flowsApi.getInstanceStatus(instanceId);
4227
- }
3572
+ async completeTask(taskId, response, options) {
4228
3573
  return this.request({
4229
- method: "GET",
4230
- path: `/api/flows/${this.repository}/instances/${instanceId}`,
3574
+ method: "POST",
3575
+ path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}/complete`,
3576
+ body: { response },
4231
3577
  signal: options?.signal
4232
3578
  });
4233
3579
  }
4234
3580
  // ==========================================================================
4235
- // Event Streaming (SSE)
3581
+ // Internal HTTP request helper
4236
3582
  // ==========================================================================
4237
- /**
4238
- * Stream real-time execution events for a flow instance via SSE.
4239
- *
4240
- * Returns an async iterable that yields FlowExecutionEvent objects.
4241
- * The stream automatically closes when a terminal event is received
4242
- * (flow_completed or flow_failed).
4243
- *
4244
- * @param instanceId - Flow instance ID (from `run()`)
4245
- * @returns Async iterable of flow execution events
4246
- *
4247
- * @example
4248
- * ```typescript
4249
- * const { instance_id } = await flows.run('/flows/my-flow', { key: 'value' });
4250
- *
4251
- * for await (const event of flows.streamEvents(instance_id)) {
4252
- * switch (event.type) {
4253
- * case 'step_started':
4254
- * console.log(`Step ${event.node_id} started`);
4255
- * break;
4256
- * case 'step_completed':
4257
- * console.log(`Step ${event.node_id} completed in ${event.duration_ms}ms`);
4258
- * break;
4259
- * case 'text_chunk':
4260
- * process.stdout.write(event.text);
4261
- * break;
4262
- * case 'flow_completed':
4263
- * console.log('Flow done:', event.output);
4264
- * break;
4265
- * case 'flow_failed':
4266
- * console.error('Flow failed:', event.error);
4267
- * break;
4268
- * }
4269
- * }
4270
- * ```
4271
- */
4272
- async *streamEvents(instanceId, options) {
4273
- if (this.flowsApi) {
4274
- const sub = await this.flowsApi.subscribeEvents(instanceId);
4275
- try {
4276
- for await (const event of sub.events) {
4277
- if (options?.signal?.aborted) throw new RaisinAbortError();
4278
- yield event;
4279
- if (event.type === "flow_completed" || event.type === "flow_failed") {
4280
- return;
4281
- }
4282
- }
4283
- } finally {
4284
- await sub.unsubscribe();
4285
- }
4286
- return;
4287
- }
4288
- const url = `${this.baseUrl}/api/flows/${this.repository}/instances/${instanceId}/events`;
4289
- const headers = {};
3583
+ async request(options) {
3584
+ const url = `${this.baseUrl}${options.path}`;
3585
+ const headers = {
3586
+ "Content-Type": "application/json"
3587
+ };
4290
3588
  const token = this.authManager.getAccessToken();
4291
3589
  if (token) {
4292
3590
  headers["Authorization"] = `Bearer ${token}`;
4293
3591
  }
4294
- const sse = new SSEClient(url, {
4295
- headers,
4296
- eventTypes: ["flow-event", "message"],
4297
- reconnect: { enabled: false },
4298
- fetch: this.fetchImpl,
4299
- signal: options?.signal
4300
- });
3592
+ const controller = new AbortController();
3593
+ const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
3594
+ if (options.signal) {
3595
+ if (options.signal.aborted) {
3596
+ clearTimeout(timeoutId);
3597
+ throw new RaisinAbortError();
3598
+ }
3599
+ options.signal.addEventListener("abort", () => controller.abort(), {
3600
+ once: true
3601
+ });
3602
+ }
4301
3603
  try {
4302
- for await (const sseEvent of sse) {
4303
- const event = sseEvent.data;
4304
- yield event;
4305
- if (event.type === "flow_completed" || event.type === "flow_failed") {
4306
- return;
3604
+ const response = await this.fetchImpl(url, {
3605
+ method: options.method,
3606
+ headers,
3607
+ body: options.body ? JSON.stringify(options.body) : void 0,
3608
+ signal: controller.signal
3609
+ });
3610
+ clearTimeout(timeoutId);
3611
+ if (!response.ok) {
3612
+ const errorText = await response.text();
3613
+ let errorMessage;
3614
+ try {
3615
+ const errorJson = JSON.parse(errorText);
3616
+ errorMessage = errorJson.message || errorJson.error || errorText;
3617
+ } catch {
3618
+ errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
4307
3619
  }
3620
+ throw classifyHttpError(response.status, errorMessage);
4308
3621
  }
4309
- } finally {
4310
- sse.close();
3622
+ const contentType = response.headers.get("content-type");
3623
+ if (contentType?.includes("application/json")) {
3624
+ return await response.json();
3625
+ }
3626
+ return void 0;
3627
+ } catch (error) {
3628
+ clearTimeout(timeoutId);
3629
+ if (error instanceof RaisinAbortError || error instanceof RaisinTimeoutError) {
3630
+ throw error;
3631
+ }
3632
+ if (error instanceof Error && error.name === "AbortError") {
3633
+ if (options.signal?.aborted) {
3634
+ throw new RaisinAbortError();
3635
+ }
3636
+ throw new RaisinTimeoutError(
3637
+ `Request timeout after ${this.requestTimeout}ms`,
3638
+ "REQUEST_TIMEOUT",
3639
+ this.requestTimeout
3640
+ );
3641
+ }
3642
+ throw error;
4311
3643
  }
4312
3644
  }
3645
+ };
3646
+
3647
+ // src/database.ts
3648
+ var Database = class _Database {
3649
+ constructor(repository, context, sendRequest, eventHandler, branch, revision, getUploadManager, getSignAssetUrl, httpOptions) {
3650
+ this.repository = repository;
3651
+ this.branch = branch;
3652
+ this.revision = revision;
3653
+ this._context = {
3654
+ ...context,
3655
+ repository,
3656
+ branch: branch || context.branch,
3657
+ revision: revision || context.revision
3658
+ };
3659
+ this.sendRequest = (payload, requestType, contextOverride, requestOptions) => sendRequest(
3660
+ payload,
3661
+ requestType || "node_create",
3662
+ contextOverride || this._context,
3663
+ requestOptions
3664
+ );
3665
+ this.eventHandler = eventHandler;
3666
+ this.getUploadManager = getUploadManager;
3667
+ this.getSignAssetUrl = getSignAssetUrl;
3668
+ this.httpOptions = httpOptions;
3669
+ }
4313
3670
  /**
4314
- * Create an eagerly-connected event stream for a flow instance.
4315
- *
4316
- * Unlike `streamEvents()` (a lazy async generator), this method opens the
4317
- * SSE connection immediately and waits until it is established before
4318
- * returning. This is critical for resume flows: you must subscribe to
4319
- * events BEFORE calling `resume()`, otherwise events emitted between
4320
- * the resume and the SSE connection are lost.
4321
- *
4322
- * @param instanceId - Flow instance ID
4323
- * @returns Object with an async iterable `events` and a `close()` method
4324
- */
4325
- async createEventStream(instanceId, options) {
4326
- if (this.flowsApi) {
4327
- const sub = await this.flowsApi.subscribeEvents(instanceId);
4328
- return {
4329
- events: sub.events,
4330
- close: () => {
4331
- sub.unsubscribe();
4332
- }
4333
- };
4334
- }
4335
- const url = `${this.baseUrl}/api/flows/${this.repository}/instances/${instanceId}/events`;
4336
- const headers = {};
4337
- const token = this.authManager.getAccessToken();
4338
- if (token) {
4339
- headers["Authorization"] = `Bearer ${token}`;
4340
- }
4341
- const sse = new SSEClient(url, {
4342
- headers,
4343
- eventTypes: ["flow-event", "message"],
4344
- reconnect: { enabled: false },
4345
- fetch: this.fetchImpl,
4346
- signal: options?.signal
4347
- });
4348
- const iterator = sse[Symbol.asyncIterator]();
4349
- let pendingFirst = iterator.next();
4350
- await sse.waitUntilConnected();
4351
- const events = {
4352
- [Symbol.asyncIterator]() {
4353
- return {
4354
- async next() {
4355
- let result;
4356
- if (pendingFirst) {
4357
- result = await pendingFirst;
4358
- pendingFirst = null;
4359
- } else {
4360
- result = await iterator.next();
4361
- }
4362
- if (result.done) return { done: true, value: void 0 };
4363
- const event = result.value.data;
4364
- return { done: false, value: event };
4365
- },
4366
- async return() {
4367
- await iterator.return?.();
4368
- sse.close();
4369
- return { done: true, value: void 0 };
4370
- }
4371
- };
4372
- }
4373
- };
4374
- return { events, close: () => sse.close() };
4375
- }
4376
- // ==========================================================================
4377
- // Flow Resume & Human Task
4378
- // ==========================================================================
4379
- /**
4380
- * Resume a flow instance that is in a waiting state.
4381
- *
4382
- * Use this when a flow is waiting for external input (tool results,
4383
- * external events, etc.). The resume data is passed to the waiting step.
4384
- *
4385
- * @param instanceId - Flow instance ID
4386
- * @param data - Resume data to pass to the waiting step
4387
- *
4388
- * @example
4389
- * ```typescript
4390
- * await flows.resume('instance-123', {
4391
- * tool_name: 'get_weather',
4392
- * result: { temperature: 72, conditions: 'sunny' },
4393
- * });
4394
- * ```
4395
- */
4396
- async resume(instanceId, data, options) {
4397
- if (this.flowsApi) {
4398
- await this.flowsApi.resume(instanceId, data);
4399
- return;
4400
- }
4401
- await this.request({
4402
- method: "POST",
4403
- path: `/api/flows/${this.repository}/instances/${instanceId}/resume`,
4404
- body: { resume_data: data },
4405
- signal: options?.signal
4406
- });
4407
- }
4408
- /**
4409
- * Respond to a human task within a flow.
4410
- *
4411
- * When a flow is waiting on a human_task step (approval, input, review),
4412
- * use this method to submit the response and resume the flow.
4413
- *
4414
- * @param instanceId - Flow instance ID
4415
- * @param taskId - The human task node ID
4416
- * @param response - The human's response data
4417
- *
4418
- * @example
4419
- * ```typescript
4420
- * // Approve a task
4421
- * await flows.respondToHumanTask('instance-123', 'step-5', {
4422
- * action: 'approve',
4423
- * comment: 'Looks good!',
4424
- * });
4425
- *
4426
- * // Provide input for an input task
4427
- * await flows.respondToHumanTask('instance-123', 'step-3', {
4428
- * name: 'John Doe',
4429
- * email: 'john@example.com',
4430
- * });
4431
- * ```
4432
- */
4433
- async respondToHumanTask(instanceId, taskId, response, options) {
4434
- await this.request({
4435
- method: "POST",
4436
- path: `/api/flows/${this.repository}/instances/${instanceId}/tasks/${taskId}/respond`,
4437
- body: { response },
4438
- signal: options?.signal
4439
- });
4440
- }
4441
- // ==========================================================================
4442
- // Convenience Methods
4443
- // ==========================================================================
4444
- /**
4445
- * Run a flow and wait for the final result.
4446
- *
4447
- * Starts the flow, streams all events, and returns the terminal outcome.
4448
- * This is a one-shot helper for fire-and-forget flows where you only
4449
- * care about the final output.
4450
- *
4451
- * @param flowPath - Path to the raisin:Flow node
4452
- * @param input - Input data passed to the flow
4453
- * @param options - Optional abort signal
4454
- * @returns The flow outcome including output or error
4455
- *
4456
- * @example
4457
- * ```typescript
4458
- * const result = await flows.runAndWait('/flows/process-order', { orderId: '123' });
4459
- * if (result.status === 'completed') {
4460
- * console.log('Output:', result.output);
4461
- * }
4462
- * ```
4463
- */
4464
- async runAndWait(flowPath, input = {}, options) {
4465
- const { instance_id } = await this.run(flowPath, input, options);
4466
- for await (const event of this.streamEvents(instance_id, options)) {
4467
- if (event.type === "flow_completed") {
4468
- return {
4469
- instanceId: instance_id,
4470
- status: "completed",
4471
- output: event.output
4472
- };
4473
- }
4474
- if (event.type === "flow_failed") {
4475
- return {
4476
- instanceId: instance_id,
4477
- status: "failed",
4478
- error: event.error
4479
- };
4480
- }
4481
- }
4482
- return { instanceId: instance_id, status: "failed", error: "Stream ended without terminal event" };
4483
- }
4484
- /**
4485
- * Run a flow and collect all events into an array.
4486
- *
4487
- * Starts the flow, streams all events, and returns them as a collected array.
4488
- * Useful for testing, logging, or post-processing all flow events.
4489
- *
4490
- * @param flowPath - Path to the raisin:Flow node
4491
- * @param input - Input data passed to the flow
4492
- * @param options - Optional abort signal
4493
- * @returns The instance ID and all collected events
4494
- *
4495
- * @example
4496
- * ```typescript
4497
- * const { events } = await flows.runAndCollect('/flows/etl-pipeline', { source: 'db' });
4498
- * const steps = events.filter(e => e.type === 'step_completed');
4499
- * console.log(`Completed ${steps.length} steps`);
4500
- * ```
4501
- */
4502
- async runAndCollect(flowPath, input = {}, options) {
4503
- const { instance_id } = await this.run(flowPath, input, options);
4504
- const events = [];
4505
- for await (const event of this.streamEvents(instance_id, options)) {
4506
- events.push(event);
4507
- }
4508
- return { instanceId: instance_id, events };
4509
- }
4510
- // ==========================================================================
4511
- // Internal HTTP request helper
4512
- // ==========================================================================
4513
- async request(options) {
4514
- const url = `${this.baseUrl}${options.path}`;
4515
- const headers = {
4516
- "Content-Type": "application/json"
4517
- };
4518
- const token = this.authManager.getAccessToken();
4519
- if (token) {
4520
- headers["Authorization"] = `Bearer ${token}`;
4521
- }
4522
- const controller = new AbortController();
4523
- const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
4524
- if (options.signal) {
4525
- if (options.signal.aborted) {
4526
- clearTimeout(timeoutId);
4527
- throw new RaisinAbortError();
4528
- }
4529
- options.signal.addEventListener("abort", () => controller.abort(), { once: true });
4530
- }
4531
- try {
4532
- const response = await this.fetchImpl(url, {
4533
- method: options.method,
4534
- headers,
4535
- body: options.body ? JSON.stringify(options.body) : void 0,
4536
- signal: controller.signal
4537
- });
4538
- clearTimeout(timeoutId);
4539
- if (!response.ok) {
4540
- const errorText = await response.text();
4541
- let errorMessage;
4542
- try {
4543
- const errorJson = JSON.parse(errorText);
4544
- errorMessage = errorJson.message || errorJson.error || errorText;
4545
- } catch {
4546
- errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
4547
- }
4548
- throw classifyHttpError(response.status, errorMessage);
4549
- }
4550
- const contentType = response.headers.get("content-type");
4551
- if (contentType?.includes("application/json")) {
4552
- return await response.json();
4553
- }
4554
- return void 0;
4555
- } catch (error) {
4556
- clearTimeout(timeoutId);
4557
- if (error instanceof RaisinAbortError || error instanceof RaisinTimeoutError) {
4558
- throw error;
4559
- }
4560
- if (error instanceof Error && error.name === "AbortError") {
4561
- if (options.signal?.aborted) {
4562
- throw new RaisinAbortError();
4563
- }
4564
- throw new RaisinTimeoutError(
4565
- `Request timeout after ${this.requestTimeout}ms`,
4566
- "REQUEST_TIMEOUT",
4567
- this.requestTimeout
4568
- );
4569
- }
4570
- throw error;
4571
- }
4572
- }
4573
- };
4574
-
4575
- // src/database.ts
4576
- var Database = class _Database {
4577
- constructor(repository, context, sendRequest, eventHandler, branch, revision, getUploadManager, getSignAssetUrl, httpOptions) {
4578
- this.repository = repository;
4579
- this.branch = branch;
4580
- this.revision = revision;
4581
- this._context = {
4582
- ...context,
4583
- repository,
4584
- branch: branch || context.branch,
4585
- revision: revision || context.revision
4586
- };
4587
- this.sendRequest = (payload, requestType, contextOverride, requestOptions) => sendRequest(
4588
- payload,
4589
- requestType || "node_create",
4590
- contextOverride || this._context,
4591
- requestOptions
4592
- );
4593
- this.eventHandler = eventHandler;
4594
- this.getUploadManager = getUploadManager;
4595
- this.getSignAssetUrl = getSignAssetUrl;
4596
- this.httpOptions = httpOptions;
4597
- }
4598
- /**
4599
- * Get the repository name
3671
+ * Get the repository name
4600
3672
  */
4601
3673
  getRepository() {
4602
3674
  return this.repository;
@@ -4848,8 +3920,44 @@ var Database = class _Database {
4848
3920
  }
4849
3921
  return this._flowClient;
4850
3922
  }
3923
+ /**
3924
+ * Get a pre-configured InboxApi for human-in-the-loop tasks.
3925
+ *
3926
+ * Requires the Database to have been created via `RaisinClient.database()`
3927
+ * (which automatically provides HTTP context).
3928
+ *
3929
+ * @example
3930
+ * ```typescript
3931
+ * const db = client.database('my-repo');
3932
+ * const { tasks } = await db.inbox.listTasks({ status: 'pending' });
3933
+ * await db.inbox.completeTask(tasks[0].id, { action: 'approve' });
3934
+ * ```
3935
+ */
3936
+ get inbox() {
3937
+ if (!this._inboxApi) {
3938
+ if (!this.httpOptions?.httpBaseUrl || !this.httpOptions?.authManager) {
3939
+ throw new Error(
3940
+ "db.inbox requires HTTP context. Use client.database() to get a Database with inbox support."
3941
+ );
3942
+ }
3943
+ this._inboxApi = new InboxApi(
3944
+ this.httpOptions.httpBaseUrl,
3945
+ this.repository,
3946
+ this.httpOptions.authManager
3947
+ );
3948
+ }
3949
+ return this._inboxApi;
3950
+ }
4851
3951
  /**
4852
3952
  * Get EventHandler for subscribing to real-time events
3953
+ *
3954
+ * Path filter semantics for subscriptions (matched server-side):
3955
+ * - A plain path matches **only that exact node** — there is no implicit
3956
+ * prefix matching.
3957
+ * - `*` matches exactly one path segment (`/inbox/*` matches `/inbox/a`,
3958
+ * not `/inbox/a/b`).
3959
+ * - `**` matches recursively (`/inbox/**` matches all descendants of
3960
+ * `/inbox`, but not `/inbox` itself).
4853
3961
  */
4854
3962
  events() {
4855
3963
  return this.eventHandler;
@@ -4857,7 +3965,7 @@ var Database = class _Database {
4857
3965
  };
4858
3966
 
4859
3967
  // src/http-client.ts
4860
- import { EventEmitter as EventEmitter2 } from "events";
3968
+ import { EventEmitter } from "events";
4861
3969
 
4862
3970
  // src/upload/types.ts
4863
3971
  var UploadErrorCode = /* @__PURE__ */ ((UploadErrorCode2) => {
@@ -5220,7 +4328,7 @@ var BatchUploader = class {
5220
4328
  /**
5221
4329
  * Create a batch uploader
5222
4330
  */
5223
- constructor(config2, files, options) {
4331
+ constructor(config, files, options) {
5224
4332
  this.fileStates = /* @__PURE__ */ new Map();
5225
4333
  this.queue = [];
5226
4334
  this.speedTracker = new BatchSpeedTracker();
@@ -5230,7 +4338,7 @@ var BatchUploader = class {
5230
4338
  this.started = false;
5231
4339
  this.resolveWhenResumed = null;
5232
4340
  this.lastBytesUploaded = 0;
5233
- this.config = config2;
4341
+ this.config = config;
5234
4342
  this.files = files;
5235
4343
  this.options = {
5236
4344
  repository: options.repository,
@@ -5684,7 +4792,7 @@ var Uploader = class {
5684
4792
  /**
5685
4793
  * Create a new Uploader
5686
4794
  */
5687
- constructor(config2, fileSource, options, uploadId) {
4795
+ constructor(config, fileSource, options, uploadId) {
5688
4796
  this.session = null;
5689
4797
  this.status = "pending";
5690
4798
  this.bytesUploaded = 0;
@@ -5693,10 +4801,10 @@ var Uploader = class {
5693
4801
  this.speedTracker = new SpeedTracker();
5694
4802
  this.pauseRequested = false;
5695
4803
  this.abortController = null;
5696
- this.baseUrl = config2.baseUrl.replace(/\/$/, "");
5697
- this.authManager = config2.authManager;
5698
- this.fetchImpl = config2.fetchImpl ?? fetch;
5699
- this.requestTimeout = config2.requestTimeout ?? 3e4;
4804
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
4805
+ this.authManager = config.authManager;
4806
+ this.fetchImpl = config.fetchImpl ?? fetch;
4807
+ this.requestTimeout = config.requestTimeout ?? 3e4;
5700
4808
  this.fileSource = fileSource;
5701
4809
  this.id = uploadId ?? generateUploadId();
5702
4810
  this.options = {
@@ -6062,9 +5170,9 @@ var UploadManager = class {
6062
5170
  /**
6063
5171
  * Create an upload manager
6064
5172
  */
6065
- constructor(config2) {
5173
+ constructor(config) {
6066
5174
  this.uploads = /* @__PURE__ */ new Map();
6067
- this.config = config2;
5175
+ this.config = config;
6068
5176
  }
6069
5177
  /**
6070
5178
  * Create a new upload
@@ -6129,7 +5237,7 @@ var UploadManager = class {
6129
5237
  };
6130
5238
 
6131
5239
  // src/http-client.ts
6132
- var RaisinHttpClient = class extends EventEmitter2 {
5240
+ var RaisinHttpClient = class extends EventEmitter {
6133
5241
  constructor(baseUrl, options = {}) {
6134
5242
  super();
6135
5243
  this._currentUser = null;
@@ -6177,9 +5285,13 @@ var RaisinHttpClient = class extends EventEmitter2 {
6177
5285
  body: payload,
6178
5286
  skipAuth: true
6179
5287
  });
6180
- this.authManager.setTokens(response.data);
5288
+ if (response.data.token) {
5289
+ this.authManager.storage.setAccessToken(response.data.token);
5290
+ } else {
5291
+ this.authManager.setTokens(response.data);
5292
+ }
6181
5293
  this._currentUser = {
6182
- userId: "admin",
5294
+ userId: response.data.user_id ?? "admin",
6183
5295
  anonymous: false
6184
5296
  };
6185
5297
  this.emit("authenticated", response.data);
@@ -6369,14 +5481,14 @@ var RaisinHttpClient = class extends EventEmitter2 {
6369
5481
  * @param config - Repository configuration
6370
5482
  * @returns Repository creation result
6371
5483
  */
6372
- async createRepository(repositoryId, description, config2) {
5484
+ async createRepository(repositoryId, description, config) {
6373
5485
  const response = await this.request({
6374
5486
  method: "POST",
6375
5487
  path: "/api/repositories",
6376
5488
  body: {
6377
5489
  repository_id: repositoryId,
6378
5490
  description,
6379
- config: config2
5491
+ config
6380
5492
  }
6381
5493
  });
6382
5494
  return response.data;
@@ -6414,13 +5526,13 @@ var RaisinHttpClient = class extends EventEmitter2 {
6414
5526
  * @param config - New repository configuration
6415
5527
  * @returns Repository update result
6416
5528
  */
6417
- async updateRepository(repositoryId, description, config2) {
5529
+ async updateRepository(repositoryId, description, config) {
6418
5530
  const response = await this.request({
6419
5531
  method: "PUT",
6420
5532
  path: `/api/repositories/${repositoryId}`,
6421
5533
  body: {
6422
5534
  description,
6423
- config: config2
5535
+ config
6424
5536
  }
6425
5537
  });
6426
5538
  return response.data;
@@ -7067,7 +6179,7 @@ var HttpWorkspaceClient = class {
7067
6179
  };
7068
6180
 
7069
6181
  // src/client.ts
7070
- var RaisinClient = class _RaisinClient extends EventEmitter3 {
6182
+ var _RaisinClient = class _RaisinClient extends EventEmitter2 {
7071
6183
  constructor(url, options = {}) {
7072
6184
  super();
7073
6185
  this._currentUser = null;
@@ -7081,23 +6193,41 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7081
6193
  // Reconnection tracking
7082
6194
  this._previousConnectionState = "disconnected" /* Disconnected */;
7083
6195
  this._reconnectedListeners = /* @__PURE__ */ new Set();
6196
+ this._everConnected = false;
6197
+ // Requests queued while the connection is re-establishing.
6198
+ // Flushed after reconnect + re-auth; bounded by MAX_QUEUED_REQUESTS.
6199
+ this._queuedRequests = [];
6200
+ /**
6201
+ * Backoff delays (ms) between subscription-restore retries after reconnect.
6202
+ * @internal - overridable in tests
6203
+ */
6204
+ this._restoreRetryDelaysMs = [1e3, 3e3, 9e3];
7084
6205
  // Upload manager (uses HTTP under the hood)
7085
6206
  this._uploadManager = null;
7086
6207
  if (options.logLevel !== void 0) {
7087
6208
  setLogLevel(options.logLevel);
7088
6209
  }
6210
+ const connectionUrl = this.buildConnectionUrl(url, options.repository);
7089
6211
  this.options = {
7090
6212
  requestTimeout: options.requestTimeout ?? 3e4,
7091
- tenantId: options.tenantId ?? this.extractTenantFromUrl(url),
6213
+ tenantId: options.tenantId ?? this.extractTenantFromUrl(connectionUrl),
7092
6214
  defaultBranch: options.defaultBranch ?? "main"
7093
6215
  };
7094
- this._httpBaseUrl = options.httpBaseUrl ?? this.deriveHttpUrl(url);
7095
- this._repository = this.extractRepositoryFromUrl(url);
6216
+ this._httpBaseUrl = options.httpBaseUrl ?? this.deriveHttpUrl(connectionUrl);
6217
+ this._repository = options.repository ?? this.extractRepositoryFromUrl(connectionUrl);
7096
6218
  this._context = {
7097
6219
  tenant_id: this.options.tenantId,
7098
6220
  branch: this.options.defaultBranch
7099
6221
  };
7100
- this.connection = new Connection(url, options.connection);
6222
+ const explicitTenant = options.tenantId ?? (this.extractTenantFromUrl(url) !== "default" ? this.options.tenantId : void 0);
6223
+ const connectionOptions = {
6224
+ ...options.connection,
6225
+ headers: {
6226
+ ...explicitTenant ? { "x-tenant-id": explicitTenant } : {},
6227
+ ...options.connection?.headers
6228
+ }
6229
+ };
6230
+ this.connection = this.createConnection(connectionUrl, connectionOptions);
7101
6231
  this.requestTracker = new RequestTracker({
7102
6232
  defaultTimeout: this.options.requestTimeout
7103
6233
  });
@@ -7107,6 +6237,14 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7107
6237
  );
7108
6238
  this.setupConnectionHandlers();
7109
6239
  }
6240
+ /**
6241
+ * Create the underlying WebSocket connection.
6242
+ * Seam for tests to inject a fake connection.
6243
+ * @internal
6244
+ */
6245
+ createConnection(url, options) {
6246
+ return new Connection(url, options);
6247
+ }
7110
6248
  /**
7111
6249
  * Derive HTTP URL from WebSocket URL
7112
6250
  * ws://host:port/path -> http://host:port
@@ -7122,15 +6260,45 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7122
6260
  return "http://localhost:8081";
7123
6261
  }
7124
6262
  }
6263
+ /**
6264
+ * Build the WebSocket connection URL.
6265
+ *
6266
+ * When a repository option is provided and the URL carries no path
6267
+ * (e.g. `ws://host:8081`), build the tenant-less `/ws/{repository}`
6268
+ * route internally. URLs that already carry a path are used as-is.
6269
+ */
6270
+ buildConnectionUrl(url, repository) {
6271
+ if (!repository) {
6272
+ return url;
6273
+ }
6274
+ try {
6275
+ const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
6276
+ const parsed = new URL(wsUrl);
6277
+ const hasPath = parsed.pathname.split("/").some((p) => p.length > 0);
6278
+ if (hasPath) {
6279
+ return url;
6280
+ }
6281
+ return `${url.replace(/\/+$/, "")}/ws/${repository}`;
6282
+ } catch {
6283
+ return url;
6284
+ }
6285
+ }
7125
6286
  /**
7126
6287
  * Extract repository from URL
7127
- * URL format: raisin://host:port/tenant/repository or ws://host:port/tenant/repository
6288
+ * URL forms: /ws/{repo} (tenant-less, preferred), /sys/{tenant}/{repo}
6289
+ * (operator/multi-tenant), /{tenant}/{repo} (legacy).
7128
6290
  */
7129
6291
  extractRepositoryFromUrl(url) {
7130
6292
  try {
7131
6293
  const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
7132
6294
  const parsed = new URL(wsUrl);
7133
6295
  const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
6296
+ if (parts[0] === "ws") {
6297
+ return parts.length > 1 ? parts[1] : "";
6298
+ }
6299
+ if (parts[0] === "sys") {
6300
+ return parts.length > 2 ? parts[2] : "";
6301
+ }
7134
6302
  return parts.length > 1 ? parts[1] : "";
7135
6303
  } catch {
7136
6304
  return "";
@@ -7138,14 +6306,24 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7138
6306
  }
7139
6307
  /**
7140
6308
  * Extract tenant ID from URL
7141
- * URL format: raisin://host:port/tenant/repository or ws://host:port/tenant/repository
6309
+ * URL forms: /ws/{repo} (tenant-less — no tenant in URL),
6310
+ * /sys/{tenant}/{repo} (canonical), /{tenant}/{repo} (legacy).
6311
+ * When the URL doesn't carry a tenant, fall back to 'default' — the
6312
+ * server's tenant middleware resolves it anyway (production derives the
6313
+ * tenant from the request itself), so an explicit tenantId is optional.
7142
6314
  */
7143
6315
  extractTenantFromUrl(url) {
7144
6316
  try {
7145
6317
  const wsUrl = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
7146
6318
  const parsed = new URL(wsUrl);
7147
6319
  const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
7148
- return parts.length > 0 ? parts[0] : "default";
6320
+ if (parts[0] === "ws") {
6321
+ return "default";
6322
+ }
6323
+ if (parts[0] === "sys") {
6324
+ return parts.length > 1 ? parts[1] : "default";
6325
+ }
6326
+ return parts.length >= 2 ? parts[0] : "default";
7149
6327
  } catch (error) {
7150
6328
  return "default";
7151
6329
  }
@@ -7159,7 +6337,11 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7159
6337
  });
7160
6338
  this.connection.on("stateChange", (state) => {
7161
6339
  const wasDisconnected = this._previousConnectionState === "disconnected" /* Disconnected */ || this._previousConnectionState === "reconnecting" /* Reconnecting */;
6340
+ if (state === "connected" /* Connected */) {
6341
+ this._everConnected = true;
6342
+ }
7162
6343
  if (state === "disconnected" /* Disconnected */ || state === "closed" /* Closed */) {
6344
+ this._queuedRequests = [];
7163
6345
  this.requestTracker.cancelAll();
7164
6346
  }
7165
6347
  if (state === "connected" /* Connected */ && wasDisconnected && this._previousConnectionState !== "disconnected" /* Disconnected */) {
@@ -7224,19 +6406,36 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7224
6406
  };
7225
6407
  logger.debug(`Anonymous user info stored: ${message.user_id}`);
7226
6408
  this._updateReadyState();
6409
+ this._flushQueuedRequests();
7227
6410
  if (hasActiveSubscriptions) {
7228
6411
  this._restoreSubscriptionsAndNotify();
7229
6412
  }
7230
6413
  } else {
7231
6414
  logger.info(`[handleConnectedMessage] Stored token found, auto-authenticating...`);
7232
- this.autoReauthenticate().then(async () => {
6415
+ this.autoReauthenticate().then(async (authenticated) => {
6416
+ if (authenticated) {
6417
+ this._flushQueuedRequests();
6418
+ } else {
6419
+ this._rejectQueuedRequests(
6420
+ new RaisinAuthError(
6421
+ "Re-authentication failed after reconnect",
6422
+ "AUTH_TOKEN_EXPIRED",
6423
+ 401
6424
+ )
6425
+ );
6426
+ }
7233
6427
  if (hasActiveSubscriptions) {
7234
6428
  await this._restoreSubscriptionsAndNotify();
7235
6429
  }
7236
6430
  }).catch((err) => {
7237
6431
  logger.error(`[handleConnectedMessage] Auto-authenticate failed:`, err);
6432
+ this._rejectQueuedRequests(
6433
+ err instanceof Error ? err : new Error("Re-authentication failed after reconnect")
6434
+ );
7238
6435
  });
7239
6436
  }
6437
+ } else {
6438
+ this._flushQueuedRequests();
7240
6439
  }
7241
6440
  this.emit("connected", {
7242
6441
  connectionId: message.connection_id,
@@ -7245,18 +6444,41 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7245
6444
  });
7246
6445
  }
7247
6446
  /**
7248
- * Restore subscriptions after reconnection and notify listeners
6447
+ * Restore subscriptions after reconnection and notify listeners.
6448
+ *
6449
+ * Retries with backoff (see `_restoreRetryDelaysMs`); on final failure a
6450
+ * `subscription_restore_failed` event is emitted so applications can
6451
+ * resync their state (e.g. reload lists and re-subscribe manually).
7249
6452
  * @internal
7250
6453
  */
7251
6454
  async _restoreSubscriptionsAndNotify() {
7252
- try {
7253
- logger.info("[_restoreSubscriptionsAndNotify] Restoring subscriptions...");
7254
- await this.eventHandler.restoreSubscriptions();
7255
- logger.info("[_restoreSubscriptionsAndNotify] Subscriptions restored successfully");
7256
- this._emitReconnected();
7257
- } catch (error) {
7258
- logger.error("[_restoreSubscriptionsAndNotify] Failed to restore subscriptions:", error);
6455
+ const delays = this._restoreRetryDelaysMs;
6456
+ let lastError;
6457
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
6458
+ try {
6459
+ logger.info(
6460
+ `[_restoreSubscriptionsAndNotify] Restoring subscriptions (attempt ${attempt + 1}/${delays.length + 1})...`
6461
+ );
6462
+ await this.eventHandler.restoreSubscriptions({ retry: attempt > 0 });
6463
+ logger.info("[_restoreSubscriptionsAndNotify] Subscriptions restored successfully");
6464
+ this._emitReconnected();
6465
+ return;
6466
+ } catch (error) {
6467
+ lastError = error;
6468
+ logger.error(
6469
+ `[_restoreSubscriptionsAndNotify] Restore attempt ${attempt + 1} failed:`,
6470
+ error
6471
+ );
6472
+ if (attempt < delays.length) {
6473
+ await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
6474
+ }
6475
+ }
7259
6476
  }
6477
+ logger.error("[_restoreSubscriptionsAndNotify] Giving up after retries; events may be stale");
6478
+ this.emit(
6479
+ "subscription_restore_failed",
6480
+ lastError instanceof Error ? lastError : new Error(String(lastError))
6481
+ );
7260
6482
  }
7261
6483
  /**
7262
6484
  * Emit reconnected event to all listeners
@@ -7275,13 +6497,15 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7275
6497
  }
7276
6498
  /**
7277
6499
  * Auto-reauthenticate with stored JWT on reconnection
6500
+ *
6501
+ * @returns true if the session was re-authenticated, false otherwise
7278
6502
  * @internal
7279
6503
  */
7280
6504
  async autoReauthenticate() {
7281
6505
  const token = this.authManager.storage.getAccessToken();
7282
6506
  if (!token) {
7283
6507
  logger.warn("[autoReauthenticate] No stored token");
7284
- return;
6508
+ return false;
7285
6509
  }
7286
6510
  const payload = AuthManager.parseToken(token);
7287
6511
  if (payload?.exp && typeof payload.exp === "number") {
@@ -7293,18 +6517,20 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7293
6517
  this.authManager.clear();
7294
6518
  this._currentUser = null;
7295
6519
  this._emitAuthEvent("SESSION_EXPIRED");
7296
- return;
6520
+ return false;
7297
6521
  }
7298
6522
  const newToken = this.authManager.storage.getAccessToken();
7299
6523
  if (newToken) {
7300
6524
  await this.authenticate({ type: "jwt", token: newToken });
6525
+ return true;
7301
6526
  }
7302
- return;
6527
+ return false;
7303
6528
  }
7304
6529
  }
7305
6530
  try {
7306
6531
  await this.authenticate({ type: "jwt", token });
7307
6532
  logger.info("[autoReauthenticate] Successfully re-authenticated");
6533
+ return true;
7308
6534
  } catch (err) {
7309
6535
  logger.error("[autoReauthenticate] Failed to authenticate:", err);
7310
6536
  const refreshed = await this.refreshToken();
@@ -7312,11 +6538,14 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7312
6538
  const newToken = this.authManager.storage.getAccessToken();
7313
6539
  if (newToken) {
7314
6540
  await this.authenticate({ type: "jwt", token: newToken });
6541
+ return true;
7315
6542
  }
6543
+ return false;
7316
6544
  } else {
7317
6545
  this.authManager.clear();
7318
6546
  this._currentUser = null;
7319
6547
  this._emitAuthEvent("SESSION_EXPIRED");
6548
+ return false;
7320
6549
  }
7321
6550
  }
7322
6551
  }
@@ -7432,18 +6661,18 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7432
6661
  }
7433
6662
  const url = `${this._httpBaseUrl}/auth/${repository}/login`;
7434
6663
  logger.debug("[loginWithEmail] POST", url);
7435
- const response = await fetch(url, {
6664
+ const response = await this._fetchWithTimeout(url, {
7436
6665
  method: "POST",
7437
6666
  headers: { "Content-Type": "application/json" },
7438
6667
  body: JSON.stringify({ email, password })
7439
6668
  });
7440
6669
  if (!response.ok) {
7441
6670
  const error = await response.json().catch(() => ({ message: response.statusText }));
7442
- const authError = {
7443
- code: error.code || "LOGIN_FAILED",
7444
- message: error.message || "Login failed"
7445
- };
7446
- throw authError;
6671
+ throw new RaisinAuthError(
6672
+ error.message || "Login failed",
6673
+ error.code || "LOGIN_FAILED",
6674
+ response.status
6675
+ );
7447
6676
  }
7448
6677
  const tokens = await response.json();
7449
6678
  logger.info("[loginWithEmail] HTTP login successful, user_id:", tokens.identity.id);
@@ -7476,7 +6705,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7476
6705
  anonymous: false,
7477
6706
  node: userNode ?? void 0
7478
6707
  };
7479
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
6708
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7480
6709
  if (expiresInMs > 0) {
7481
6710
  this.scheduleAutoRefresh(expiresInMs);
7482
6711
  }
@@ -7500,18 +6729,18 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7500
6729
  async registerWithEmail(email, password, repository, displayName) {
7501
6730
  logger.info("[registerWithEmail] Starting registration...");
7502
6731
  const url = `${this._httpBaseUrl}/auth/${repository}/register`;
7503
- const response = await fetch(url, {
6732
+ const response = await this._fetchWithTimeout(url, {
7504
6733
  method: "POST",
7505
6734
  headers: { "Content-Type": "application/json" },
7506
6735
  body: JSON.stringify({ email, password, display_name: displayName })
7507
6736
  });
7508
6737
  if (!response.ok) {
7509
6738
  const error = await response.json().catch(() => ({ message: response.statusText }));
7510
- const authError = {
7511
- code: error.code || "REGISTRATION_FAILED",
7512
- message: error.message || "Registration failed"
7513
- };
7514
- throw authError;
6739
+ throw new RaisinAuthError(
6740
+ error.message || "Registration failed",
6741
+ error.code || "REGISTRATION_FAILED",
6742
+ response.status
6743
+ );
7515
6744
  }
7516
6745
  const tokens = await response.json();
7517
6746
  logger.info("[registerWithEmail] Registration successful, user_id:", tokens.identity.id);
@@ -7542,7 +6771,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7542
6771
  anonymous: false,
7543
6772
  node: userNode ?? void 0
7544
6773
  };
7545
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
6774
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7546
6775
  if (expiresInMs > 0) {
7547
6776
  this.scheduleAutoRefresh(expiresInMs);
7548
6777
  }
@@ -7638,7 +6867,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7638
6867
  if (refreshToken) {
7639
6868
  try {
7640
6869
  const url = this._repository ? `${this._httpBaseUrl}/auth/${this._repository}/refresh` : `${this._httpBaseUrl}/auth/refresh`;
7641
- const response = await fetch(url, {
6870
+ const response = await this._fetchWithTimeout(url, {
7642
6871
  method: "POST",
7643
6872
  headers: { "Content-Type": "application/json" },
7644
6873
  body: JSON.stringify({ refresh_token: refreshToken })
@@ -7647,7 +6876,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7647
6876
  const tokens = await response.json();
7648
6877
  this.authManager.storage.setAccessToken(tokens.access_token);
7649
6878
  this.authManager.storage.setRefreshToken(tokens.refresh_token);
7650
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
6879
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7651
6880
  if (expiresInMs > 0) {
7652
6881
  this.scheduleAutoRefresh(expiresInMs);
7653
6882
  }
@@ -7764,7 +6993,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7764
6993
  try {
7765
6994
  logger.info("[refreshToken] Refreshing access token...");
7766
6995
  const url = this._repository ? `${this._httpBaseUrl}/auth/${this._repository}/refresh` : `${this._httpBaseUrl}/auth/refresh`;
7767
- const response = await fetch(url, {
6996
+ const response = await this._fetchWithTimeout(url, {
7768
6997
  method: "POST",
7769
6998
  headers: { "Content-Type": "application/json" },
7770
6999
  body: JSON.stringify({ refresh_token: refreshToken })
@@ -7794,7 +7023,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7794
7023
  if (this.authManager.storage instanceof LocalStorageTokenStorage) {
7795
7024
  this.authManager.storage.setUser(user);
7796
7025
  }
7797
- const expiresInMs = tokens.expires_at * 1e3 - Date.now() - 5 * 60 * 1e3;
7026
+ const expiresInMs = _RaisinClient.expiresAtToMs(tokens.expires_at) - Date.now() - 5 * 60 * 1e3;
7798
7027
  if (expiresInMs > 0) {
7799
7028
  this.scheduleAutoRefresh(expiresInMs);
7800
7029
  }
@@ -7808,18 +7037,66 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7808
7037
  }
7809
7038
  }
7810
7039
  /**
7811
- * Schedule automatic token refresh
7040
+ * Fetch with a timeout derived from `requestTimeout`.
7041
+ *
7042
+ * Aborts the request after `requestTimeout` ms and rejects with a
7043
+ * `RaisinTimeoutError`. If the caller provides its own AbortSignal in
7044
+ * `init.signal`, it is respected: an external abort rejects with the
7045
+ * original AbortError instead of a timeout error.
7812
7046
  * @internal
7813
7047
  */
7814
- scheduleAutoRefresh(delayMs) {
7815
- if (this._refreshTimer) {
7816
- clearTimeout(this._refreshTimer);
7048
+ async _fetchWithTimeout(url, init = {}) {
7049
+ const timeoutMs = this.options.requestTimeout;
7050
+ const controller = new AbortController();
7051
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
7052
+ const externalSignal = init.signal ?? void 0;
7053
+ if (externalSignal) {
7054
+ if (externalSignal.aborted) {
7055
+ controller.abort();
7056
+ } else {
7057
+ externalSignal.addEventListener("abort", () => controller.abort(), { once: true });
7058
+ }
7817
7059
  }
7818
- logger.debug(`[scheduleAutoRefresh] Scheduling refresh in ${Math.round(delayMs / 1e3 / 60)} minutes`);
7819
- this._refreshTimer = setTimeout(async () => {
7820
- logger.info("[autoRefresh] Auto-refreshing token...");
7821
- await this.refreshToken();
7822
- }, delayMs);
7060
+ try {
7061
+ return await fetch(url, { ...init, signal: controller.signal });
7062
+ } catch (error) {
7063
+ if (error instanceof Error && error.name === "AbortError" && !externalSignal?.aborted) {
7064
+ throw new RaisinTimeoutError(
7065
+ `Request to ${url} timed out after ${timeoutMs}ms`,
7066
+ "REQUEST_TIMEOUT",
7067
+ timeoutMs
7068
+ );
7069
+ }
7070
+ throw error;
7071
+ } finally {
7072
+ clearTimeout(timer);
7073
+ }
7074
+ }
7075
+ /**
7076
+ * Normalize a token `expires_at` value to epoch milliseconds.
7077
+ *
7078
+ * Server endpoints are inconsistent: some return seconds, others
7079
+ * milliseconds. Anything above 1e11 can't be a seconds-epoch (year 5138),
7080
+ * so treat it as milliseconds.
7081
+ * @internal
7082
+ */
7083
+ static expiresAtToMs(expiresAt) {
7084
+ return expiresAt > 1e11 ? expiresAt : expiresAt * 1e3;
7085
+ }
7086
+ /**
7087
+ * Schedule automatic token refresh
7088
+ * @internal
7089
+ */
7090
+ scheduleAutoRefresh(delayMs) {
7091
+ if (this._refreshTimer) {
7092
+ clearTimeout(this._refreshTimer);
7093
+ }
7094
+ const clamped = Math.min(Math.max(delayMs, 3e4), 2147483647);
7095
+ logger.debug(`[scheduleAutoRefresh] Scheduling refresh in ${Math.round(clamped / 1e3 / 60)} minutes`);
7096
+ this._refreshTimer = setTimeout(async () => {
7097
+ logger.info("[autoRefresh] Auto-refreshing token...");
7098
+ await this.refreshToken();
7099
+ }, clamped);
7823
7100
  }
7824
7101
  /**
7825
7102
  * Get a database/repository interface
@@ -7843,10 +7120,26 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7843
7120
  }
7844
7121
  /**
7845
7122
  * Send a request to the server
7123
+ *
7124
+ * If the connection is temporarily down (connecting/reconnecting, or
7125
+ * disconnected with auto-reconnect enabled), the request is queued and
7126
+ * flushed once the connection is re-established and re-authenticated.
7127
+ * Queued requests are still subject to their own request timeout, and the
7128
+ * queue is bounded (overflow rejects immediately).
7846
7129
  */
7847
7130
  async sendRequestInternal(payload, requestType, contextOverride, requestOptions) {
7848
- if (!this.connection.isConnected()) {
7849
- throw new Error("Not connected to server");
7131
+ const connected = this.connection.isConnected();
7132
+ if (!connected) {
7133
+ const state = this.connection.getState();
7134
+ const willReconnect = state === "connecting" /* Connecting */ || state === "reconnecting" /* Reconnecting */ || state === "disconnected" /* Disconnected */ && this._everConnected && this.connection.isAutoReconnectEnabled();
7135
+ if (!willReconnect) {
7136
+ throw new Error("Not connected to server");
7137
+ }
7138
+ if (this._queuedRequests.length >= _RaisinClient.MAX_QUEUED_REQUESTS) {
7139
+ throw new Error(
7140
+ `Request queue is full (${_RaisinClient.MAX_QUEUED_REQUESTS} requests pending while reconnecting)`
7141
+ );
7142
+ }
7850
7143
  }
7851
7144
  const requestId = this.requestTracker.generateRequestId();
7852
7145
  const context = contextOverride || this._context;
@@ -7865,8 +7158,15 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7865
7158
  try {
7866
7159
  const encoded = encodeMessage(request);
7867
7160
  logger.debug(`Encoded request - ID: ${requestId}, type: ${requestType}, size: ${encoded.length} bytes`);
7868
- this.connection.send(encoded);
7869
- logger.debug(`Request sent - ID: ${requestId}`);
7161
+ if (connected) {
7162
+ this.connection.send(encoded);
7163
+ logger.debug(`Request sent - ID: ${requestId}`);
7164
+ } else {
7165
+ this._queuedRequests.push({ requestId, encoded });
7166
+ logger.debug(
7167
+ `Request queued while reconnecting - ID: ${requestId}, queue size: ${this._queuedRequests.length}`
7168
+ );
7169
+ }
7870
7170
  } catch (error) {
7871
7171
  logger.error(`Failed to send request - ID: ${requestId}:`, error);
7872
7172
  this.requestTracker.rejectRequest(
@@ -7877,6 +7177,42 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
7877
7177
  }
7878
7178
  return responsePromise;
7879
7179
  }
7180
+ /**
7181
+ * Flush requests queued while the connection was re-establishing.
7182
+ * Called after the connection is connected and (re-)authenticated.
7183
+ * @internal
7184
+ */
7185
+ _flushQueuedRequests() {
7186
+ if (this._queuedRequests.length === 0) return;
7187
+ const queued = this._queuedRequests;
7188
+ this._queuedRequests = [];
7189
+ logger.info(`[_flushQueuedRequests] Flushing ${queued.length} queued request(s)`);
7190
+ for (const { requestId, encoded } of queued) {
7191
+ if (!this.requestTracker.hasPendingRequest(requestId)) continue;
7192
+ try {
7193
+ this.connection.send(encoded);
7194
+ } catch (error) {
7195
+ this.requestTracker.rejectRequest(
7196
+ requestId,
7197
+ error instanceof Error ? error : new Error("Failed to send queued request")
7198
+ );
7199
+ }
7200
+ }
7201
+ }
7202
+ /**
7203
+ * Reject all requests queued while the connection was re-establishing
7204
+ * (e.g. re-authentication failed after reconnect).
7205
+ * @internal
7206
+ */
7207
+ _rejectQueuedRequests(error) {
7208
+ if (this._queuedRequests.length === 0) return;
7209
+ const queued = this._queuedRequests;
7210
+ this._queuedRequests = [];
7211
+ logger.warn(`[_rejectQueuedRequests] Rejecting ${queued.length} queued request(s): ${error.message}`);
7212
+ for (const { requestId } of queued) {
7213
+ this.requestTracker.rejectRequest(requestId, error);
7214
+ }
7215
+ }
7880
7216
  /**
7881
7217
  * Get the HTTP base URL derived from the WebSocket URL
7882
7218
  */
@@ -8202,6 +7538,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
8202
7538
  */
8203
7539
  async _subscribeToUserHome(repository, userHome) {
8204
7540
  await this._unsubscribeFromUserHome();
7541
+ userHome = normalizeHomePath(userHome) ?? userHome;
8205
7542
  try {
8206
7543
  const db = this.database(repository);
8207
7544
  const pathParts = userHome.split("/").filter((p) => p.length > 0);
@@ -8254,12 +7591,12 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
8254
7591
  * @param config - Repository configuration
8255
7592
  * @returns Repository creation result
8256
7593
  */
8257
- async createRepository(repositoryId, description, config2) {
7594
+ async createRepository(repositoryId, description, config) {
8258
7595
  return this.sendRequestInternal(
8259
7596
  {
8260
7597
  repository_id: repositoryId,
8261
7598
  description,
8262
- config: config2
7599
+ config
8263
7600
  },
8264
7601
  "repository_create" /* RepositoryCreate */
8265
7602
  );
@@ -8298,12 +7635,12 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
8298
7635
  * @param config - New repository configuration
8299
7636
  * @returns Repository update result
8300
7637
  */
8301
- async updateRepository(repositoryId, description, config2) {
7638
+ async updateRepository(repositoryId, description, config) {
8302
7639
  return this.sendRequestInternal(
8303
7640
  {
8304
7641
  repository_id: repositoryId,
8305
7642
  description,
8306
- config: config2
7643
+ config
8307
7644
  },
8308
7645
  "repository_update" /* RepositoryUpdate */
8309
7646
  );
@@ -8501,7 +7838,7 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
8501
7838
  const branch = options.branch ?? this._context.branch ?? "main";
8502
7839
  const pathWithProperty = options.propertyPath ? `${options.path}@${options.propertyPath}` : options.path;
8503
7840
  const endpoint = `/api/repository/${options.repository}/${branch}/head/${options.workspace}${pathWithProperty}/raisin:sign`;
8504
- const response = await fetch(`${this._httpBaseUrl}${endpoint}`, {
7841
+ const response = await this._fetchWithTimeout(`${this._httpBaseUrl}${endpoint}`, {
8505
7842
  method: "POST",
8506
7843
  headers: {
8507
7844
  "Content-Type": "application/json",
@@ -8556,6 +7893,9 @@ var RaisinClient = class _RaisinClient extends EventEmitter3 {
8556
7893
  return _RaisinClient.forSSR(baseUrl, options);
8557
7894
  }
8558
7895
  };
7896
+ /** Hard cap on requests held while reconnecting; overflow rejects immediately. */
7897
+ _RaisinClient.MAX_QUEUED_REQUESTS = 100;
7898
+ var RaisinClient = _RaisinClient;
8559
7899
 
8560
7900
  // src/node-builder.ts
8561
7901
  var NodeBuilder = class _NodeBuilder {
@@ -8840,17 +8180,17 @@ var NodeEventType = {
8840
8180
  var AllNodeEventTypes = Object.values(NodeEventType);
8841
8181
 
8842
8182
  // src/integrations/react-router.ts
8843
- function createSSRClient(config2) {
8844
- const client = RaisinClient.forSSR(config2.httpBaseUrl, config2.httpOptions);
8845
- if (config2.credentials) {
8183
+ function createSSRClient(config) {
8184
+ const client = RaisinClient.forSSR(config.httpBaseUrl, config.httpOptions);
8185
+ if (config.credentials) {
8846
8186
  }
8847
8187
  return client;
8848
8188
  }
8849
- function createLoader(config2, loaderFn) {
8189
+ function createLoader(config, loaderFn) {
8850
8190
  return async (args) => {
8851
- const client = createSSRClient(config2);
8852
- if (config2.credentials) {
8853
- await client.authenticate(config2.credentials);
8191
+ const client = createSSRClient(config);
8192
+ if (config.credentials) {
8193
+ await client.authenticate(config.credentials);
8854
8194
  }
8855
8195
  try {
8856
8196
  return await loaderFn(client, args);
@@ -8890,1846 +8230,27 @@ function isTerminalEvent(event) {
8890
8230
  return event.type === "flow_completed" || event.type === "flow_failed";
8891
8231
  }
8892
8232
 
8893
- // src/utils/plan-projection.ts
8894
- function asRecord(value) {
8895
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
8896
- return value;
8897
- }
8898
- function asString(value) {
8899
- return typeof value === "string" && value.length > 0 ? value : void 0;
8900
- }
8901
- function parsePlanTasks(rawTasks) {
8902
- if (!Array.isArray(rawTasks)) return [];
8903
- return rawTasks.map((rawTask, index) => {
8904
- if (typeof rawTask === "string") {
8905
- return {
8906
- title: rawTask,
8907
- status: "pending"
8908
- };
8909
- }
8910
- const task = asRecord(rawTask);
8911
- if (!task) return null;
8912
- return {
8913
- taskId: asString(task.task_id) ?? asString(task.id),
8914
- title: asString(task.title) ?? `Task ${index + 1}`,
8915
- status: asString(task.status) ?? "pending",
8916
- description: asString(task.description),
8917
- priority: asString(task.priority)
8918
- };
8919
- }).filter((task) => task !== null);
8920
- }
8921
- function upsertTask(plan, nextTask) {
8922
- const existingIndex = plan.tasks.findIndex(
8923
- (task) => nextTask.taskId && task.taskId === nextTask.taskId || !nextTask.taskId && task.title === nextTask.title
8924
- );
8925
- if (existingIndex === -1) {
8926
- plan.tasks.push(nextTask);
8927
- return;
8928
- }
8929
- plan.tasks[existingIndex] = {
8930
- ...plan.tasks[existingIndex],
8931
- ...nextTask
8932
- };
8233
+ // src/types/flow-definition.ts
8234
+ function getRefDisplayName(ref) {
8235
+ const path = ref["raisin:path"] || ref["raisin:ref"];
8236
+ const segments = path.split("/").filter(Boolean);
8237
+ return segments[segments.length - 1] || path;
8933
8238
  }
8934
- function ensurePlan(plans, key, fallbackTitle, timestamp) {
8935
- const existing = plans.get(key);
8936
- if (existing) {
8937
- if (timestamp) existing.updatedAt = timestamp;
8938
- return existing;
8939
- }
8940
- const created = {
8941
- key,
8942
- title: fallbackTitle,
8943
- status: "pending_approval",
8944
- requiresApproval: true,
8945
- tasks: [],
8946
- updatedAt: timestamp
8947
- };
8948
- plans.set(key, created);
8949
- return created;
8950
- }
8951
- function projectPlansFromMessages(messages) {
8952
- const ordered = [...messages].sort((a, b) => {
8953
- const aTime = Date.parse(a.timestamp || "");
8954
- const bTime = Date.parse(b.timestamp || "");
8955
- return (Number.isNaN(aTime) ? 0 : aTime) - (Number.isNaN(bTime) ? 0 : bTime);
8956
- });
8957
- const plans = /* @__PURE__ */ new Map();
8958
- for (const message of ordered) {
8959
- const timestamp = message.timestamp;
8960
- const data = asRecord(message.data);
8961
- if (message.messageType === "ai_plan" && data) {
8962
- const planPath = asString(data.plan_path);
8963
- const planId = asString(data.plan_id);
8964
- const key = planPath || planId || message.path || message.id || `plan:${timestamp}`;
8965
- const plan = ensurePlan(
8966
- plans,
8967
- key,
8968
- asString(data.title) || message.content || "Plan",
8969
- timestamp
8970
- );
8971
- plan.planPath = planPath ?? plan.planPath;
8972
- plan.planId = planId ?? plan.planId;
8973
- plan.title = asString(data.title) || plan.title;
8974
- plan.status = asString(data.status) || plan.status;
8975
- plan.requiresApproval = Boolean(data.requires_approval) || plan.status === "pending_approval";
8976
- plan.sourceMessagePath = message.path ?? plan.sourceMessagePath;
8977
- const parsedTasks = parsePlanTasks(data.tasks);
8978
- if (parsedTasks.length > 0) {
8979
- plan.tasks = parsedTasks;
8980
- }
8981
- }
8982
- if (message.messageType === "ai_task_update" && data) {
8983
- const planPath = asString(data.plan_path);
8984
- const planId = asString(data.plan_id);
8985
- const key = planPath || planId;
8986
- if (!key) continue;
8987
- const plan = ensurePlan(plans, key, "Plan", timestamp);
8988
- plan.planPath = planPath ?? plan.planPath;
8989
- plan.planId = planId ?? plan.planId;
8990
- plan.status = asString(data.plan_status) || plan.status;
8991
- plan.requiresApproval = plan.status === "pending_approval";
8992
- const taskUpdate = {
8993
- taskId: asString(data.task_id),
8994
- title: asString(data.task_title) || asString(data.title) || "Task",
8995
- status: asString(data.status) || "pending"
8996
- };
8997
- upsertTask(plan, taskUpdate);
8998
- }
8999
- if (Array.isArray(message.children)) {
9000
- for (const child of message.children) {
9001
- if (child.type !== "plan") continue;
9002
- const key = child.path || message.path || message.id || `plan-child:${child.id}:${timestamp}`;
9003
- const plan = ensurePlan(plans, key, child.planTitle || message.content || "Plan", timestamp);
9004
- plan.title = child.planTitle || plan.title;
9005
- plan.status = child.status || plan.status;
9006
- plan.requiresApproval = plan.status === "pending_approval";
9007
- if (Array.isArray(child.tasks) && child.tasks.length > 0) {
9008
- plan.tasks = child.tasks.map((task) => ({
9009
- taskId: task.id,
9010
- title: task.title,
9011
- status: task.status
9012
- }));
9013
- }
9014
- }
9015
- }
9016
- }
9017
- return [...plans.values()].sort((a, b) => {
9018
- const aTime = Date.parse(a.updatedAt || "");
9019
- const bTime = Date.parse(b.updatedAt || "");
9020
- return (Number.isNaN(bTime) ? 0 : bTime) - (Number.isNaN(aTime) ? 0 : aTime);
9021
- });
8239
+ function getRefPath(ref) {
8240
+ return ref["raisin:path"] || ref["raisin:ref"];
9022
8241
  }
9023
-
9024
- // src/stores/conversation-store.ts
9025
- var ConversationStore = class {
9026
- constructor(options) {
9027
- this.subscribers = /* @__PURE__ */ new Set();
9028
- // Persistent SSE subscription
9029
- this.subscription = null;
9030
- this._messages = [];
9031
- this._isStreaming = false;
9032
- this._isWaiting = false;
9033
- this._streamingText = "";
9034
- this._error = null;
9035
- this._activeToolCalls = [];
9036
- this._isLoading = false;
9037
- this._conversationType = null;
9038
- this._streamingTimer = null;
9039
- this._watchdogTimer = null;
9040
- this.manager = options.database.conversations;
9041
- this._conversationPath = options.conversationPath ?? null;
9042
- this.createOptions = options.createOptions;
9043
- this.onEvent = options.onEvent;
9044
- this._streamingTimeoutMs = options.streamingTimeoutMs ?? 12e4;
9045
- this._watchdogIntervalMs = options.watchdogIntervalMs ?? 3e4;
9046
- if (this._conversationPath) {
9047
- this.ensureSubscription();
9048
- }
9049
- }
9050
- // ==========================================================================
9051
- // Subscription
9052
- // ==========================================================================
9053
- /**
9054
- * Subscribe to state changes.
9055
- * Returns an unsubscribe function.
9056
- */
9057
- subscribe(callback) {
9058
- this.subscribers.add(callback);
9059
- callback(this.getSnapshot());
9060
- return () => {
9061
- this.subscribers.delete(callback);
9062
- };
9063
- }
9064
- /**
9065
- * Get the current state snapshot.
9066
- */
9067
- getSnapshot() {
9068
- return {
9069
- conversation: this._conversationPath ? { conversationPath: this._conversationPath, type: this._conversationType || "ai_chat" } : null,
9070
- messages: [...this._messages],
9071
- isStreaming: this._isStreaming,
9072
- isWaiting: this._isWaiting,
9073
- streamingText: this._streamingText,
9074
- error: this._error,
9075
- activeToolCalls: [...this._activeToolCalls],
9076
- plans: projectPlansFromMessages(this._messages),
9077
- isLoading: this._isLoading,
9078
- conversationPath: this._conversationPath
9079
- };
9080
- }
9081
- // ==========================================================================
9082
- // Actions
9083
- // ==========================================================================
9084
- /**
9085
- * Send a user message and stream the AI response.
9086
- *
9087
- * If no conversation exists yet and `createOptions` was provided,
9088
- * the conversation is created automatically on the first call.
9089
- */
9090
- async sendMessage(content) {
9091
- if (this._isStreaming) {
9092
- logger.warn("[ConversationStore] Cannot send message while streaming");
9093
- return;
9094
- }
9095
- logger.info("[ConversationStore] Sending message", { path: this._conversationPath });
9096
- this._error = null;
9097
- if (!this._conversationPath) {
9098
- if (!this.createOptions) {
9099
- throw new Error("No conversationPath and no createOptions provided");
9100
- }
9101
- try {
9102
- const convo = await this.manager.create({
9103
- participant: this.createOptions.participant,
9104
- input: this.createOptions.input
9105
- });
9106
- this._conversationPath = convo.conversationPath;
9107
- this._conversationType = convo.type;
9108
- for (const event of convo.initialEvents ?? []) {
9109
- this.onEvent?.(event);
9110
- this.handleEvent(event);
9111
- }
9112
- this.notify();
9113
- } catch (err) {
9114
- this._error = err instanceof Error ? err.message : String(err);
9115
- this.notify();
9116
- return;
9117
- }
9118
- }
9119
- const userMessage = {
9120
- role: "user",
9121
- content,
9122
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
9123
- };
9124
- this._messages.push(userMessage);
9125
- this._isStreaming = true;
9126
- this._isWaiting = false;
9127
- this._streamingText = "";
9128
- this._activeToolCalls = [];
9129
- this.resetStreamingTimer();
9130
- this.startWatchdog();
9131
- this.notify();
9132
- try {
9133
- this.ensureSubscription();
9134
- await this.subscription.waitUntilConnected();
9135
- await this.manager.createUserMessage(this._conversationPath, content);
9136
- } catch (err) {
9137
- logger.error("[ConversationStore] Send failed", err);
9138
- this._error = err instanceof Error ? err.message : String(err);
9139
- this._isStreaming = false;
9140
- this.clearStreamingTimer();
9141
- this.stopWatchdog();
9142
- this.notify();
9143
- }
9144
- }
9145
- /**
9146
- * Load messages from the persisted node tree.
9147
- * Useful for loading history on page reload.
9148
- */
9149
- async loadMessages() {
9150
- if (!this._conversationPath) return [];
9151
- this._isLoading = true;
9152
- this.notify();
9153
- try {
9154
- const messages = await this.manager.getMessages(this._conversationPath);
9155
- logger.debug(`[ConversationStore] Loaded ${messages.length} messages`);
9156
- if (messages.length > 0) {
9157
- this._messages = messages;
9158
- this._activeToolCalls = [];
9159
- }
9160
- return messages;
9161
- } finally {
9162
- this._isLoading = false;
9163
- this.notify();
9164
- }
9165
- }
9166
- /**
9167
- * Approve a pending plan.
9168
- */
9169
- async approvePlan(planPath) {
9170
- await this.manager.approvePlan(planPath);
9171
- }
9172
- /**
9173
- * Reject a pending plan.
9174
- */
9175
- async rejectPlan(planPath, feedback) {
9176
- await this.manager.rejectPlan(planPath, feedback);
9177
- }
9178
- /**
9179
- * Mark a single message as read.
9180
- */
9181
- async markMessageAsRead(messagePath) {
9182
- await this.manager.markMessageAsRead(messagePath);
9183
- }
9184
- /**
9185
- * Stop the current streaming response.
9186
- */
9187
- stop() {
9188
- this._isStreaming = false;
9189
- this.clearStreamingTimer();
9190
- this.stopWatchdog();
9191
- this.notify();
9192
- }
9193
- /**
9194
- * Clean up the store and release resources.
9195
- */
9196
- destroy() {
9197
- this.stop();
9198
- this.clearStreamingTimer();
9199
- this.stopWatchdog();
9200
- this.subscription?.unsubscribe();
9201
- this.subscription = null;
9202
- this.subscribers.clear();
9203
- }
9204
- /**
9205
- * Get the conversation path.
9206
- */
9207
- getConversationPath() {
9208
- return this._conversationPath;
9209
- }
9210
- // ==========================================================================
9211
- // Internal
9212
- // ==========================================================================
9213
- ensureSubscription() {
9214
- if (this.subscription) return;
9215
- if (!this._conversationPath) return;
9216
- this.subscription = this.manager.subscribe(
9217
- this._conversationPath,
9218
- (event) => {
9219
- this.onEvent?.(event);
9220
- this.handleEvent(event);
9221
- }
9222
- );
9223
- }
9224
- handleEvent(event) {
9225
- logger.debug("[ConversationStore] SSE:", event.type);
9226
- if (this._isStreaming) {
9227
- this.resetStreamingTimer();
9228
- }
9229
- switch (event.type) {
9230
- case "text_chunk":
9231
- this._streamingText += event.text;
9232
- this.notify();
9233
- break;
9234
- case "thought_chunk":
9235
- this.notify();
9236
- break;
9237
- case "assistant_message":
9238
- this._messages.push(event.message);
9239
- this._streamingText = "";
9240
- this.notify();
9241
- break;
9242
- case "tool_call_started": {
9243
- logger.debug("[ConversationStore] Tool started:", event.functionName);
9244
- const info = {
9245
- id: event.toolCallId,
9246
- functionName: event.functionName,
9247
- arguments: event.arguments,
9248
- status: "running"
9249
- };
9250
- this._activeToolCalls.push(info);
9251
- this.notify();
9252
- break;
9253
- }
9254
- case "tool_call_completed": {
9255
- logger.debug("[ConversationStore] Tool completed:", event.toolCallId);
9256
- const idx = this._activeToolCalls.findIndex((tc) => tc.id === event.toolCallId);
9257
- if (idx >= 0) {
9258
- this._activeToolCalls[idx] = {
9259
- ...this._activeToolCalls[idx],
9260
- status: event.error ? "failed" : "completed",
9261
- result: event.result,
9262
- durationMs: event.durationMs
9263
- };
9264
- }
9265
- this.notify();
9266
- break;
9267
- }
9268
- case "waiting": {
9269
- logger.info("[ConversationStore] Waiting for input");
9270
- this.flushStreamingText(event.timestamp);
9271
- this._isStreaming = false;
9272
- this._isWaiting = true;
9273
- this._activeToolCalls = [];
9274
- this.clearStreamingTimer();
9275
- this.stopWatchdog();
9276
- this.notify();
9277
- this.deferredReload();
9278
- break;
9279
- }
9280
- case "done": {
9281
- if (isRecoveredDoneEvent(event)) {
9282
- logger.debug("[ConversationStore] Ignoring recovered done event");
9283
- this.reconcileRecoveredDone();
9284
- break;
9285
- }
9286
- const doneEvent = event;
9287
- if (doneEvent.dispatchPhase && doneEvent.dispatchPhase !== "terminal") {
9288
- logger.debug("[ConversationStore] Ignoring non-terminal done event");
9289
- break;
9290
- }
9291
- logger.info("[ConversationStore] Turn done");
9292
- this.flushStreamingText(event.timestamp);
9293
- if (doneEvent.content && !this.lastMessageIsAssistant()) {
9294
- this._messages.push({
9295
- role: doneEvent.role || "assistant",
9296
- content: doneEvent.content,
9297
- timestamp: event.timestamp,
9298
- senderDisplayName: doneEvent.senderDisplayName,
9299
- finishReason: doneEvent.finishReason
9300
- });
9301
- }
9302
- this._isStreaming = false;
9303
- this._isWaiting = true;
9304
- this._activeToolCalls = [];
9305
- this.clearStreamingTimer();
9306
- this.stopWatchdog();
9307
- this.notify();
9308
- this.deferredReload();
9309
- break;
9310
- }
9311
- case "completed": {
9312
- this.flushStreamingText(event.timestamp);
9313
- if (event.messages && event.messages.length > 0) {
9314
- this._messages = event.messages;
9315
- }
9316
- this._isStreaming = false;
9317
- this._isWaiting = false;
9318
- this._activeToolCalls = [];
9319
- this.clearStreamingTimer();
9320
- this.stopWatchdog();
9321
- this.notify();
9322
- this.deferredReload();
9323
- break;
9324
- }
9325
- case "failed":
9326
- logger.error("[ConversationStore] Turn failed", event.error);
9327
- this._error = event.error;
9328
- this._isStreaming = false;
9329
- this._streamingText = "";
9330
- this._activeToolCalls = [];
9331
- this.clearStreamingTimer();
9332
- this.stopWatchdog();
9333
- this.notify();
9334
- break;
9335
- case "conversation_created":
9336
- this._conversationPath = event.conversationPath;
9337
- this.notify();
9338
- break;
9339
- case "message_saved":
9340
- this.silentReload().catch(() => {
9341
- });
9342
- this.notify();
9343
- break;
9344
- case "message_delivered":
9345
- this._messages.push(event.message);
9346
- this.notify();
9347
- break;
9348
- case "log": {
9349
- const logEvent = event;
9350
- const currentLevel = getLogLevel();
9351
- const levelMap = {
9352
- debug: 4 /* Debug */,
9353
- info: 3 /* Info */,
9354
- warn: 2 /* Warn */,
9355
- error: 1 /* Error */
9356
- };
9357
- const eventLevel = levelMap[logEvent.level] ?? 4 /* Debug */;
9358
- if (currentLevel >= eventLevel) {
9359
- const fn = logEvent.level === "error" ? console.error : logEvent.level === "warn" ? console.warn : logEvent.level === "debug" ? console.debug : console.info;
9360
- fn("[server]", logEvent.message);
9361
- }
9362
- break;
9363
- }
9364
- }
9365
- }
9366
- /**
9367
- * If accumulated streaming text hasn't been captured in an assistant_message,
9368
- * synthesize one.
9369
- */
9370
- flushStreamingText(timestamp) {
9371
- if (this._streamingText && !this.lastMessageIsAssistant()) {
9372
- this._messages.push({
9373
- role: "assistant",
9374
- content: this._streamingText,
9375
- timestamp
9376
- });
9377
- }
9378
- this._streamingText = "";
9379
- }
9380
- lastMessageIsAssistant() {
9381
- const last = this._messages[this._messages.length - 1];
9382
- return last?.role === "assistant";
9383
- }
9384
- /**
9385
- * After a turn completes, reload canonical messages from the node tree
9386
- * to catch any events missed during SSE gaps.
9387
- */
9388
- deferredReload() {
9389
- if (!this._conversationPath) return;
9390
- logger.debug("[ConversationStore] Scheduling deferred reload");
9391
- const attemptReload = (delay, retriesLeft) => {
9392
- setTimeout(async () => {
9393
- try {
9394
- const prevCount = this._messages.length;
9395
- await this.silentReload();
9396
- if (this._messages.length === prevCount && retriesLeft > 0) {
9397
- attemptReload(delay * 2, retriesLeft - 1);
9398
- }
9399
- } catch {
9400
- }
9401
- }, delay);
9402
- };
9403
- attemptReload(500, 2);
9404
- }
9405
- async silentReload() {
9406
- if (!this._conversationPath) return;
9407
- try {
9408
- const fresh = await this.manager.getMessages(this._conversationPath);
9409
- if (fresh.length === 0) return;
9410
- let matchCount = 0;
9411
- const limit = Math.min(this._messages.length, fresh.length);
9412
- for (let i = 0; i < limit; i++) {
9413
- if (this._messages[i].role !== fresh[i].role || this._messages[i].content !== fresh[i].content) break;
9414
- matchCount++;
9415
- }
9416
- if (matchCount === this._messages.length && fresh.length === matchCount) {
9417
- for (let i = 0; i < matchCount; i++) {
9418
- const f = fresh[i];
9419
- const e = this._messages[i];
9420
- if (f.id) e.id = f.id;
9421
- if (f.path) e.path = f.path;
9422
- if (f.messageType) e.messageType = f.messageType;
9423
- if (f.data) e.data = f.data;
9424
- if (f.children) e.children = f.children;
9425
- if (f.finishReason) e.finishReason = f.finishReason;
9426
- }
9427
- await this.rebuildToolCallState();
9428
- return;
9429
- }
9430
- if (matchCount === this._messages.length && fresh.length > matchCount) {
9431
- for (let i = matchCount; i < fresh.length; i++) {
9432
- this._messages.push(fresh[i]);
9433
- }
9434
- for (let i = 0; i < matchCount; i++) {
9435
- const f = fresh[i];
9436
- const e = this._messages[i];
9437
- if (f.id) e.id = f.id;
9438
- if (f.path) e.path = f.path;
9439
- if (f.messageType) e.messageType = f.messageType;
9440
- if (f.data) e.data = f.data;
9441
- if (f.children) e.children = f.children;
9442
- }
9443
- await this.rebuildToolCallState();
9444
- this.notify();
9445
- return;
9446
- }
9447
- if (matchCount === fresh.length && fresh.length < this._messages.length) {
9448
- for (let i = 0; i < matchCount; i++) {
9449
- const f = fresh[i];
9450
- const e = this._messages[i];
9451
- if (f.id) e.id = f.id;
9452
- if (f.path) e.path = f.path;
9453
- if (f.messageType) e.messageType = f.messageType;
9454
- if (f.data) e.data = f.data;
9455
- if (f.children) e.children = f.children;
9456
- }
9457
- await this.rebuildToolCallState();
9458
- return;
9459
- }
9460
- this._messages = fresh;
9461
- await this.rebuildToolCallState();
9462
- this.notify();
9463
- } catch (err) {
9464
- logger.debug("[ConversationStore] Silent reload failed:", err);
9465
- }
9466
- }
9467
- // ==========================================================================
9468
- // Streaming timeout (Fix A) — auto-recover on SSE inactivity
9469
- // ==========================================================================
9470
- resetStreamingTimer() {
9471
- this.clearStreamingTimer();
9472
- if (!this._isStreaming) return;
9473
- this._streamingTimer = setTimeout(() => {
9474
- if (this._isStreaming) {
9475
- logger.warn("[ConversationStore] Streaming timeout \u2014 no SSE activity for", this._streamingTimeoutMs, "ms");
9476
- this.recoverFromHang();
9477
- }
9478
- }, this._streamingTimeoutMs);
9479
- }
9480
- clearStreamingTimer() {
9481
- if (this._streamingTimer) {
9482
- clearTimeout(this._streamingTimer);
9483
- this._streamingTimer = null;
9484
- }
9485
- }
9486
- // ==========================================================================
9487
- // Activity watchdog (Fix B) — periodic backend health check
9488
- // ==========================================================================
9489
- startWatchdog() {
9490
- this.stopWatchdog();
9491
- if (!this._conversationPath) return;
9492
- this._watchdogTimer = setInterval(async () => {
9493
- if (!this._isStreaming || !this._conversationPath) {
9494
- this.stopWatchdog();
9495
- return;
9496
- }
9497
- try {
9498
- const health = await this.manager.checkTurnHealth(this._conversationPath);
9499
- if (health === "done" && this._isStreaming) {
9500
- logger.warn("[ConversationStore] Watchdog detected completed turn while still streaming \u2014 recovering");
9501
- this.recoverFromHang();
9502
- }
9503
- } catch {
9504
- }
9505
- }, this._watchdogIntervalMs);
9506
- }
9507
- stopWatchdog() {
9508
- if (this._watchdogTimer) {
9509
- clearInterval(this._watchdogTimer);
9510
- this._watchdogTimer = null;
9511
- }
9512
- }
9513
- // ==========================================================================
9514
- // Hang recovery (shared by timeout + watchdog)
9515
- // ==========================================================================
9516
- recoverFromHang() {
9517
- logger.info("[ConversationStore] Recovering from hang");
9518
- this.clearStreamingTimer();
9519
- this.stopWatchdog();
9520
- this._isStreaming = false;
9521
- this._isWaiting = true;
9522
- this._streamingText = "";
9523
- this.notify();
9524
- this.silentReload().catch(() => {
9525
- });
9526
- }
9527
- /**
9528
- * Reconcile a recovered done event against persisted state.
9529
- * If the backend turn is actually complete, finalize the local state.
9530
- * Otherwise keep streaming active and let normal events continue.
9531
- */
9532
- reconcileRecoveredDone() {
9533
- if (!this._conversationPath) return;
9534
- void this.manager.checkTurnHealth(this._conversationPath).then((health) => {
9535
- if (health !== "done") return;
9536
- if (!this._isStreaming) return;
9537
- this._isStreaming = false;
9538
- this._isWaiting = true;
9539
- this._activeToolCalls = [];
9540
- this.clearStreamingTimer();
9541
- this.stopWatchdog();
9542
- this.notify();
9543
- this.deferredReload();
9544
- }).catch(() => {
9545
- });
9546
- }
9547
- // ==========================================================================
9548
- // Tool call state rebuild (Fix C) — reconstruct from node tree on reload
9549
- // ==========================================================================
9550
- async rebuildToolCallState() {
9551
- if (!this._conversationPath) {
9552
- this._activeToolCalls = [];
9553
- return;
9554
- }
9555
- try {
9556
- const pendingCalls = await this.manager.getActiveToolCalls(this._conversationPath);
9557
- if (pendingCalls.length > 0) {
9558
- this._activeToolCalls = pendingCalls.map((tc) => ({
9559
- id: tc.id,
9560
- functionName: tc.name,
9561
- arguments: void 0,
9562
- status: "running"
9563
- }));
9564
- if (!this._isStreaming) {
9565
- this._isStreaming = true;
9566
- this.resetStreamingTimer();
9567
- this.startWatchdog();
9568
- }
9569
- } else {
9570
- this._activeToolCalls = [];
9571
- if (this._isStreaming) {
9572
- const lastMsg = this._messages[this._messages.length - 1];
9573
- const isLastAssistantDone = lastMsg?.role === "assistant" && (lastMsg.dispatchPhase ? lastMsg.dispatchPhase === "terminal" : !!lastMsg.finishReason && lastMsg.finishReason !== "tool_calls");
9574
- if (isLastAssistantDone) {
9575
- this._isStreaming = false;
9576
- this._isWaiting = true;
9577
- this.clearStreamingTimer();
9578
- this.stopWatchdog();
9579
- }
9580
- }
9581
- }
9582
- } catch {
9583
- this._activeToolCalls = [];
9584
- }
9585
- }
9586
- notify() {
9587
- const snapshot = this.getSnapshot();
9588
- for (const sub of this.subscribers) {
9589
- try {
9590
- sub(snapshot);
9591
- } catch (err) {
9592
- logger.error("Error in conversation store subscriber:", err);
9593
- }
9594
- }
9595
- }
8242
+ var RETRY_STRATEGIES = {
8243
+ none: { max_retries: 0, base_delay_ms: 0, max_delay_ms: 0 },
8244
+ quick: { max_retries: 3, base_delay_ms: 1e3, max_delay_ms: 1e4 },
8245
+ standard: { max_retries: 5, base_delay_ms: 2e3, max_delay_ms: 6e4 },
8246
+ aggressive: { max_retries: 10, base_delay_ms: 5e3, max_delay_ms: 12e4 },
8247
+ llm: { max_retries: 5, base_delay_ms: 1e4, max_delay_ms: 12e4 }
9596
8248
  };
9597
-
9598
- // src/stores/conversation-list-store.ts
9599
- var ConversationListStore = class {
9600
- constructor(options) {
9601
- this.subscribers = /* @__PURE__ */ new Set();
9602
- // Internal state
9603
- this._conversations = [];
9604
- this._isLoading = false;
9605
- this._error = null;
9606
- // Cached ConversationStore instances
9607
- this._stores = /* @__PURE__ */ new Map();
9608
- // Real-time subscription
9609
- this._realtimeSubscription = null;
9610
- this.database = options.database;
9611
- this.type = options.type;
9612
- this.realtime = options.realtime ?? false;
9613
- }
9614
- // ==========================================================================
9615
- // Subscription
9616
- // ==========================================================================
9617
- subscribe(callback) {
9618
- this.subscribers.add(callback);
9619
- callback(this.getSnapshot());
9620
- return () => {
9621
- this.subscribers.delete(callback);
9622
- };
9623
- }
9624
- getSnapshot() {
9625
- return {
9626
- conversations: [...this._conversations],
9627
- totalUnreadCount: this._conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0),
9628
- isLoading: this._isLoading,
9629
- error: this._error
9630
- };
9631
- }
9632
- // ==========================================================================
9633
- // Actions
9634
- // ==========================================================================
9635
- /**
9636
- * Load conversations from the server.
9637
- */
9638
- async load() {
9639
- this._isLoading = true;
9640
- this._error = null;
9641
- this.notify();
9642
- try {
9643
- const conversations = await this.database.conversations.list({
9644
- type: this.type
9645
- });
9646
- logger.debug(`[ConversationListStore] Loaded ${conversations.length} conversations`);
9647
- this._conversations = conversations;
9648
- if (this.realtime) {
9649
- this.startRealtimeSubscription();
9650
- }
9651
- } catch (err) {
9652
- logger.error("[ConversationListStore] Failed to load conversations", err);
9653
- this._error = err instanceof Error ? err.message : String(err);
9654
- } finally {
9655
- this._isLoading = false;
9656
- this.notify();
9657
- }
9658
- }
9659
- /**
9660
- * Create a new conversation.
9661
- */
9662
- async createConversation(options) {
9663
- const convo = await this.database.conversations.create(options);
9664
- const listItem = {
9665
- id: convo.id,
9666
- type: convo.type,
9667
- conversationPath: convo.conversationPath,
9668
- conversationWorkspace: convo.conversationWorkspace,
9669
- agentRef: convo.agentRef,
9670
- participants: convo.participants,
9671
- unreadCount: 0,
9672
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9673
- };
9674
- this._conversations = [listItem, ...this._conversations];
9675
- this.notify();
9676
- return listItem;
9677
- }
9678
- /**
9679
- * Delete a conversation.
9680
- */
9681
- async deleteConversation(conversationPath) {
9682
- const store = this._stores.get(conversationPath);
9683
- if (store) {
9684
- store.destroy();
9685
- this._stores.delete(conversationPath);
9686
- }
9687
- await this.database.conversations.delete(conversationPath);
9688
- this._conversations = this._conversations.filter((c) => c.conversationPath !== conversationPath);
9689
- this.notify();
9690
- }
9691
- /**
9692
- * Mark a conversation as read.
9693
- */
9694
- async markAsRead(conversationPath) {
9695
- await this.database.conversations.markAsRead(conversationPath);
9696
- this._conversations = this._conversations.map(
9697
- (c) => c.conversationPath === conversationPath ? { ...c, unreadCount: 0 } : c
9698
- );
9699
- this.notify();
9700
- }
9701
- /**
9702
- * Get or create a ConversationStore for a specific conversation.
9703
- * Caches instances so repeated calls return the same store.
9704
- */
9705
- getConversationStore(conversationPath, overrides) {
9706
- let store = this._stores.get(conversationPath);
9707
- if (!store) {
9708
- store = new ConversationStore({
9709
- database: this.database,
9710
- conversationPath,
9711
- ...overrides
9712
- });
9713
- this._stores.set(conversationPath, store);
9714
- }
9715
- return store;
9716
- }
9717
- /**
9718
- * Clean up all resources.
9719
- */
9720
- destroy() {
9721
- this._realtimeSubscription?.unsubscribe();
9722
- this._realtimeSubscription = null;
9723
- for (const store of this._stores.values()) {
9724
- store.destroy();
9725
- }
9726
- this._stores.clear();
9727
- this.subscribers.clear();
9728
- }
9729
- // ==========================================================================
9730
- // Internal
9731
- // ==========================================================================
9732
- async startRealtimeSubscription() {
9733
- if (this._realtimeSubscription) return;
9734
- logger.debug("[ConversationListStore] Starting realtime subscription");
9735
- try {
9736
- const workspace = this.database.workspace("raisin:access_control");
9737
- const events = workspace.events();
9738
- const result = await this.database.executeSql(
9739
- `SELECT RAISIN_CURRENT_USER()->>'path'::String as home`
9740
- );
9741
- const rows = result.rows;
9742
- const userHome = rows?.[0]?.home;
9743
- if (!userHome) return;
9744
- const chatsPath = `${userHome}/inbox/chats`;
9745
- this._realtimeSubscription = await events.subscribe(
9746
- {
9747
- workspace: "raisin:access_control",
9748
- path: chatsPath,
9749
- event_types: ["node:created", "node:updated"]
9750
- },
9751
- async (event) => {
9752
- const eventData = event;
9753
- const nodeId = eventData.payload?.node_id;
9754
- if (!nodeId) return;
9755
- try {
9756
- const nodeResult = await this.database.executeSql(
9757
- `SELECT id, path, node_type, properties, created_at, updated_at FROM 'raisin:access_control' WHERE id = $1 LIMIT 1`,
9758
- [nodeId]
9759
- );
9760
- const row = nodeResult.rows?.[0];
9761
- if (!row || row.node_type !== "raisin:Conversation") return;
9762
- const convPath = row.path;
9763
- const props = row.properties ?? {};
9764
- const item = {
9765
- id: row.id,
9766
- type: props.conversation_type ?? "ai_chat",
9767
- conversationPath: convPath,
9768
- conversationWorkspace: "raisin:access_control",
9769
- agentRef: props.agent_ref,
9770
- participants: props.participants,
9771
- unreadCount: props.unread_count ?? 0,
9772
- lastMessage: props.last_message,
9773
- updatedAt: row.updated_at ?? row.created_at
9774
- };
9775
- const existingIdx = this._conversations.findIndex((c) => c.conversationPath === convPath);
9776
- if (existingIdx >= 0) {
9777
- this._conversations.splice(existingIdx, 1);
9778
- }
9779
- this._conversations = [item, ...this._conversations];
9780
- this.notify();
9781
- } catch (err) {
9782
- logger.debug("Failed to process conversation event:", err);
9783
- }
9784
- }
9785
- );
9786
- } catch (err) {
9787
- logger.debug("Failed to start realtime subscription:", err);
9788
- }
9789
- }
9790
- notify() {
9791
- const snapshot = this.getSnapshot();
9792
- for (const sub of this.subscribers) {
9793
- try {
9794
- sub(snapshot);
9795
- } catch (err) {
9796
- logger.error("Error in conversation list store subscriber:", err);
9797
- }
9798
- }
9799
- }
9800
- };
9801
-
9802
- // src/integrations/react-conversation.ts
9803
- var INITIAL_SNAPSHOT = {
9804
- conversation: null,
9805
- messages: [],
9806
- isStreaming: false,
9807
- isWaiting: false,
9808
- streamingText: "",
9809
- error: null,
9810
- activeToolCalls: [],
9811
- plans: [],
9812
- isLoading: false,
9813
- conversationPath: null
9814
- };
9815
- function useConversation(react, options) {
9816
- const { useState, useEffect, useRef, useCallback } = react;
9817
- const [snapshot, setSnapshot] = useState(INITIAL_SNAPSHOT);
9818
- const storeRef = useRef(null);
9819
- useEffect(() => {
9820
- const store = new ConversationStore(options);
9821
- storeRef.current = store;
9822
- const unsubscribe = store.subscribe((next) => setSnapshot(next));
9823
- return () => {
9824
- unsubscribe();
9825
- store.destroy();
9826
- storeRef.current = null;
9827
- };
9828
- }, [options.conversationPath]);
9829
- const sendMessage = useCallback(async (content) => {
9830
- await storeRef.current?.sendMessage(content);
9831
- }, []);
9832
- const approvePlan = useCallback(async (planPath) => {
9833
- await storeRef.current?.approvePlan(planPath);
9834
- }, []);
9835
- const rejectPlan = useCallback(async (planPath, feedback) => {
9836
- await storeRef.current?.rejectPlan(planPath, feedback);
9837
- }, []);
9838
- const stop = useCallback(() => {
9839
- storeRef.current?.stop();
9840
- }, []);
9841
- const loadMessages = useCallback(async () => {
9842
- return await storeRef.current?.loadMessages() ?? [];
9843
- }, []);
9844
- return {
9845
- ...snapshot,
9846
- sendMessage,
9847
- approvePlan,
9848
- rejectPlan,
9849
- stop,
9850
- loadMessages
9851
- };
9852
- }
9853
- var INITIAL_LIST_SNAPSHOT = {
9854
- conversations: [],
9855
- totalUnreadCount: 0,
9856
- isLoading: false,
9857
- error: null
9858
- };
9859
- function useConversationList(react, options) {
9860
- const { useState, useEffect, useRef, useCallback } = react;
9861
- const [snapshot, setSnapshot] = useState(INITIAL_LIST_SNAPSHOT);
9862
- const storeRef = useRef(null);
9863
- useEffect(() => {
9864
- const store = new ConversationListStore(options);
9865
- storeRef.current = store;
9866
- const unsubscribe = store.subscribe((next) => setSnapshot(next));
9867
- store.load();
9868
- return () => {
9869
- unsubscribe();
9870
- store.destroy();
9871
- storeRef.current = null;
9872
- };
9873
- }, [options.type, options.realtime]);
9874
- const createConversation = useCallback(async (opts) => {
9875
- return storeRef.current.createConversation(opts);
9876
- }, []);
9877
- const deleteConversation = useCallback(async (conversationPath) => {
9878
- await storeRef.current?.deleteConversation(conversationPath);
9879
- }, []);
9880
- const markAsRead = useCallback(async (conversationPath) => {
9881
- await storeRef.current?.markAsRead(conversationPath);
9882
- }, []);
9883
- return {
9884
- ...snapshot,
9885
- createConversation,
9886
- deleteConversation,
9887
- markAsRead
9888
- };
9889
- }
9890
-
9891
- // src/integrations/svelte-conversation.ts
9892
- function createConversationAdapter(options) {
9893
- const store = new ConversationStore(options);
9894
- return {
9895
- /** The underlying ConversationStore instance */
9896
- store,
9897
- /** Get the current snapshot */
9898
- getSnapshot: () => store.getSnapshot(),
9899
- /** Subscribe to snapshot changes. Returns unsubscribe function. */
9900
- subscribe: (cb) => store.subscribe(cb),
9901
- /** Send a user message */
9902
- sendMessage: (content) => store.sendMessage(content),
9903
- /** Load persisted messages */
9904
- loadMessages: () => store.loadMessages(),
9905
- /** Approve a pending plan */
9906
- approvePlan: (planPath) => store.approvePlan(planPath),
9907
- /** Reject a pending plan */
9908
- rejectPlan: (planPath, feedback) => store.rejectPlan(planPath, feedback),
9909
- /** Mark a message as read */
9910
- markMessageAsRead: (messagePath) => store.markMessageAsRead(messagePath),
9911
- /** Stop streaming */
9912
- stop: () => store.stop(),
9913
- /** Get the conversation path */
9914
- getConversationPath: () => store.getConversationPath(),
9915
- /** Destroy the store and release resources */
9916
- destroy: () => store.destroy()
9917
- };
9918
- }
9919
- function createConversationListAdapter(options) {
9920
- const store = new ConversationListStore(options);
9921
- return {
9922
- /** The underlying ConversationListStore instance */
9923
- store,
9924
- /** Get the current snapshot */
9925
- getSnapshot: () => store.getSnapshot(),
9926
- /** Subscribe to snapshot changes. Returns unsubscribe function. */
9927
- subscribe: (cb) => store.subscribe(cb),
9928
- /** Load conversations from the server */
9929
- load: () => store.load(),
9930
- /** Create a new conversation */
9931
- createConversation: (opts) => store.createConversation(opts),
9932
- /** Delete a conversation */
9933
- deleteConversation: (path) => store.deleteConversation(path),
9934
- /** Mark a conversation as read */
9935
- markAsRead: (path) => store.markAsRead(path),
9936
- /** Get or create a ConversationStore for a specific path */
9937
- getConversationStore: (path) => store.getConversationStore(path),
9938
- /** Destroy the store and release resources */
9939
- destroy: () => store.destroy()
9940
- };
9941
- }
9942
-
9943
- // src/integrations/svelte/svelte-auth.ts
9944
- function createAuthAdapter(client) {
9945
- let snapshot = {
9946
- user: client.getStoredUser(),
9947
- isAuthenticated: client.getStoredUser() !== null,
9948
- isLoading: false
9949
- };
9950
- const listeners = /* @__PURE__ */ new Set();
9951
- function emit() {
9952
- for (const cb of listeners) cb(snapshot);
9953
- }
9954
- function update(partial) {
9955
- snapshot = { ...snapshot, ...partial };
9956
- emit();
9957
- }
9958
- const unsubAuth = client.onAuthStateChange(({ event, session }) => {
9959
- if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
9960
- update({ user: session.user, isAuthenticated: true });
9961
- } else if (event === "SIGNED_OUT" || event === "SESSION_EXPIRED") {
9962
- update({ user: null, isAuthenticated: false });
9963
- }
9964
- });
9965
- const unsubUser = client.onUserChange(({ node }) => {
9966
- if (!snapshot.user) return;
9967
- update({
9968
- user: {
9969
- ...snapshot.user,
9970
- home: node.path,
9971
- displayName: node.properties.display_name ?? snapshot.user.displayName,
9972
- avatarUrl: node.properties.avatar_url ?? snapshot.user.avatarUrl
9973
- }
9974
- });
9975
- });
9976
- return {
9977
- subscribe(cb) {
9978
- listeners.add(cb);
9979
- return () => {
9980
- listeners.delete(cb);
9981
- };
9982
- },
9983
- getSnapshot: () => snapshot,
9984
- async login(email, password, repository) {
9985
- update({ isLoading: true });
9986
- try {
9987
- return await client.loginWithEmail(email, password, repository);
9988
- } finally {
9989
- update({ isLoading: false });
9990
- }
9991
- },
9992
- async register(email, password, repository, displayName) {
9993
- update({ isLoading: true });
9994
- try {
9995
- return await client.registerWithEmail(email, password, repository, displayName);
9996
- } finally {
9997
- update({ isLoading: false });
9998
- }
9999
- },
10000
- async logout(options) {
10001
- update({ isLoading: true });
10002
- try {
10003
- await client.logout(options);
10004
- } finally {
10005
- update({ isLoading: false });
10006
- }
10007
- },
10008
- async initSession(repository) {
10009
- update({ isLoading: true });
10010
- try {
10011
- return await client.initSession(repository);
10012
- } finally {
10013
- update({ isLoading: false });
10014
- }
10015
- },
10016
- destroy() {
10017
- unsubAuth();
10018
- unsubUser();
10019
- listeners.clear();
10020
- }
10021
- };
10022
- }
10023
-
10024
- // src/integrations/svelte/svelte-connection.ts
10025
- function createConnectionAdapter(client) {
10026
- let snapshot = {
10027
- state: client.getConnectionState(),
10028
- isConnected: client.getConnectionState() === "connected" /* Connected */,
10029
- isReady: client.isReady()
10030
- };
10031
- const listeners = /* @__PURE__ */ new Set();
10032
- function emit() {
10033
- for (const cb of listeners) cb(snapshot);
10034
- }
10035
- const unsubConn = client.onConnectionStateChange((newState) => {
10036
- snapshot = {
10037
- state: newState,
10038
- isConnected: newState === "connected" /* Connected */,
10039
- isReady: snapshot.isReady
10040
- };
10041
- emit();
10042
- });
10043
- const unsubReady = client.onReadyStateChange((ready) => {
10044
- snapshot = { ...snapshot, isReady: ready };
10045
- emit();
10046
- });
10047
- return {
10048
- subscribe(cb) {
10049
- listeners.add(cb);
10050
- return () => {
10051
- listeners.delete(cb);
10052
- };
10053
- },
10054
- getSnapshot: () => snapshot,
10055
- async connect() {
10056
- await client.connect();
10057
- },
10058
- disconnect() {
10059
- client.disconnect();
10060
- },
10061
- destroy() {
10062
- unsubConn();
10063
- unsubReady();
10064
- listeners.clear();
10065
- }
10066
- };
10067
- }
10068
-
10069
- // src/integrations/svelte/svelte-sql.ts
10070
- function rowsToObjects2(result) {
10071
- return result.rows;
10072
- }
10073
- function createSqlAdapter(database, sql, params, options) {
10074
- let snapshot = {
10075
- data: null,
10076
- isLoading: false,
10077
- error: null
10078
- };
10079
- const listeners = /* @__PURE__ */ new Set();
10080
- let destroyed = false;
10081
- function emit() {
10082
- for (const cb of listeners) cb(snapshot);
10083
- }
10084
- function update(partial) {
10085
- snapshot = { ...snapshot, ...partial };
10086
- emit();
10087
- }
10088
- async function fetchData() {
10089
- if (destroyed) return;
10090
- update({ isLoading: true, error: null });
10091
- try {
10092
- const result = await database.executeSql(sql, params);
10093
- if (!destroyed) {
10094
- update({ data: rowsToObjects2(result), isLoading: false });
10095
- }
10096
- } catch (err) {
10097
- if (!destroyed) {
10098
- update({
10099
- error: err instanceof Error ? err : new Error(String(err)),
10100
- isLoading: false
10101
- });
10102
- }
10103
- }
10104
- }
10105
- const cleanups = [];
10106
- const enabled = options?.enabled !== false;
10107
- if (enabled) {
10108
- fetchData();
10109
- }
10110
- const refetchOnReconnect = options?.refetchOnReconnect !== false;
10111
- if (enabled && refetchOnReconnect) {
10112
- const client = database._client;
10113
- if (client?.onReconnected) {
10114
- const unsub = client.onReconnected(() => {
10115
- fetchData();
10116
- });
10117
- cleanups.push(unsub);
10118
- }
10119
- }
10120
- const realtime = options?.realtime;
10121
- if (enabled && realtime) {
10122
- let unsubscribe;
10123
- let cancelled = false;
10124
- database.events().subscribe(
10125
- {
10126
- workspace: realtime.workspace,
10127
- event_types: realtime.eventTypes,
10128
- path: realtime.path,
10129
- node_type: realtime.nodeType
10130
- },
10131
- () => {
10132
- fetchData();
10133
- }
10134
- ).then((sub) => {
10135
- if (cancelled) {
10136
- sub.unsubscribe();
10137
- } else {
10138
- unsubscribe = () => sub.unsubscribe();
10139
- }
10140
- });
10141
- cleanups.push(() => {
10142
- cancelled = true;
10143
- unsubscribe?.();
10144
- });
10145
- }
10146
- return {
10147
- subscribe(cb) {
10148
- listeners.add(cb);
10149
- return () => {
10150
- listeners.delete(cb);
10151
- };
10152
- },
10153
- getSnapshot: () => snapshot,
10154
- refetch: fetchData,
10155
- destroy() {
10156
- destroyed = true;
10157
- for (const cleanup of cleanups) cleanup();
10158
- listeners.clear();
10159
- }
10160
- };
10161
- }
10162
-
10163
- // src/integrations/svelte/svelte-subscription.ts
10164
- function createSubscriptionAdapter(database, filters, callback) {
10165
- let unsubscribe;
10166
- let cancelled = false;
10167
- database.events().subscribe(filters, (event) => {
10168
- if (!cancelled) callback(event);
10169
- }).then((sub) => {
10170
- if (cancelled) {
10171
- sub.unsubscribe();
10172
- } else {
10173
- unsubscribe = () => sub.unsubscribe();
10174
- }
10175
- });
10176
- return {
10177
- destroy() {
10178
- cancelled = true;
10179
- unsubscribe?.();
10180
- }
10181
- };
10182
- }
10183
-
10184
- // src/integrations/svelte/svelte-flow.ts
10185
- function createFlowAdapter(options) {
10186
- let snapshot = {
10187
- events: [],
10188
- status: "idle",
10189
- isRunning: false,
10190
- error: null,
10191
- output: null,
10192
- instanceId: null
10193
- };
10194
- const listeners = /* @__PURE__ */ new Set();
10195
- let aborted = false;
10196
- function emit() {
10197
- for (const cb of listeners) cb(snapshot);
10198
- }
10199
- function update(partial) {
10200
- snapshot = { ...snapshot, ...partial };
10201
- if ("status" in partial) {
10202
- snapshot.isRunning = snapshot.status === "running";
10203
- }
10204
- emit();
10205
- }
10206
- let flowClient = null;
10207
- if (options.database) {
10208
- flowClient = options.database.flow;
10209
- } else if (options.baseUrl && options.repository && options.authManager) {
10210
- flowClient = new FlowClient(
10211
- options.baseUrl,
10212
- options.repository,
10213
- options.authManager,
10214
- options.clientOptions,
10215
- options.flowsApi
10216
- );
10217
- }
10218
- return {
10219
- subscribe(cb) {
10220
- listeners.add(cb);
10221
- return () => {
10222
- listeners.delete(cb);
10223
- };
10224
- },
10225
- getSnapshot: () => snapshot,
10226
- async run(flowPath, input = {}) {
10227
- if (!flowClient) return;
10228
- aborted = false;
10229
- update({
10230
- events: [],
10231
- error: null,
10232
- output: null,
10233
- status: "running"
10234
- });
10235
- try {
10236
- const result = await flowClient.run(flowPath, input);
10237
- update({ instanceId: result.instance_id });
10238
- for await (const event of flowClient.streamEvents(result.instance_id)) {
10239
- if (aborted) break;
10240
- update({ events: [...snapshot.events, event] });
10241
- if (event.type === "flow_waiting") {
10242
- update({ status: "waiting" });
10243
- } else if (event.type === "flow_completed") {
10244
- update({ output: event.output, status: "completed" });
10245
- } else if (event.type === "flow_failed") {
10246
- update({ error: event.error, status: "failed" });
10247
- }
10248
- }
10249
- } catch (err) {
10250
- update({
10251
- error: err instanceof Error ? err.message : String(err),
10252
- status: "failed"
10253
- });
10254
- }
10255
- },
10256
- async resume(data) {
10257
- if (!flowClient || !snapshot.instanceId) return;
10258
- update({ status: "running", error: null });
10259
- try {
10260
- const stream = await flowClient.createEventStream(snapshot.instanceId);
10261
- await flowClient.resume(snapshot.instanceId, data);
10262
- for await (const event of stream.events) {
10263
- if (aborted) break;
10264
- update({ events: [...snapshot.events, event] });
10265
- if (event.type === "flow_waiting") {
10266
- update({ status: "waiting" });
10267
- stream.close();
10268
- return;
10269
- } else if (event.type === "flow_completed") {
10270
- update({ output: event.output, status: "completed" });
10271
- stream.close();
10272
- return;
10273
- } else if (event.type === "flow_failed") {
10274
- update({ error: event.error, status: "failed" });
10275
- stream.close();
10276
- return;
10277
- }
10278
- }
10279
- stream.close();
10280
- } catch (err) {
10281
- update({
10282
- error: err instanceof Error ? err.message : String(err),
10283
- status: "failed"
10284
- });
10285
- }
10286
- },
10287
- reset() {
10288
- aborted = true;
10289
- update({
10290
- events: [],
10291
- status: "idle",
10292
- error: null,
10293
- output: null,
10294
- instanceId: null
10295
- });
10296
- },
10297
- destroy() {
10298
- aborted = true;
10299
- listeners.clear();
10300
- }
10301
- };
10302
- }
10303
-
10304
- // src/integrations/svelte/svelte-context.ts
10305
- var RAISIN_CONTEXT_KEY = /* @__PURE__ */ Symbol("raisin");
10306
-
10307
- // src/integrations/react-flow.ts
10308
- function useFlow(react, options) {
10309
- const { useState, useEffect, useRef, useCallback } = react;
10310
- const [events, setEvents] = useState([]);
10311
- const [status, setStatus] = useState("idle");
10312
- const [error, setError] = useState(null);
10313
- const [output, setOutput] = useState(null);
10314
- const [instanceId, setInstanceId] = useState(null);
10315
- const clientRef = useRef(null);
10316
- const abortRef = useRef(false);
10317
- useEffect(() => {
10318
- if (options.database) {
10319
- clientRef.current = options.database.flow;
10320
- } else if (options.baseUrl && options.repository && options.authManager) {
10321
- clientRef.current = new FlowClient(
10322
- options.baseUrl,
10323
- options.repository,
10324
- options.authManager,
10325
- options.clientOptions,
10326
- options.flowsApi
10327
- );
10328
- }
10329
- return () => {
10330
- abortRef.current = true;
10331
- clientRef.current = null;
10332
- };
10333
- }, [options.database, options.baseUrl, options.repository]);
10334
- const run = useCallback(async (flowPath, input = {}) => {
10335
- const client = clientRef.current;
10336
- if (!client) return;
10337
- abortRef.current = false;
10338
- setEvents([]);
10339
- setError(null);
10340
- setOutput(null);
10341
- setStatus("running");
10342
- try {
10343
- const result = await client.run(flowPath, input);
10344
- setInstanceId(result.instance_id);
10345
- for await (const event of client.streamEvents(result.instance_id)) {
10346
- if (abortRef.current) break;
10347
- setEvents((prev) => [...prev, event]);
10348
- if (event.type === "flow_waiting") {
10349
- setStatus("waiting");
10350
- } else if (event.type === "flow_completed") {
10351
- setOutput(event.output);
10352
- setStatus("completed");
10353
- } else if (event.type === "flow_failed") {
10354
- setError(event.error);
10355
- setStatus("failed");
10356
- }
10357
- }
10358
- } catch (err) {
10359
- setError(err instanceof Error ? err.message : String(err));
10360
- setStatus("failed");
10361
- }
10362
- }, []);
10363
- const resume = useCallback(async (data) => {
10364
- const client = clientRef.current;
10365
- if (!client || !instanceId) return;
10366
- setStatus("running");
10367
- setError(null);
10368
- try {
10369
- const stream = await client.createEventStream(instanceId);
10370
- await client.resume(instanceId, data);
10371
- for await (const event of stream.events) {
10372
- if (abortRef.current) break;
10373
- setEvents((prev) => [...prev, event]);
10374
- if (event.type === "flow_waiting") {
10375
- setStatus("waiting");
10376
- stream.close();
10377
- return;
10378
- } else if (event.type === "flow_completed") {
10379
- setOutput(event.output);
10380
- setStatus("completed");
10381
- stream.close();
10382
- return;
10383
- } else if (event.type === "flow_failed") {
10384
- setError(event.error);
10385
- setStatus("failed");
10386
- stream.close();
10387
- return;
10388
- }
10389
- }
10390
- stream.close();
10391
- } catch (err) {
10392
- setError(err instanceof Error ? err.message : String(err));
10393
- setStatus("failed");
10394
- }
10395
- }, [instanceId]);
10396
- const reset = useCallback(() => {
10397
- abortRef.current = true;
10398
- setEvents([]);
10399
- setStatus("idle");
10400
- setError(null);
10401
- setOutput(null);
10402
- setInstanceId(null);
10403
- }, []);
10404
- return {
10405
- events,
10406
- status,
10407
- isRunning: status === "running",
10408
- error,
10409
- output,
10410
- instanceId,
10411
- run,
10412
- resume,
10413
- reset
10414
- };
10415
- }
10416
-
10417
- // src/integrations/react/use-auth.ts
10418
- function createUseAuth(react, context) {
10419
- return function useAuth() {
10420
- const { useState, useEffect, useRef, useCallback, useContext } = react;
10421
- const ctx = useContext(context);
10422
- if (!ctx) {
10423
- throw new Error("useAuth must be used within a <RaisinProvider>");
10424
- }
10425
- const { client } = ctx;
10426
- const [user, setUser] = useState(client.getStoredUser());
10427
- const [isLoading, setIsLoading] = useState(false);
10428
- const mountedRef = useRef(true);
10429
- useEffect(() => {
10430
- mountedRef.current = true;
10431
- const unsubAuth = client.onAuthStateChange(({ event, session }) => {
10432
- if (!mountedRef.current) return;
10433
- if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
10434
- setUser(session.user);
10435
- } else if (event === "SIGNED_OUT" || event === "SESSION_EXPIRED") {
10436
- setUser(null);
10437
- }
10438
- });
10439
- const unsubUser = client.onUserChange(({ node }) => {
10440
- if (!mountedRef.current) return;
10441
- setUser((prev) => {
10442
- if (!prev) return prev;
10443
- return {
10444
- ...prev,
10445
- home: node.path,
10446
- displayName: node.properties.display_name ?? prev.displayName,
10447
- avatarUrl: node.properties.avatar_url ?? prev.avatarUrl
10448
- };
10449
- });
10450
- });
10451
- return () => {
10452
- mountedRef.current = false;
10453
- unsubAuth();
10454
- unsubUser();
10455
- };
10456
- }, [client]);
10457
- const login = useCallback(async (email, password, repository) => {
10458
- setIsLoading(true);
10459
- try {
10460
- const result = await client.loginWithEmail(email, password, repository);
10461
- return result;
10462
- } finally {
10463
- if (mountedRef.current) setIsLoading(false);
10464
- }
10465
- }, [client]);
10466
- const register = useCallback(async (email, password, repository, displayName) => {
10467
- setIsLoading(true);
10468
- try {
10469
- const result = await client.registerWithEmail(email, password, repository, displayName);
10470
- return result;
10471
- } finally {
10472
- if (mountedRef.current) setIsLoading(false);
10473
- }
10474
- }, [client]);
10475
- const logout = useCallback(async (options) => {
10476
- setIsLoading(true);
10477
- try {
10478
- await client.logout(options);
10479
- } finally {
10480
- if (mountedRef.current) setIsLoading(false);
10481
- }
10482
- }, [client]);
10483
- const initSession = useCallback(async (repository) => {
10484
- setIsLoading(true);
10485
- try {
10486
- const result = await client.initSession(repository);
10487
- return result;
10488
- } finally {
10489
- if (mountedRef.current) setIsLoading(false);
10490
- }
10491
- }, [client]);
10492
- return {
10493
- user,
10494
- isAuthenticated: user !== null,
10495
- isLoading,
10496
- login,
10497
- register,
10498
- logout,
10499
- initSession
10500
- };
10501
- };
10502
- }
10503
-
10504
- // src/integrations/react/use-connection.ts
10505
- function createUseConnection(react, context) {
10506
- return function useConnection() {
10507
- const { useState, useEffect, useCallback, useContext } = react;
10508
- const ctx = useContext(context);
10509
- if (!ctx) {
10510
- throw new Error("useConnection must be used within a <RaisinProvider>");
10511
- }
10512
- const { client } = ctx;
10513
- const [state, setState] = useState(client.getConnectionState());
10514
- const [isReady, setIsReady] = useState(client.isReady());
10515
- useEffect(() => {
10516
- const unsubConn = client.onConnectionStateChange((newState) => {
10517
- setState(newState);
10518
- });
10519
- const unsubReady = client.onReadyStateChange((ready) => {
10520
- setIsReady(ready);
10521
- });
10522
- return () => {
10523
- unsubConn();
10524
- unsubReady();
10525
- };
10526
- }, [client]);
10527
- const connect = useCallback(async () => {
10528
- await client.connect();
10529
- }, [client]);
10530
- const disconnect = useCallback(() => {
10531
- client.disconnect();
10532
- }, [client]);
10533
- return {
10534
- state,
10535
- isConnected: state === "connected" /* Connected */,
10536
- isReady,
10537
- connect,
10538
- disconnect
10539
- };
10540
- };
8249
+ function isFlowDefinitionStep(node) {
8250
+ return node.node_type === "raisin:FlowStep";
10541
8251
  }
10542
-
10543
- // src/integrations/react/use-sql.ts
10544
- function rowsToObjects3(result) {
10545
- return result.rows;
10546
- }
10547
- function createUseSql(react, context) {
10548
- return function useSql(sql, params, options) {
10549
- const { useState, useEffect, useRef, useCallback, useContext } = react;
10550
- const ctx = useContext(context);
10551
- if (!ctx) {
10552
- throw new Error("useSql must be used within a <RaisinProvider>");
10553
- }
10554
- const { client, repository: defaultRepo } = ctx;
10555
- const [data, setData] = useState(null);
10556
- const [isLoading, setIsLoading] = useState(false);
10557
- const [error, setError] = useState(null);
10558
- const mountedRef = useRef(true);
10559
- const enabled = options?.enabled !== false;
10560
- const repo = options?.repository ?? defaultRepo;
10561
- const refetchOnReconnect = options?.refetchOnReconnect !== false;
10562
- const realtime = options?.realtime;
10563
- const paramsKey = params ? JSON.stringify(params) : "";
10564
- const fetchData = useCallback(async () => {
10565
- if (!repo) return;
10566
- setIsLoading(true);
10567
- setError(null);
10568
- try {
10569
- const db = client.database(repo);
10570
- const result = await db.executeSql(sql, params);
10571
- if (mountedRef.current) {
10572
- setData(rowsToObjects3(result));
10573
- }
10574
- } catch (err) {
10575
- if (mountedRef.current) {
10576
- setError(err instanceof Error ? err : new Error(String(err)));
10577
- }
10578
- } finally {
10579
- if (mountedRef.current) {
10580
- setIsLoading(false);
10581
- }
10582
- }
10583
- }, [client, repo, sql, paramsKey]);
10584
- useEffect(() => {
10585
- mountedRef.current = true;
10586
- if (enabled) {
10587
- fetchData();
10588
- }
10589
- return () => {
10590
- mountedRef.current = false;
10591
- };
10592
- }, [enabled, fetchData]);
10593
- useEffect(() => {
10594
- if (!enabled || !refetchOnReconnect) return;
10595
- const unsub = client.onReconnected(() => {
10596
- fetchData();
10597
- });
10598
- return unsub;
10599
- }, [client, enabled, refetchOnReconnect, fetchData]);
10600
- useEffect(() => {
10601
- if (!enabled || !realtime || !repo) return;
10602
- const db = client.database(repo);
10603
- let unsubscribe;
10604
- db.events().subscribe(
10605
- {
10606
- workspace: realtime.workspace,
10607
- event_types: realtime.eventTypes,
10608
- path: realtime.path,
10609
- node_type: realtime.nodeType
10610
- },
10611
- () => {
10612
- fetchData();
10613
- }
10614
- ).then((sub) => {
10615
- unsubscribe = () => sub.unsubscribe();
10616
- });
10617
- return () => {
10618
- unsubscribe?.();
10619
- };
10620
- }, [client, enabled, repo, realtime?.workspace, realtime?.path, realtime?.nodeType, fetchData]);
10621
- return {
10622
- data,
10623
- isLoading,
10624
- error,
10625
- refetch: fetchData
10626
- };
10627
- };
10628
- }
10629
-
10630
- // src/integrations/react/use-subscription.ts
10631
- function createUseSubscription(react, context) {
10632
- return function useSubscription(options, callback) {
10633
- const { useEffect, useRef, useContext } = react;
10634
- const ctx = useContext(context);
10635
- if (!ctx) {
10636
- throw new Error("useSubscription must be used within a <RaisinProvider>");
10637
- }
10638
- const { client, repository } = ctx;
10639
- const callbackRef = useRef(callback);
10640
- callbackRef.current = callback;
10641
- const enabled = options.enabled !== false;
10642
- useEffect(() => {
10643
- if (!enabled || !repository || !options.workspace) return;
10644
- const db = client.database(repository);
10645
- let unsubscribe;
10646
- let cancelled = false;
10647
- db.events().subscribe(
10648
- {
10649
- workspace: options.workspace,
10650
- event_types: options.event_types,
10651
- path: options.path,
10652
- node_type: options.node_type,
10653
- include_node: options.include_node
10654
- },
10655
- (event) => {
10656
- if (!cancelled) {
10657
- callbackRef.current(event);
10658
- }
10659
- }
10660
- ).then((sub) => {
10661
- if (cancelled) {
10662
- sub.unsubscribe();
10663
- } else {
10664
- unsubscribe = () => sub.unsubscribe();
10665
- }
10666
- });
10667
- return () => {
10668
- cancelled = true;
10669
- unsubscribe?.();
10670
- };
10671
- }, [client, repository, enabled, options.workspace, options.path, options.node_type, options.include_node]);
10672
- };
10673
- }
10674
-
10675
- // src/integrations/react/index.ts
10676
- function createRaisinReact(react) {
10677
- const RaisinContext = react.createContext(null);
10678
- function RaisinProvider(props) {
10679
- const { client, repository, children } = props;
10680
- const value = react.useMemo(
10681
- () => ({ client, repository }),
10682
- [client, repository]
10683
- );
10684
- return react.createElement(
10685
- RaisinContext.Provider,
10686
- { value },
10687
- children
10688
- );
10689
- }
10690
- function useRaisinClient() {
10691
- const ctx = react.useContext(RaisinContext);
10692
- if (!ctx) {
10693
- throw new Error("useRaisinClient must be used within a <RaisinProvider>");
10694
- }
10695
- return ctx.client;
10696
- }
10697
- function useDatabase(repository) {
10698
- const ctx = react.useContext(RaisinContext);
10699
- if (!ctx) {
10700
- throw new Error("useDatabase must be used within a <RaisinProvider>");
10701
- }
10702
- const repo = repository ?? ctx.repository;
10703
- if (!repo) {
10704
- throw new Error("useDatabase requires a repository \u2014 pass it as an argument or set it on <RaisinProvider>");
10705
- }
10706
- return react.useMemo(() => ctx.client.database(repo), [ctx.client, repo]);
10707
- }
10708
- const useAuth = createUseAuth(react, RaisinContext);
10709
- const useConnection = createUseConnection(react, RaisinContext);
10710
- const useSql = createUseSql(react, RaisinContext);
10711
- const useSubscription = createUseSubscription(react, RaisinContext);
10712
- function useConversation2(options) {
10713
- return useConversation(react, options);
10714
- }
10715
- function useConversationList2(options) {
10716
- return useConversationList(react, options);
10717
- }
10718
- function useFlow2(options) {
10719
- return useFlow(react, options);
10720
- }
10721
- return {
10722
- RaisinProvider,
10723
- useRaisinClient,
10724
- useDatabase,
10725
- useAuth,
10726
- useConnection,
10727
- useSql,
10728
- useSubscription,
10729
- useConversation: useConversation2,
10730
- useConversationList: useConversationList2,
10731
- useFlow: useFlow2
10732
- };
8252
+ function isFlowDefinitionContainer(node) {
8253
+ return node.node_type === "raisin:FlowContainer";
10733
8254
  }
10734
8255
  export {
10735
8256
  AllNodeEventTypes,
@@ -10751,6 +8272,7 @@ export {
10751
8272
  DEFAULT_RETRY_BASE_DELAY,
10752
8273
  DEFAULT_RETRY_MAX_DELAY,
10753
8274
  DEFAULT_RETRY_OPTIONS,
8275
+ DEFAULT_SEND_MESSAGE_INACTIVITY_TIMEOUT_MS,
10754
8276
  Database,
10755
8277
  ElementTypes,
10756
8278
  EventHandler,
@@ -10761,6 +8283,7 @@ export {
10761
8283
  HttpDatabase,
10762
8284
  HttpFunctionsApi,
10763
8285
  HttpWorkspaceClient,
8286
+ InboxApi,
10764
8287
  LocalStorageTokenStorage,
10765
8288
  LogLevel,
10766
8289
  MemoryTokenStorage,
@@ -10771,6 +8294,7 @@ export {
10771
8294
  NodeOperations,
10772
8295
  NodeTypes,
10773
8296
  RAISIN_CONTEXT_KEY,
8297
+ RETRY_STRATEGIES,
10774
8298
  RaisinAbortError,
10775
8299
  RaisinAuthError,
10776
8300
  RaisinClient,
@@ -10806,6 +8330,7 @@ export {
10806
8330
  createFlowAdapter,
10807
8331
  createLoader,
10808
8332
  createRaisinReact,
8333
+ createRaisinVue,
10809
8334
  createRequestScopedClient,
10810
8335
  createSSRClient,
10811
8336
  createSqlAdapter,
@@ -10818,13 +8343,18 @@ export {
10818
8343
  getErrorMessage,
10819
8344
  getLogLevel,
10820
8345
  getLoggerConfig,
8346
+ getRefDisplayName,
8347
+ getRefPath,
10821
8348
  isEventMessage,
8349
+ isFlowDefinitionContainer,
8350
+ isFlowDefinitionStep,
10822
8351
  isHttpClient,
10823
8352
  isResponseEnvelope,
10824
8353
  isRetryableError,
10825
8354
  isTerminalEvent,
10826
8355
  isWebSocketClient,
10827
8356
  logger,
8357
+ normalizeHomePath,
10828
8358
  projectPlansFromMessages,
10829
8359
  rowsToObjects,
10830
8360
  setLogLevel,
@@ -10834,3 +8364,4 @@ export {
10834
8364
  useFlow,
10835
8365
  withRetry
10836
8366
  };
8367
+ //# sourceMappingURL=index.mjs.map