@raisindb/client 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -70
- package/dist/chunk-ASOWGNQZ.mjs +532 -0
- package/dist/chunk-ASOWGNQZ.mjs.map +1 -0
- package/dist/chunk-FYWYSNQG.mjs +190 -0
- package/dist/chunk-FYWYSNQG.mjs.map +1 -0
- package/dist/chunk-NOPZAMIV.mjs +1427 -0
- package/dist/chunk-NOPZAMIV.mjs.map +1 -0
- package/dist/chunk-T2ZSNYFG.mjs +248 -0
- package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
- package/dist/chunk-TPZNJNZR.mjs +866 -0
- package/dist/chunk-TPZNJNZR.mjs.map +1 -0
- package/dist/chunk-ZCRYQCXS.mjs +170 -0
- package/dist/chunk-ZCRYQCXS.mjs.map +1 -0
- package/dist/conversation-list-store-D-W6_nsv.d.mts +8447 -0
- package/dist/conversation-list-store-D-W6_nsv.d.ts +8447 -0
- package/dist/index-B4CqE3Cb.d.ts +301 -0
- package/dist/index-CeBRaSop.d.mts +301 -0
- package/dist/index.d.mts +72 -6109
- package/dist/index.d.ts +72 -6109
- package/dist/index.js +3123 -257
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2745 -3185
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +4 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.js +2569 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +9 -0
- package/dist/react.mjs.map +1 -0
- package/dist/svelte-sql-CQoB4FEm.d.mts +52 -0
- package/dist/svelte-sql-CTXjkz0R.d.ts +52 -0
- package/dist/svelte.d.mts +288 -0
- package/dist/svelte.d.ts +288 -0
- package/dist/svelte.js +2482 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +25 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/vue.d.mts +197 -0
- package/dist/vue.d.ts +197 -0
- package/dist/vue.js +1510 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +9 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +18 -3
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,2482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/integrations/svelte/index.ts
|
|
21
|
+
var svelte_exports = {};
|
|
22
|
+
__export(svelte_exports, {
|
|
23
|
+
RAISIN_CONTEXT_KEY: () => RAISIN_CONTEXT_KEY,
|
|
24
|
+
createAuthAdapter: () => createAuthAdapter,
|
|
25
|
+
createConnectionAdapter: () => createConnectionAdapter,
|
|
26
|
+
createConversationAdapter: () => createConversationAdapter,
|
|
27
|
+
createConversationListAdapter: () => createConversationListAdapter,
|
|
28
|
+
createFlowAdapter: () => createFlowAdapter,
|
|
29
|
+
createSqlAdapter: () => createSqlAdapter,
|
|
30
|
+
createSubscriptionAdapter: () => createSubscriptionAdapter
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(svelte_exports);
|
|
33
|
+
|
|
34
|
+
// src/integrations/svelte/svelte-auth.ts
|
|
35
|
+
function createAuthAdapter(client) {
|
|
36
|
+
let snapshot = {
|
|
37
|
+
user: client.getStoredUser(),
|
|
38
|
+
isAuthenticated: client.getStoredUser() !== null,
|
|
39
|
+
isLoading: false
|
|
40
|
+
};
|
|
41
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
42
|
+
function emit() {
|
|
43
|
+
for (const cb of listeners) cb(snapshot);
|
|
44
|
+
}
|
|
45
|
+
function update(partial) {
|
|
46
|
+
snapshot = { ...snapshot, ...partial };
|
|
47
|
+
emit();
|
|
48
|
+
}
|
|
49
|
+
const unsubAuth = client.onAuthStateChange(({ event, session }) => {
|
|
50
|
+
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
|
|
51
|
+
update({ user: session.user, isAuthenticated: true });
|
|
52
|
+
} else if (event === "SIGNED_OUT" || event === "SESSION_EXPIRED") {
|
|
53
|
+
update({ user: null, isAuthenticated: false });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const unsubUser = client.onUserChange(({ node }) => {
|
|
57
|
+
if (!snapshot.user) return;
|
|
58
|
+
update({
|
|
59
|
+
user: {
|
|
60
|
+
...snapshot.user,
|
|
61
|
+
home: node.path,
|
|
62
|
+
displayName: node.properties.display_name ?? snapshot.user.displayName,
|
|
63
|
+
avatarUrl: node.properties.avatar_url ?? snapshot.user.avatarUrl
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
subscribe(cb) {
|
|
69
|
+
listeners.add(cb);
|
|
70
|
+
return () => {
|
|
71
|
+
listeners.delete(cb);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
getSnapshot: () => snapshot,
|
|
75
|
+
async login(email, password, repository) {
|
|
76
|
+
update({ isLoading: true });
|
|
77
|
+
try {
|
|
78
|
+
return await client.loginWithEmail(email, password, repository);
|
|
79
|
+
} finally {
|
|
80
|
+
update({ isLoading: false });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async register(email, password, repository, displayName) {
|
|
84
|
+
update({ isLoading: true });
|
|
85
|
+
try {
|
|
86
|
+
return await client.registerWithEmail(email, password, repository, displayName);
|
|
87
|
+
} finally {
|
|
88
|
+
update({ isLoading: false });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async logout(options) {
|
|
92
|
+
update({ isLoading: true });
|
|
93
|
+
try {
|
|
94
|
+
await client.logout(options);
|
|
95
|
+
} finally {
|
|
96
|
+
update({ isLoading: false });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async initSession(repository) {
|
|
100
|
+
update({ isLoading: true });
|
|
101
|
+
try {
|
|
102
|
+
return await client.initSession(repository);
|
|
103
|
+
} finally {
|
|
104
|
+
update({ isLoading: false });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
destroy() {
|
|
108
|
+
unsubAuth();
|
|
109
|
+
unsubUser();
|
|
110
|
+
listeners.clear();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/connection.ts
|
|
116
|
+
var import_events = require("events");
|
|
117
|
+
|
|
118
|
+
// src/utils/reconnect.ts
|
|
119
|
+
var ReconnectManager = class {
|
|
120
|
+
constructor(options = {}) {
|
|
121
|
+
this.attempts = 0;
|
|
122
|
+
this.isReconnecting = false;
|
|
123
|
+
this.initialDelay = options.initialDelay ?? 1e3;
|
|
124
|
+
this.maxDelay = options.maxDelay ?? 3e4;
|
|
125
|
+
this.backoffMultiplier = options.backoffMultiplier ?? 2;
|
|
126
|
+
this.maxAttempts = options.maxAttempts ?? Infinity;
|
|
127
|
+
this.jitter = options.jitter ?? true;
|
|
128
|
+
this.currentDelay = this.initialDelay;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Schedule a reconnection attempt
|
|
132
|
+
*
|
|
133
|
+
* @param callback - Function to call when it's time to reconnect
|
|
134
|
+
* @returns true if reconnection was scheduled, false if max attempts reached
|
|
135
|
+
*/
|
|
136
|
+
scheduleReconnect(callback) {
|
|
137
|
+
if (this.attempts >= this.maxAttempts) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
this.cancelReconnect();
|
|
141
|
+
this.isReconnecting = true;
|
|
142
|
+
this.attempts++;
|
|
143
|
+
let delay = this.currentDelay;
|
|
144
|
+
if (this.jitter) {
|
|
145
|
+
const jitterRange = delay * 0.25;
|
|
146
|
+
delay += (Math.random() * 2 - 1) * jitterRange;
|
|
147
|
+
delay = Math.max(0, Math.round(delay));
|
|
148
|
+
}
|
|
149
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
150
|
+
try {
|
|
151
|
+
await callback();
|
|
152
|
+
this.reset();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.currentDelay = Math.min(
|
|
155
|
+
this.currentDelay * this.backoffMultiplier,
|
|
156
|
+
this.maxDelay
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}, delay);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Cancel any pending reconnection attempt
|
|
164
|
+
*/
|
|
165
|
+
cancelReconnect() {
|
|
166
|
+
if (this.reconnectTimeout) {
|
|
167
|
+
clearTimeout(this.reconnectTimeout);
|
|
168
|
+
this.reconnectTimeout = void 0;
|
|
169
|
+
}
|
|
170
|
+
this.isReconnecting = false;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reset the reconnection state (call after successful connection)
|
|
174
|
+
*/
|
|
175
|
+
reset() {
|
|
176
|
+
this.cancelReconnect();
|
|
177
|
+
this.currentDelay = this.initialDelay;
|
|
178
|
+
this.attempts = 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if currently in reconnection mode
|
|
182
|
+
*/
|
|
183
|
+
isActive() {
|
|
184
|
+
return this.isReconnecting;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get the current number of reconnection attempts
|
|
188
|
+
*/
|
|
189
|
+
getAttempts() {
|
|
190
|
+
return this.attempts;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get the current delay in milliseconds
|
|
194
|
+
*/
|
|
195
|
+
getCurrentDelay() {
|
|
196
|
+
return this.currentDelay;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if max attempts have been reached
|
|
200
|
+
*/
|
|
201
|
+
hasReachedMaxAttempts() {
|
|
202
|
+
return this.attempts >= this.maxAttempts;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/logger.ts
|
|
207
|
+
var config = {
|
|
208
|
+
level: 1 /* Error */,
|
|
209
|
+
prefix: "[RaisinDB]"
|
|
210
|
+
};
|
|
211
|
+
function setLogLevel(level) {
|
|
212
|
+
config.level = level;
|
|
213
|
+
}
|
|
214
|
+
function getLogLevel() {
|
|
215
|
+
return config.level;
|
|
216
|
+
}
|
|
217
|
+
var logger = {
|
|
218
|
+
/**
|
|
219
|
+
* Log debug message (only shown when level >= Debug)
|
|
220
|
+
*/
|
|
221
|
+
debug(...args) {
|
|
222
|
+
if (config.level >= 4 /* Debug */) {
|
|
223
|
+
console.log(config.prefix, ...args);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
/**
|
|
227
|
+
* Log info message (only shown when level >= Info)
|
|
228
|
+
*/
|
|
229
|
+
info(...args) {
|
|
230
|
+
if (config.level >= 3 /* Info */) {
|
|
231
|
+
console.log(config.prefix, ...args);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Log warning message (only shown when level >= Warn)
|
|
236
|
+
*/
|
|
237
|
+
warn(...args) {
|
|
238
|
+
if (config.level >= 2 /* Warn */) {
|
|
239
|
+
console.warn(config.prefix, ...args);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
/**
|
|
243
|
+
* Log error message (only shown when level >= Error)
|
|
244
|
+
*/
|
|
245
|
+
error(...args) {
|
|
246
|
+
if (config.level >= 1 /* Error */) {
|
|
247
|
+
console.error(config.prefix, ...args);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
if (typeof globalThis !== "undefined") {
|
|
252
|
+
globalThis.__raisinDebug = (enable = true) => {
|
|
253
|
+
setLogLevel(enable ? 4 /* Debug */ : 1 /* Error */);
|
|
254
|
+
console.log(`[RaisinDB] Debug logging ${enable ? "ENABLED" : "DISABLED"} (includes server handler logs)`);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/integrations/svelte/svelte-connection.ts
|
|
259
|
+
function createConnectionAdapter(client) {
|
|
260
|
+
let snapshot = {
|
|
261
|
+
state: client.getConnectionState(),
|
|
262
|
+
isConnected: client.getConnectionState() === "connected" /* Connected */,
|
|
263
|
+
isReady: client.isReady()
|
|
264
|
+
};
|
|
265
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
266
|
+
function emit() {
|
|
267
|
+
for (const cb of listeners) cb(snapshot);
|
|
268
|
+
}
|
|
269
|
+
const unsubConn = client.onConnectionStateChange((newState) => {
|
|
270
|
+
snapshot = {
|
|
271
|
+
state: newState,
|
|
272
|
+
isConnected: newState === "connected" /* Connected */,
|
|
273
|
+
isReady: snapshot.isReady
|
|
274
|
+
};
|
|
275
|
+
emit();
|
|
276
|
+
});
|
|
277
|
+
const unsubReady = client.onReadyStateChange((ready) => {
|
|
278
|
+
snapshot = { ...snapshot, isReady: ready };
|
|
279
|
+
emit();
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
subscribe(cb) {
|
|
283
|
+
listeners.add(cb);
|
|
284
|
+
return () => {
|
|
285
|
+
listeners.delete(cb);
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
getSnapshot: () => snapshot,
|
|
289
|
+
async connect() {
|
|
290
|
+
await client.connect();
|
|
291
|
+
},
|
|
292
|
+
disconnect() {
|
|
293
|
+
client.disconnect();
|
|
294
|
+
},
|
|
295
|
+
destroy() {
|
|
296
|
+
unsubConn();
|
|
297
|
+
unsubReady();
|
|
298
|
+
listeners.clear();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/integrations/svelte/svelte-sql.ts
|
|
304
|
+
function rowsToObjects(result) {
|
|
305
|
+
return result.rows;
|
|
306
|
+
}
|
|
307
|
+
function createSqlAdapter(database, sql, params, options) {
|
|
308
|
+
let snapshot = {
|
|
309
|
+
data: null,
|
|
310
|
+
isLoading: false,
|
|
311
|
+
error: null
|
|
312
|
+
};
|
|
313
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
314
|
+
let destroyed = false;
|
|
315
|
+
function emit() {
|
|
316
|
+
for (const cb of listeners) cb(snapshot);
|
|
317
|
+
}
|
|
318
|
+
function update(partial) {
|
|
319
|
+
snapshot = { ...snapshot, ...partial };
|
|
320
|
+
emit();
|
|
321
|
+
}
|
|
322
|
+
async function fetchData() {
|
|
323
|
+
if (destroyed) return;
|
|
324
|
+
update({ isLoading: true, error: null });
|
|
325
|
+
try {
|
|
326
|
+
const result = await database.executeSql(sql, params);
|
|
327
|
+
if (!destroyed) {
|
|
328
|
+
update({ data: rowsToObjects(result), isLoading: false });
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (!destroyed) {
|
|
332
|
+
update({
|
|
333
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
334
|
+
isLoading: false
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const cleanups = [];
|
|
340
|
+
const enabled = options?.enabled !== false;
|
|
341
|
+
if (enabled) {
|
|
342
|
+
fetchData();
|
|
343
|
+
}
|
|
344
|
+
const refetchOnReconnect = options?.refetchOnReconnect !== false;
|
|
345
|
+
if (enabled && refetchOnReconnect) {
|
|
346
|
+
const client = database._client;
|
|
347
|
+
if (client?.onReconnected) {
|
|
348
|
+
const unsub = client.onReconnected(() => {
|
|
349
|
+
fetchData();
|
|
350
|
+
});
|
|
351
|
+
cleanups.push(unsub);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const realtime = options?.realtime;
|
|
355
|
+
if (enabled && realtime) {
|
|
356
|
+
let unsubscribe;
|
|
357
|
+
let cancelled = false;
|
|
358
|
+
database.events().subscribe(
|
|
359
|
+
{
|
|
360
|
+
workspace: realtime.workspace,
|
|
361
|
+
event_types: realtime.eventTypes,
|
|
362
|
+
path: realtime.path,
|
|
363
|
+
node_type: realtime.nodeType
|
|
364
|
+
},
|
|
365
|
+
() => {
|
|
366
|
+
fetchData();
|
|
367
|
+
}
|
|
368
|
+
).then((sub) => {
|
|
369
|
+
if (cancelled) {
|
|
370
|
+
sub.unsubscribe();
|
|
371
|
+
} else {
|
|
372
|
+
unsubscribe = () => sub.unsubscribe();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
cleanups.push(() => {
|
|
376
|
+
cancelled = true;
|
|
377
|
+
unsubscribe?.();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
subscribe(cb) {
|
|
382
|
+
listeners.add(cb);
|
|
383
|
+
return () => {
|
|
384
|
+
listeners.delete(cb);
|
|
385
|
+
};
|
|
386
|
+
},
|
|
387
|
+
getSnapshot: () => snapshot,
|
|
388
|
+
refetch: fetchData,
|
|
389
|
+
destroy() {
|
|
390
|
+
destroyed = true;
|
|
391
|
+
for (const cleanup of cleanups) cleanup();
|
|
392
|
+
listeners.clear();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/integrations/svelte/svelte-subscription.ts
|
|
398
|
+
function createSubscriptionAdapter(database, filters, callback) {
|
|
399
|
+
let unsubscribe;
|
|
400
|
+
let cancelled = false;
|
|
401
|
+
database.events().subscribe(filters, (event) => {
|
|
402
|
+
if (!cancelled) callback(event);
|
|
403
|
+
}).then((sub) => {
|
|
404
|
+
if (cancelled) {
|
|
405
|
+
sub.unsubscribe();
|
|
406
|
+
} else {
|
|
407
|
+
unsubscribe = () => sub.unsubscribe();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
destroy() {
|
|
412
|
+
cancelled = true;
|
|
413
|
+
unsubscribe?.();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/errors.ts
|
|
419
|
+
var RaisinError = class extends Error {
|
|
420
|
+
constructor(message, code, details) {
|
|
421
|
+
super(message);
|
|
422
|
+
this.name = "RaisinError";
|
|
423
|
+
this.code = code;
|
|
424
|
+
this.details = details;
|
|
425
|
+
if (Error.captureStackTrace) {
|
|
426
|
+
Error.captureStackTrace(this, this.constructor);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
var RaisinConnectionError = class extends RaisinError {
|
|
431
|
+
constructor(message, code, options) {
|
|
432
|
+
super(message, code, options?.details);
|
|
433
|
+
this.name = "RaisinConnectionError";
|
|
434
|
+
this.status = options?.status;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
var RaisinAuthError = class extends RaisinError {
|
|
438
|
+
constructor(message, code, status, details) {
|
|
439
|
+
super(message, code, details);
|
|
440
|
+
this.name = "RaisinAuthError";
|
|
441
|
+
this.status = status;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var RaisinFlowError = class extends RaisinError {
|
|
445
|
+
constructor(message, code, instanceId, details) {
|
|
446
|
+
super(message, code, details);
|
|
447
|
+
this.name = "RaisinFlowError";
|
|
448
|
+
this.instanceId = instanceId;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var RaisinTimeoutError = class extends RaisinError {
|
|
452
|
+
constructor(message, code, timeoutMs, details) {
|
|
453
|
+
super(message, code, details);
|
|
454
|
+
this.name = "RaisinTimeoutError";
|
|
455
|
+
this.timeoutMs = timeoutMs;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
var RaisinAbortError = class extends RaisinError {
|
|
459
|
+
constructor(message = "Operation was aborted") {
|
|
460
|
+
super(message, "ABORTED");
|
|
461
|
+
this.name = "RaisinAbortError";
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function classifyHttpError(status, message, details) {
|
|
465
|
+
if (status === 401) {
|
|
466
|
+
return new RaisinAuthError(message, "AUTH_UNAUTHORIZED", status, details);
|
|
467
|
+
}
|
|
468
|
+
if (status === 403) {
|
|
469
|
+
return new RaisinAuthError(message, "AUTH_FORBIDDEN", status, details);
|
|
470
|
+
}
|
|
471
|
+
if (status === 404) {
|
|
472
|
+
return new RaisinFlowError(message, "FLOW_NOT_FOUND", void 0, details);
|
|
473
|
+
}
|
|
474
|
+
return new RaisinConnectionError(message, "CONNECTION_FAILED", { status, details });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/streaming/sse-client.ts
|
|
478
|
+
function parseSSEBlock(block) {
|
|
479
|
+
let event = "message";
|
|
480
|
+
let data = "";
|
|
481
|
+
let id;
|
|
482
|
+
const lines = block.split("\n");
|
|
483
|
+
for (const line of lines) {
|
|
484
|
+
if (line.startsWith("event:")) {
|
|
485
|
+
event = line.slice(6).trim();
|
|
486
|
+
} else if (line.startsWith("data:")) {
|
|
487
|
+
if (data) data += "\n";
|
|
488
|
+
data += line.slice(5).trim();
|
|
489
|
+
} else if (line.startsWith("id:")) {
|
|
490
|
+
id = line.slice(3).trim();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (!data) return null;
|
|
494
|
+
return { event, data, id };
|
|
495
|
+
}
|
|
496
|
+
var SSEClient = class {
|
|
497
|
+
constructor(url, options = {}) {
|
|
498
|
+
this.abortController = null;
|
|
499
|
+
this._state = "disconnected";
|
|
500
|
+
this.url = url;
|
|
501
|
+
this.options = options;
|
|
502
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
503
|
+
this._lastEventId = options.lastEventId;
|
|
504
|
+
this.autoReconnect = options.reconnect?.enabled ?? true;
|
|
505
|
+
this.externalSignal = options.signal;
|
|
506
|
+
this.inactivityTimeoutMs = options.inactivityTimeoutMs;
|
|
507
|
+
this.eventTypeSet = options.eventTypes?.length ? new Set(options.eventTypes) : null;
|
|
508
|
+
this.reconnectManager = new ReconnectManager({
|
|
509
|
+
initialDelay: options.reconnect?.initialDelay ?? 1e3,
|
|
510
|
+
maxDelay: options.reconnect?.maxDelay ?? 3e4,
|
|
511
|
+
backoffMultiplier: options.reconnect?.backoffMultiplier ?? 2,
|
|
512
|
+
maxAttempts: options.reconnect?.maxAttempts ?? Infinity
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/** Current connection state */
|
|
516
|
+
get state() {
|
|
517
|
+
return this._state;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Returns a promise that resolves once the SSE connection is established.
|
|
521
|
+
*
|
|
522
|
+
* Useful for ensuring the connection is ready before triggering events
|
|
523
|
+
* on the server side (e.g., calling resume before consuming the stream).
|
|
524
|
+
* If the client is already connected, resolves immediately.
|
|
525
|
+
*/
|
|
526
|
+
waitUntilConnected() {
|
|
527
|
+
if (this._state === "connected") return Promise.resolve();
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
529
|
+
const originalOnState = this.options.onStateChange;
|
|
530
|
+
this.options.onStateChange = (state) => {
|
|
531
|
+
originalOnState?.(state);
|
|
532
|
+
if (state === "connected") {
|
|
533
|
+
this.options.onStateChange = originalOnState;
|
|
534
|
+
resolve();
|
|
535
|
+
} else if (state === "disconnected") {
|
|
536
|
+
this.options.onStateChange = originalOnState;
|
|
537
|
+
reject(new Error("SSE connection failed"));
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/** Last received event ID */
|
|
543
|
+
get lastEventId() {
|
|
544
|
+
return this._lastEventId;
|
|
545
|
+
}
|
|
546
|
+
setState(state) {
|
|
547
|
+
if (this._state !== state) {
|
|
548
|
+
this._state = state;
|
|
549
|
+
this.options.onStateChange?.(state);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Connect and consume events via callback.
|
|
554
|
+
*
|
|
555
|
+
* @param onEvent - Called for each matching SSE event
|
|
556
|
+
* @param onError - Called when an error occurs (optional)
|
|
557
|
+
*/
|
|
558
|
+
async connect(onEvent, onError) {
|
|
559
|
+
this.close();
|
|
560
|
+
await this.startStream(onEvent, onError);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Close the SSE connection and stop reconnection attempts.
|
|
564
|
+
*/
|
|
565
|
+
close() {
|
|
566
|
+
this.reconnectManager.cancelReconnect();
|
|
567
|
+
if (this.abortController) {
|
|
568
|
+
this.abortController.abort();
|
|
569
|
+
this.abortController = null;
|
|
570
|
+
}
|
|
571
|
+
this.setState("disconnected");
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* AsyncIterable interface: allows `for await (const event of sseClient)`.
|
|
575
|
+
*
|
|
576
|
+
* The iterator ends when close() is called or the server closes the stream
|
|
577
|
+
* (and auto-reconnect is disabled or max attempts are exhausted).
|
|
578
|
+
*/
|
|
579
|
+
async *[Symbol.asyncIterator]() {
|
|
580
|
+
const queue = [];
|
|
581
|
+
let resolve = null;
|
|
582
|
+
let finished = false;
|
|
583
|
+
const push = (item) => {
|
|
584
|
+
if (finished) return;
|
|
585
|
+
if (resolve) {
|
|
586
|
+
const r = resolve;
|
|
587
|
+
resolve = null;
|
|
588
|
+
r(item);
|
|
589
|
+
} else {
|
|
590
|
+
queue.push(item);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const pull = () => {
|
|
594
|
+
const queued = queue.shift();
|
|
595
|
+
if (queued) return Promise.resolve(queued);
|
|
596
|
+
return new Promise((r) => {
|
|
597
|
+
resolve = r;
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
this.startStream(
|
|
601
|
+
(event) => push({ done: false, value: event }),
|
|
602
|
+
(error) => push({ done: true, error })
|
|
603
|
+
).then(() => {
|
|
604
|
+
push({ done: true });
|
|
605
|
+
}).catch((error) => {
|
|
606
|
+
push({ done: true, error });
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
while (!finished) {
|
|
610
|
+
const item = await pull();
|
|
611
|
+
if (item.done) {
|
|
612
|
+
finished = true;
|
|
613
|
+
if (item.error) {
|
|
614
|
+
throw item.error;
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
yield item.value;
|
|
619
|
+
}
|
|
620
|
+
} finally {
|
|
621
|
+
finished = true;
|
|
622
|
+
this.close();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Core streaming loop: connects, reads the SSE stream, and handles reconnection.
|
|
627
|
+
*/
|
|
628
|
+
async startStream(onEvent, onError) {
|
|
629
|
+
while (true) {
|
|
630
|
+
try {
|
|
631
|
+
await this.readStream(onEvent);
|
|
632
|
+
if (!this.autoReconnect || this._state === "disconnected") {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
this.setState("reconnecting");
|
|
636
|
+
const scheduled = await this.waitForReconnect();
|
|
637
|
+
if (!scheduled) {
|
|
638
|
+
logger.warn("SSE max reconnection attempts reached");
|
|
639
|
+
this.setState("disconnected");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (this._state === "disconnected") {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
647
|
+
if (err.name === "AbortError" || err instanceof RaisinAbortError) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (err instanceof RaisinTimeoutError && err.code === "SSE_INACTIVITY_TIMEOUT") {
|
|
651
|
+
logger.warn("SSE inactivity timeout:", err.message);
|
|
652
|
+
if (!this.autoReconnect) {
|
|
653
|
+
this.setState("disconnected");
|
|
654
|
+
onError?.(err);
|
|
655
|
+
throw err;
|
|
656
|
+
}
|
|
657
|
+
this.setState("reconnecting");
|
|
658
|
+
const scheduled2 = await this.waitForReconnect();
|
|
659
|
+
if (!scheduled2) {
|
|
660
|
+
logger.warn("SSE max reconnection attempts reached");
|
|
661
|
+
this.setState("disconnected");
|
|
662
|
+
onError?.(err);
|
|
663
|
+
throw err;
|
|
664
|
+
}
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
logger.error("SSE connection error:", err.message);
|
|
668
|
+
onError?.(err);
|
|
669
|
+
if (!this.autoReconnect) {
|
|
670
|
+
this.setState("disconnected");
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
this.setState("reconnecting");
|
|
674
|
+
const scheduled = await this.waitForReconnect();
|
|
675
|
+
if (!scheduled) {
|
|
676
|
+
logger.warn("SSE max reconnection attempts reached");
|
|
677
|
+
this.setState("disconnected");
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Perform a single fetch and read the response body as SSE stream.
|
|
685
|
+
*/
|
|
686
|
+
async readStream(onEvent) {
|
|
687
|
+
this.abortController = new AbortController();
|
|
688
|
+
this.setState("connecting");
|
|
689
|
+
if (this.externalSignal) {
|
|
690
|
+
if (this.externalSignal.aborted) {
|
|
691
|
+
this.abortController.abort();
|
|
692
|
+
} else {
|
|
693
|
+
const ctrl = this.abortController;
|
|
694
|
+
this.externalSignal.addEventListener("abort", () => ctrl.abort(), { once: true });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const headers = {
|
|
698
|
+
Accept: "text/event-stream",
|
|
699
|
+
"Cache-Control": "no-cache",
|
|
700
|
+
...this.options.headers
|
|
701
|
+
};
|
|
702
|
+
if (this._lastEventId) {
|
|
703
|
+
headers["Last-Event-ID"] = this._lastEventId;
|
|
704
|
+
}
|
|
705
|
+
const method = this.options.method ?? "GET";
|
|
706
|
+
const fetchOptions = {
|
|
707
|
+
method,
|
|
708
|
+
headers,
|
|
709
|
+
signal: this.abortController.signal
|
|
710
|
+
};
|
|
711
|
+
if (method === "POST" && this.options.body !== void 0) {
|
|
712
|
+
fetchOptions.headers["Content-Type"] = "application/json";
|
|
713
|
+
fetchOptions.body = JSON.stringify(this.options.body);
|
|
714
|
+
}
|
|
715
|
+
const response = await this.withInactivityTimeout(
|
|
716
|
+
this.fetchImpl(this.url, fetchOptions)
|
|
717
|
+
);
|
|
718
|
+
if (!response.ok) {
|
|
719
|
+
const text = await response.text().catch(() => "");
|
|
720
|
+
const msg = `SSE connection failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`;
|
|
721
|
+
if (response.status === 401 || response.status === 403) {
|
|
722
|
+
throw new RaisinAuthError(
|
|
723
|
+
msg,
|
|
724
|
+
response.status === 401 ? "AUTH_UNAUTHORIZED" : "AUTH_FORBIDDEN",
|
|
725
|
+
response.status
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
throw new RaisinConnectionError(msg, "SSE_STREAM_ERROR", { status: response.status });
|
|
729
|
+
}
|
|
730
|
+
if (!response.body) {
|
|
731
|
+
throw new RaisinConnectionError("SSE response has no body", "SSE_STREAM_ERROR");
|
|
732
|
+
}
|
|
733
|
+
this.setState("connected");
|
|
734
|
+
this.reconnectManager.reset();
|
|
735
|
+
const reader = response.body.getReader();
|
|
736
|
+
const decoder = new TextDecoder();
|
|
737
|
+
let buffer = "";
|
|
738
|
+
try {
|
|
739
|
+
while (true) {
|
|
740
|
+
const { done, value } = await this.withInactivityTimeout(reader.read());
|
|
741
|
+
if (done) break;
|
|
742
|
+
buffer += decoder.decode(value, { stream: true });
|
|
743
|
+
const blocks = buffer.split("\n\n");
|
|
744
|
+
buffer = blocks.pop() ?? "";
|
|
745
|
+
for (const block of blocks) {
|
|
746
|
+
const trimmed = block.trim();
|
|
747
|
+
if (!trimmed) continue;
|
|
748
|
+
const parsed = parseSSEBlock(trimmed);
|
|
749
|
+
if (!parsed) continue;
|
|
750
|
+
if (parsed.id) {
|
|
751
|
+
this._lastEventId = parsed.id;
|
|
752
|
+
}
|
|
753
|
+
if (this.eventTypeSet && !this.eventTypeSet.has(parsed.event)) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
let data;
|
|
757
|
+
try {
|
|
758
|
+
data = JSON.parse(parsed.data);
|
|
759
|
+
} catch {
|
|
760
|
+
data = parsed.data;
|
|
761
|
+
}
|
|
762
|
+
const sseEvent = {
|
|
763
|
+
type: parsed.event,
|
|
764
|
+
data,
|
|
765
|
+
id: parsed.id
|
|
766
|
+
};
|
|
767
|
+
onEvent(sseEvent);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} finally {
|
|
771
|
+
try {
|
|
772
|
+
reader.releaseLock();
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Race a stream operation against the inactivity timeout.
|
|
779
|
+
*
|
|
780
|
+
* If no bytes (or response) arrive within `inactivityTimeoutMs`, the
|
|
781
|
+
* internal connection is aborted and the operation rejects with a
|
|
782
|
+
* `RaisinTimeoutError` (code `SSE_INACTIVITY_TIMEOUT`). The timer is
|
|
783
|
+
* effectively reset on every chunk because each `reader.read()` is
|
|
784
|
+
* wrapped individually.
|
|
785
|
+
*/
|
|
786
|
+
withInactivityTimeout(promise) {
|
|
787
|
+
const timeoutMs = this.inactivityTimeoutMs;
|
|
788
|
+
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
789
|
+
return new Promise((resolve, reject) => {
|
|
790
|
+
const timer = setTimeout(() => {
|
|
791
|
+
promise.catch(() => void 0);
|
|
792
|
+
this.abortController?.abort();
|
|
793
|
+
reject(
|
|
794
|
+
new RaisinTimeoutError(
|
|
795
|
+
`SSE stream inactive for ${timeoutMs}ms`,
|
|
796
|
+
"SSE_INACTIVITY_TIMEOUT",
|
|
797
|
+
timeoutMs
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
}, timeoutMs);
|
|
801
|
+
promise.then(
|
|
802
|
+
(value) => {
|
|
803
|
+
clearTimeout(timer);
|
|
804
|
+
resolve(value);
|
|
805
|
+
},
|
|
806
|
+
(error) => {
|
|
807
|
+
clearTimeout(timer);
|
|
808
|
+
reject(error);
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Wait for the reconnect manager to schedule a reconnection attempt.
|
|
815
|
+
* Returns true if reconnection was scheduled, false if max attempts reached.
|
|
816
|
+
*/
|
|
817
|
+
waitForReconnect() {
|
|
818
|
+
return new Promise((resolve) => {
|
|
819
|
+
const scheduled = this.reconnectManager.scheduleReconnect(() => {
|
|
820
|
+
resolve(true);
|
|
821
|
+
});
|
|
822
|
+
if (!scheduled) {
|
|
823
|
+
resolve(false);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/flow-client.ts
|
|
830
|
+
var FlowClient = class _FlowClient {
|
|
831
|
+
/**
|
|
832
|
+
* Create a new FlowClient.
|
|
833
|
+
*
|
|
834
|
+
* @param baseUrl - Base HTTP URL of the RaisinDB server (e.g., "http://localhost:8081")
|
|
835
|
+
* @param repository - Repository name
|
|
836
|
+
* @param authManager - Auth manager for token access
|
|
837
|
+
* @param options - Additional options
|
|
838
|
+
* @param flowsApi - Optional FlowsApi for WebSocket-based flow operations
|
|
839
|
+
*/
|
|
840
|
+
constructor(baseUrl, repository, authManager, options = {}, flowsApi) {
|
|
841
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
842
|
+
this.repository = repository;
|
|
843
|
+
this.authManager = authManager;
|
|
844
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
845
|
+
this.requestTimeout = options.requestTimeout ?? 6e4;
|
|
846
|
+
this.flowsApi = flowsApi;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Create a FlowClient from an existing RaisinHttpClient.
|
|
850
|
+
*
|
|
851
|
+
* @param httpClient - An authenticated RaisinHttpClient instance
|
|
852
|
+
* @param baseUrl - Base HTTP URL (same as the one used for the HTTP client)
|
|
853
|
+
* @param repository - Repository name
|
|
854
|
+
* @param options - Additional options
|
|
855
|
+
*/
|
|
856
|
+
static fromHttpClient(httpClient, baseUrl, repository, options = {}, flowsApi) {
|
|
857
|
+
return new _FlowClient(
|
|
858
|
+
baseUrl,
|
|
859
|
+
repository,
|
|
860
|
+
httpClient.getAuthManager(),
|
|
861
|
+
options,
|
|
862
|
+
flowsApi
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
// ==========================================================================
|
|
866
|
+
// Flow Execution
|
|
867
|
+
// ==========================================================================
|
|
868
|
+
/**
|
|
869
|
+
* Execute a flow by path.
|
|
870
|
+
*
|
|
871
|
+
* Creates a FlowInstance and queues it for execution. Returns immediately
|
|
872
|
+
* with the instance ID - use `streamEvents()` to monitor progress.
|
|
873
|
+
*
|
|
874
|
+
* @param flowPath - Path to the raisin:Flow node (e.g., "/flows/my-flow")
|
|
875
|
+
* @param input - Input data passed to the flow
|
|
876
|
+
* @returns Flow run response with instance_id and job_id
|
|
877
|
+
*
|
|
878
|
+
* @example
|
|
879
|
+
* ```typescript
|
|
880
|
+
* const result = await flows.run('/flows/process-order', {
|
|
881
|
+
* orderId: 'ord-123',
|
|
882
|
+
* items: [{ sku: 'ABC', qty: 2 }],
|
|
883
|
+
* });
|
|
884
|
+
* console.log('Instance:', result.instance_id);
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
async run(flowPath, input = {}, options) {
|
|
888
|
+
if (this.flowsApi) {
|
|
889
|
+
return this.flowsApi.run(flowPath, input);
|
|
890
|
+
}
|
|
891
|
+
const response = await this.request({
|
|
892
|
+
method: "POST",
|
|
893
|
+
path: `/api/flows/${this.repository}/run`,
|
|
894
|
+
body: { flow_path: flowPath, input },
|
|
895
|
+
signal: options?.signal
|
|
896
|
+
});
|
|
897
|
+
return response;
|
|
898
|
+
}
|
|
899
|
+
// ==========================================================================
|
|
900
|
+
// Instance Status
|
|
901
|
+
// ==========================================================================
|
|
902
|
+
/**
|
|
903
|
+
* Get the current status of a flow instance.
|
|
904
|
+
*
|
|
905
|
+
* Returns the instance status, variables, and metadata. Useful for
|
|
906
|
+
* polling-based approaches where SSE is not reliable (e.g., initial
|
|
907
|
+
* flow execution where events may be missed).
|
|
908
|
+
*
|
|
909
|
+
* @param instanceId - Flow instance ID (from `run()`)
|
|
910
|
+
* @returns Instance status including variables and flow path
|
|
911
|
+
*
|
|
912
|
+
* @example
|
|
913
|
+
* ```typescript
|
|
914
|
+
* const status = await flows.getInstanceStatus('instance-123');
|
|
915
|
+
* console.log('Status:', status.status); // 'waiting', 'completed', etc.
|
|
916
|
+
* ```
|
|
917
|
+
*/
|
|
918
|
+
async getInstanceStatus(instanceId, options) {
|
|
919
|
+
if (this.flowsApi) {
|
|
920
|
+
return this.flowsApi.getInstanceStatus(instanceId);
|
|
921
|
+
}
|
|
922
|
+
return this.request({
|
|
923
|
+
method: "GET",
|
|
924
|
+
path: `/api/flows/${this.repository}/instances/${instanceId}`,
|
|
925
|
+
signal: options?.signal
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
// ==========================================================================
|
|
929
|
+
// Event Streaming (SSE)
|
|
930
|
+
// ==========================================================================
|
|
931
|
+
/**
|
|
932
|
+
* Stream real-time execution events for a flow instance via SSE.
|
|
933
|
+
*
|
|
934
|
+
* Returns an async iterable that yields FlowExecutionEvent objects.
|
|
935
|
+
* The stream automatically closes when a terminal event is received
|
|
936
|
+
* (flow_completed or flow_failed).
|
|
937
|
+
*
|
|
938
|
+
* @param instanceId - Flow instance ID (from `run()`)
|
|
939
|
+
* @returns Async iterable of flow execution events
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```typescript
|
|
943
|
+
* const { instance_id } = await flows.run('/flows/my-flow', { key: 'value' });
|
|
944
|
+
*
|
|
945
|
+
* for await (const event of flows.streamEvents(instance_id)) {
|
|
946
|
+
* switch (event.type) {
|
|
947
|
+
* case 'step_started':
|
|
948
|
+
* console.log(`Step ${event.node_id} started`);
|
|
949
|
+
* break;
|
|
950
|
+
* case 'step_completed':
|
|
951
|
+
* console.log(`Step ${event.node_id} completed in ${event.duration_ms}ms`);
|
|
952
|
+
* break;
|
|
953
|
+
* case 'text_chunk':
|
|
954
|
+
* process.stdout.write(event.text);
|
|
955
|
+
* break;
|
|
956
|
+
* case 'flow_completed':
|
|
957
|
+
* console.log('Flow done:', event.output);
|
|
958
|
+
* break;
|
|
959
|
+
* case 'flow_failed':
|
|
960
|
+
* console.error('Flow failed:', event.error);
|
|
961
|
+
* break;
|
|
962
|
+
* }
|
|
963
|
+
* }
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
async *streamEvents(instanceId, options) {
|
|
967
|
+
if (this.flowsApi) {
|
|
968
|
+
const sub = await this.flowsApi.subscribeEvents(instanceId);
|
|
969
|
+
try {
|
|
970
|
+
for await (const event of sub.events) {
|
|
971
|
+
if (options?.signal?.aborted) throw new RaisinAbortError();
|
|
972
|
+
yield event;
|
|
973
|
+
if (event.type === "flow_completed" || event.type === "flow_failed") {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} finally {
|
|
978
|
+
await sub.unsubscribe();
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const url = `${this.baseUrl}/api/flows/${this.repository}/instances/${instanceId}/events`;
|
|
983
|
+
const headers = {};
|
|
984
|
+
const token = this.authManager.getAccessToken();
|
|
985
|
+
if (token) {
|
|
986
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
987
|
+
}
|
|
988
|
+
const sse = new SSEClient(url, {
|
|
989
|
+
headers,
|
|
990
|
+
eventTypes: ["flow-event", "message"],
|
|
991
|
+
reconnect: { enabled: false },
|
|
992
|
+
fetch: this.fetchImpl,
|
|
993
|
+
signal: options?.signal
|
|
994
|
+
});
|
|
995
|
+
try {
|
|
996
|
+
for await (const sseEvent of sse) {
|
|
997
|
+
const event = sseEvent.data;
|
|
998
|
+
yield event;
|
|
999
|
+
if (event.type === "flow_completed" || event.type === "flow_failed") {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
} finally {
|
|
1004
|
+
sse.close();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Create an eagerly-connected event stream for a flow instance.
|
|
1009
|
+
*
|
|
1010
|
+
* Unlike `streamEvents()` (a lazy async generator), this method opens the
|
|
1011
|
+
* SSE connection immediately and waits until it is established before
|
|
1012
|
+
* returning. This is critical for resume flows: you must subscribe to
|
|
1013
|
+
* events BEFORE calling `resume()`, otherwise events emitted between
|
|
1014
|
+
* the resume and the SSE connection are lost.
|
|
1015
|
+
*
|
|
1016
|
+
* @param instanceId - Flow instance ID
|
|
1017
|
+
* @returns Object with an async iterable `events` and a `close()` method
|
|
1018
|
+
*/
|
|
1019
|
+
async createEventStream(instanceId, options) {
|
|
1020
|
+
if (this.flowsApi) {
|
|
1021
|
+
const sub = await this.flowsApi.subscribeEvents(instanceId);
|
|
1022
|
+
return {
|
|
1023
|
+
events: sub.events,
|
|
1024
|
+
close: () => {
|
|
1025
|
+
sub.unsubscribe();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const url = `${this.baseUrl}/api/flows/${this.repository}/instances/${instanceId}/events`;
|
|
1030
|
+
const headers = {};
|
|
1031
|
+
const token = this.authManager.getAccessToken();
|
|
1032
|
+
if (token) {
|
|
1033
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
1034
|
+
}
|
|
1035
|
+
const sse = new SSEClient(url, {
|
|
1036
|
+
headers,
|
|
1037
|
+
eventTypes: ["flow-event", "message"],
|
|
1038
|
+
reconnect: { enabled: false },
|
|
1039
|
+
fetch: this.fetchImpl,
|
|
1040
|
+
signal: options?.signal
|
|
1041
|
+
});
|
|
1042
|
+
const iterator = sse[Symbol.asyncIterator]();
|
|
1043
|
+
let pendingFirst = iterator.next();
|
|
1044
|
+
await sse.waitUntilConnected();
|
|
1045
|
+
const events = {
|
|
1046
|
+
[Symbol.asyncIterator]() {
|
|
1047
|
+
return {
|
|
1048
|
+
async next() {
|
|
1049
|
+
let result;
|
|
1050
|
+
if (pendingFirst) {
|
|
1051
|
+
result = await pendingFirst;
|
|
1052
|
+
pendingFirst = null;
|
|
1053
|
+
} else {
|
|
1054
|
+
result = await iterator.next();
|
|
1055
|
+
}
|
|
1056
|
+
if (result.done) return { done: true, value: void 0 };
|
|
1057
|
+
const event = result.value.data;
|
|
1058
|
+
return { done: false, value: event };
|
|
1059
|
+
},
|
|
1060
|
+
async return() {
|
|
1061
|
+
await iterator.return?.();
|
|
1062
|
+
sse.close();
|
|
1063
|
+
return { done: true, value: void 0 };
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
return { events, close: () => sse.close() };
|
|
1069
|
+
}
|
|
1070
|
+
// ==========================================================================
|
|
1071
|
+
// Flow Resume & Human Task
|
|
1072
|
+
// ==========================================================================
|
|
1073
|
+
/**
|
|
1074
|
+
* Resume a flow instance that is in a waiting state.
|
|
1075
|
+
*
|
|
1076
|
+
* Use this when a flow is waiting for external input (tool results,
|
|
1077
|
+
* external events, etc.). The resume data is passed to the waiting step.
|
|
1078
|
+
*
|
|
1079
|
+
* @param instanceId - Flow instance ID
|
|
1080
|
+
* @param data - Resume data to pass to the waiting step
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* ```typescript
|
|
1084
|
+
* await flows.resume('instance-123', {
|
|
1085
|
+
* tool_name: 'get_weather',
|
|
1086
|
+
* result: { temperature: 72, conditions: 'sunny' },
|
|
1087
|
+
* });
|
|
1088
|
+
* ```
|
|
1089
|
+
*/
|
|
1090
|
+
async resume(instanceId, data, options) {
|
|
1091
|
+
if (this.flowsApi) {
|
|
1092
|
+
await this.flowsApi.resume(instanceId, data);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
await this.request({
|
|
1096
|
+
method: "POST",
|
|
1097
|
+
path: `/api/flows/${this.repository}/instances/${instanceId}/resume`,
|
|
1098
|
+
body: { resume_data: data },
|
|
1099
|
+
signal: options?.signal
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Respond to a human task within a flow.
|
|
1104
|
+
*
|
|
1105
|
+
* When a flow is waiting on a human_task step (approval, input, review),
|
|
1106
|
+
* use this method to submit the response. Completion is validated
|
|
1107
|
+
* (caller must be the task's assignee or an admin) and resumes the flow.
|
|
1108
|
+
*
|
|
1109
|
+
* Note: prefer `InboxApi` for full inbox functionality (listing tasks,
|
|
1110
|
+
* task details); this is a convenience passthrough to the same endpoint.
|
|
1111
|
+
*
|
|
1112
|
+
* @param instanceId - Flow instance ID (unused; kept for API stability)
|
|
1113
|
+
* @param taskId - The inbox task node ID (or path)
|
|
1114
|
+
* @param response - The response data
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```typescript
|
|
1118
|
+
* // Approve a task
|
|
1119
|
+
* await flows.respondToHumanTask('instance-123', 'task-abc', {
|
|
1120
|
+
* action: 'approve',
|
|
1121
|
+
* comment: 'Looks good!',
|
|
1122
|
+
* });
|
|
1123
|
+
* ```
|
|
1124
|
+
*/
|
|
1125
|
+
async respondToHumanTask(_instanceId, taskId, response, options) {
|
|
1126
|
+
await this.request({
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
path: `/api/inbox/${this.repository}/tasks/${encodeURIComponent(taskId)}/complete`,
|
|
1129
|
+
body: { response },
|
|
1130
|
+
signal: options?.signal
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
// ==========================================================================
|
|
1134
|
+
// Convenience Methods
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
/**
|
|
1137
|
+
* Run a flow and wait for the final result.
|
|
1138
|
+
*
|
|
1139
|
+
* Starts the flow, streams all events, and returns the terminal outcome.
|
|
1140
|
+
* This is a one-shot helper for fire-and-forget flows where you only
|
|
1141
|
+
* care about the final output.
|
|
1142
|
+
*
|
|
1143
|
+
* @param flowPath - Path to the raisin:Flow node
|
|
1144
|
+
* @param input - Input data passed to the flow
|
|
1145
|
+
* @param options - Optional abort signal
|
|
1146
|
+
* @returns The flow outcome including output or error
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* ```typescript
|
|
1150
|
+
* const result = await flows.runAndWait('/flows/process-order', { orderId: '123' });
|
|
1151
|
+
* if (result.status === 'completed') {
|
|
1152
|
+
* console.log('Output:', result.output);
|
|
1153
|
+
* }
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
async runAndWait(flowPath, input = {}, options) {
|
|
1157
|
+
const { instance_id } = await this.run(flowPath, input, options);
|
|
1158
|
+
for await (const event of this.streamEvents(instance_id, options)) {
|
|
1159
|
+
if (event.type === "flow_completed") {
|
|
1160
|
+
return {
|
|
1161
|
+
instanceId: instance_id,
|
|
1162
|
+
status: "completed",
|
|
1163
|
+
output: event.output
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (event.type === "flow_failed") {
|
|
1167
|
+
return {
|
|
1168
|
+
instanceId: instance_id,
|
|
1169
|
+
status: "failed",
|
|
1170
|
+
error: event.error
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return { instanceId: instance_id, status: "failed", error: "Stream ended without terminal event" };
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Run a flow and collect all events into an array.
|
|
1178
|
+
*
|
|
1179
|
+
* Starts the flow, streams all events, and returns them as a collected array.
|
|
1180
|
+
* Useful for testing, logging, or post-processing all flow events.
|
|
1181
|
+
*
|
|
1182
|
+
* @param flowPath - Path to the raisin:Flow node
|
|
1183
|
+
* @param input - Input data passed to the flow
|
|
1184
|
+
* @param options - Optional abort signal
|
|
1185
|
+
* @returns The instance ID and all collected events
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```typescript
|
|
1189
|
+
* const { events } = await flows.runAndCollect('/flows/etl-pipeline', { source: 'db' });
|
|
1190
|
+
* const steps = events.filter(e => e.type === 'step_completed');
|
|
1191
|
+
* console.log(`Completed ${steps.length} steps`);
|
|
1192
|
+
* ```
|
|
1193
|
+
*/
|
|
1194
|
+
async runAndCollect(flowPath, input = {}, options) {
|
|
1195
|
+
const { instance_id } = await this.run(flowPath, input, options);
|
|
1196
|
+
const events = [];
|
|
1197
|
+
for await (const event of this.streamEvents(instance_id, options)) {
|
|
1198
|
+
events.push(event);
|
|
1199
|
+
}
|
|
1200
|
+
return { instanceId: instance_id, events };
|
|
1201
|
+
}
|
|
1202
|
+
// ==========================================================================
|
|
1203
|
+
// Internal HTTP request helper
|
|
1204
|
+
// ==========================================================================
|
|
1205
|
+
async request(options) {
|
|
1206
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
1207
|
+
const headers = {
|
|
1208
|
+
"Content-Type": "application/json"
|
|
1209
|
+
};
|
|
1210
|
+
const token = this.authManager.getAccessToken();
|
|
1211
|
+
if (token) {
|
|
1212
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
1213
|
+
}
|
|
1214
|
+
const controller = new AbortController();
|
|
1215
|
+
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
1216
|
+
if (options.signal) {
|
|
1217
|
+
if (options.signal.aborted) {
|
|
1218
|
+
clearTimeout(timeoutId);
|
|
1219
|
+
throw new RaisinAbortError();
|
|
1220
|
+
}
|
|
1221
|
+
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
const response = await this.fetchImpl(url, {
|
|
1225
|
+
method: options.method,
|
|
1226
|
+
headers,
|
|
1227
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
1228
|
+
signal: controller.signal
|
|
1229
|
+
});
|
|
1230
|
+
clearTimeout(timeoutId);
|
|
1231
|
+
if (!response.ok) {
|
|
1232
|
+
const errorText = await response.text();
|
|
1233
|
+
let errorMessage;
|
|
1234
|
+
try {
|
|
1235
|
+
const errorJson = JSON.parse(errorText);
|
|
1236
|
+
errorMessage = errorJson.message || errorJson.error || errorText;
|
|
1237
|
+
} catch {
|
|
1238
|
+
errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
|
|
1239
|
+
}
|
|
1240
|
+
throw classifyHttpError(response.status, errorMessage);
|
|
1241
|
+
}
|
|
1242
|
+
const contentType = response.headers.get("content-type");
|
|
1243
|
+
if (contentType?.includes("application/json")) {
|
|
1244
|
+
return await response.json();
|
|
1245
|
+
}
|
|
1246
|
+
return void 0;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
clearTimeout(timeoutId);
|
|
1249
|
+
if (error instanceof RaisinAbortError || error instanceof RaisinTimeoutError) {
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1253
|
+
if (options.signal?.aborted) {
|
|
1254
|
+
throw new RaisinAbortError();
|
|
1255
|
+
}
|
|
1256
|
+
throw new RaisinTimeoutError(
|
|
1257
|
+
`Request timeout after ${this.requestTimeout}ms`,
|
|
1258
|
+
"REQUEST_TIMEOUT",
|
|
1259
|
+
this.requestTimeout
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// src/integrations/svelte/svelte-flow.ts
|
|
1268
|
+
function createFlowAdapter(options) {
|
|
1269
|
+
let snapshot = {
|
|
1270
|
+
events: [],
|
|
1271
|
+
status: "idle",
|
|
1272
|
+
isRunning: false,
|
|
1273
|
+
error: null,
|
|
1274
|
+
output: null,
|
|
1275
|
+
instanceId: null
|
|
1276
|
+
};
|
|
1277
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1278
|
+
let aborted = false;
|
|
1279
|
+
function emit() {
|
|
1280
|
+
for (const cb of listeners) cb(snapshot);
|
|
1281
|
+
}
|
|
1282
|
+
function update(partial) {
|
|
1283
|
+
snapshot = { ...snapshot, ...partial };
|
|
1284
|
+
if ("status" in partial) {
|
|
1285
|
+
snapshot.isRunning = snapshot.status === "running";
|
|
1286
|
+
}
|
|
1287
|
+
emit();
|
|
1288
|
+
}
|
|
1289
|
+
let flowClient = null;
|
|
1290
|
+
if (options.database) {
|
|
1291
|
+
flowClient = options.database.flow;
|
|
1292
|
+
} else if (options.baseUrl && options.repository && options.authManager) {
|
|
1293
|
+
flowClient = new FlowClient(
|
|
1294
|
+
options.baseUrl,
|
|
1295
|
+
options.repository,
|
|
1296
|
+
options.authManager,
|
|
1297
|
+
options.clientOptions,
|
|
1298
|
+
options.flowsApi
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
subscribe(cb) {
|
|
1303
|
+
listeners.add(cb);
|
|
1304
|
+
return () => {
|
|
1305
|
+
listeners.delete(cb);
|
|
1306
|
+
};
|
|
1307
|
+
},
|
|
1308
|
+
getSnapshot: () => snapshot,
|
|
1309
|
+
async run(flowPath, input = {}) {
|
|
1310
|
+
if (!flowClient) return;
|
|
1311
|
+
aborted = false;
|
|
1312
|
+
update({
|
|
1313
|
+
events: [],
|
|
1314
|
+
error: null,
|
|
1315
|
+
output: null,
|
|
1316
|
+
status: "running"
|
|
1317
|
+
});
|
|
1318
|
+
try {
|
|
1319
|
+
const result = await flowClient.run(flowPath, input);
|
|
1320
|
+
update({ instanceId: result.instance_id });
|
|
1321
|
+
for await (const event of flowClient.streamEvents(result.instance_id)) {
|
|
1322
|
+
if (aborted) break;
|
|
1323
|
+
update({ events: [...snapshot.events, event] });
|
|
1324
|
+
if (event.type === "flow_waiting") {
|
|
1325
|
+
update({ status: "waiting" });
|
|
1326
|
+
} else if (event.type === "flow_completed") {
|
|
1327
|
+
update({ output: event.output, status: "completed" });
|
|
1328
|
+
} else if (event.type === "flow_failed") {
|
|
1329
|
+
update({ error: event.error, status: "failed" });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
update({
|
|
1334
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1335
|
+
status: "failed"
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
async resume(data) {
|
|
1340
|
+
if (!flowClient || !snapshot.instanceId) return;
|
|
1341
|
+
update({ status: "running", error: null });
|
|
1342
|
+
try {
|
|
1343
|
+
const stream = await flowClient.createEventStream(snapshot.instanceId);
|
|
1344
|
+
await flowClient.resume(snapshot.instanceId, data);
|
|
1345
|
+
for await (const event of stream.events) {
|
|
1346
|
+
if (aborted) break;
|
|
1347
|
+
update({ events: [...snapshot.events, event] });
|
|
1348
|
+
if (event.type === "flow_waiting") {
|
|
1349
|
+
update({ status: "waiting" });
|
|
1350
|
+
stream.close();
|
|
1351
|
+
return;
|
|
1352
|
+
} else if (event.type === "flow_completed") {
|
|
1353
|
+
update({ output: event.output, status: "completed" });
|
|
1354
|
+
stream.close();
|
|
1355
|
+
return;
|
|
1356
|
+
} else if (event.type === "flow_failed") {
|
|
1357
|
+
update({ error: event.error, status: "failed" });
|
|
1358
|
+
stream.close();
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
stream.close();
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
update({
|
|
1365
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1366
|
+
status: "failed"
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
reset() {
|
|
1371
|
+
aborted = true;
|
|
1372
|
+
update({
|
|
1373
|
+
events: [],
|
|
1374
|
+
status: "idle",
|
|
1375
|
+
error: null,
|
|
1376
|
+
output: null,
|
|
1377
|
+
instanceId: null
|
|
1378
|
+
});
|
|
1379
|
+
},
|
|
1380
|
+
destroy() {
|
|
1381
|
+
aborted = true;
|
|
1382
|
+
listeners.clear();
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/integrations/svelte/svelte-context.ts
|
|
1388
|
+
var RAISIN_CONTEXT_KEY = /* @__PURE__ */ Symbol("raisin");
|
|
1389
|
+
|
|
1390
|
+
// src/utils/plan-projection.ts
|
|
1391
|
+
function asRecord(value) {
|
|
1392
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1393
|
+
return value;
|
|
1394
|
+
}
|
|
1395
|
+
function asString(value) {
|
|
1396
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function parsePlanTasks(rawTasks) {
|
|
1399
|
+
if (!Array.isArray(rawTasks)) return [];
|
|
1400
|
+
return rawTasks.map((rawTask, index) => {
|
|
1401
|
+
if (typeof rawTask === "string") {
|
|
1402
|
+
return {
|
|
1403
|
+
title: rawTask,
|
|
1404
|
+
status: "pending"
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
const task = asRecord(rawTask);
|
|
1408
|
+
if (!task) return null;
|
|
1409
|
+
const delegation = asRecord(task.delegation);
|
|
1410
|
+
return {
|
|
1411
|
+
taskId: asString(task.task_id) ?? asString(task.id),
|
|
1412
|
+
title: asString(task.title) ?? `Task ${index + 1}`,
|
|
1413
|
+
status: asString(task.status) ?? "pending",
|
|
1414
|
+
description: asString(task.description),
|
|
1415
|
+
priority: asString(task.priority),
|
|
1416
|
+
delegation: delegation ? {
|
|
1417
|
+
id: asString(delegation.id),
|
|
1418
|
+
status: asString(delegation.status) ?? "unknown",
|
|
1419
|
+
agentRef: asString(delegation.agent_ref) ?? asString(delegation.agentRef),
|
|
1420
|
+
flowInstanceId: asString(delegation.flow_instance_id) ?? asString(delegation.flowInstanceId),
|
|
1421
|
+
branch: asString(delegation.branch),
|
|
1422
|
+
baseBranch: asString(delegation.base_branch) ?? asString(delegation.baseBranch),
|
|
1423
|
+
result: delegation.result,
|
|
1424
|
+
error: asString(delegation.error)
|
|
1425
|
+
} : void 0
|
|
1426
|
+
};
|
|
1427
|
+
}).filter((task) => task !== null);
|
|
1428
|
+
}
|
|
1429
|
+
var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled"]);
|
|
1430
|
+
var TERMINAL_PLAN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
1431
|
+
function setPlanStatus(plan, nextStatus) {
|
|
1432
|
+
if (!nextStatus) return;
|
|
1433
|
+
if (TERMINAL_PLAN_STATUSES.has(plan.status) && !TERMINAL_PLAN_STATUSES.has(nextStatus)) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
plan.status = nextStatus;
|
|
1437
|
+
}
|
|
1438
|
+
function upsertTask(plan, nextTask) {
|
|
1439
|
+
const existingIndex = plan.tasks.findIndex(
|
|
1440
|
+
(task) => nextTask.taskId && task.taskId === nextTask.taskId || !nextTask.taskId && task.title === nextTask.title
|
|
1441
|
+
);
|
|
1442
|
+
if (existingIndex === -1) {
|
|
1443
|
+
plan.tasks.push(nextTask);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const existing = plan.tasks[existingIndex];
|
|
1447
|
+
if (TERMINAL_TASK_STATUSES.has(existing.status) && !TERMINAL_TASK_STATUSES.has(nextTask.status)) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
plan.tasks[existingIndex] = {
|
|
1451
|
+
...existing,
|
|
1452
|
+
...nextTask
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function ensurePlan(plans, key, fallbackTitle, timestamp) {
|
|
1456
|
+
const existing = plans.get(key);
|
|
1457
|
+
if (existing) {
|
|
1458
|
+
if (timestamp) existing.updatedAt = timestamp;
|
|
1459
|
+
return existing;
|
|
1460
|
+
}
|
|
1461
|
+
const created = {
|
|
1462
|
+
key,
|
|
1463
|
+
title: fallbackTitle,
|
|
1464
|
+
status: "pending_approval",
|
|
1465
|
+
requiresApproval: true,
|
|
1466
|
+
tasks: [],
|
|
1467
|
+
updatedAt: timestamp
|
|
1468
|
+
};
|
|
1469
|
+
plans.set(key, created);
|
|
1470
|
+
return created;
|
|
1471
|
+
}
|
|
1472
|
+
function projectPlansFromMessages(messages) {
|
|
1473
|
+
const ordered = [...messages].sort((a, b) => {
|
|
1474
|
+
const aTime = Date.parse(a.timestamp || "");
|
|
1475
|
+
const bTime = Date.parse(b.timestamp || "");
|
|
1476
|
+
return (Number.isNaN(aTime) ? 0 : aTime) - (Number.isNaN(bTime) ? 0 : bTime);
|
|
1477
|
+
});
|
|
1478
|
+
const plans = /* @__PURE__ */ new Map();
|
|
1479
|
+
for (const message of ordered) {
|
|
1480
|
+
const timestamp = message.timestamp;
|
|
1481
|
+
const data = asRecord(message.data);
|
|
1482
|
+
if (message.messageType === "ai_plan" && data) {
|
|
1483
|
+
const planPath = asString(data.plan_path);
|
|
1484
|
+
const planId = asString(data.plan_id);
|
|
1485
|
+
const key = planPath || planId || message.path || message.id || `plan:${timestamp}`;
|
|
1486
|
+
const plan = ensurePlan(
|
|
1487
|
+
plans,
|
|
1488
|
+
key,
|
|
1489
|
+
asString(data.title) || message.content || "Plan",
|
|
1490
|
+
timestamp
|
|
1491
|
+
);
|
|
1492
|
+
plan.planPath = planPath ?? plan.planPath;
|
|
1493
|
+
plan.planId = planId ?? plan.planId;
|
|
1494
|
+
plan.title = asString(data.title) || plan.title;
|
|
1495
|
+
setPlanStatus(plan, asString(data.status));
|
|
1496
|
+
plan.requiresApproval = Boolean(data.requires_approval) || plan.status === "pending_approval";
|
|
1497
|
+
plan.sourceMessagePath = message.path ?? plan.sourceMessagePath;
|
|
1498
|
+
const parsedTasks = parsePlanTasks(data.tasks);
|
|
1499
|
+
if (parsedTasks.length > 0) {
|
|
1500
|
+
plan.tasks = parsedTasks;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (message.messageType === "ai_task_update" && data) {
|
|
1504
|
+
const planPath = asString(data.plan_path);
|
|
1505
|
+
const planId = asString(data.plan_id);
|
|
1506
|
+
const key = planPath || planId;
|
|
1507
|
+
if (!key) continue;
|
|
1508
|
+
const plan = ensurePlan(plans, key, "Plan", timestamp);
|
|
1509
|
+
plan.planPath = planPath ?? plan.planPath;
|
|
1510
|
+
plan.planId = planId ?? plan.planId;
|
|
1511
|
+
setPlanStatus(plan, asString(data.plan_status));
|
|
1512
|
+
plan.requiresApproval = plan.status === "pending_approval";
|
|
1513
|
+
const taskUpdate = {
|
|
1514
|
+
taskId: asString(data.task_id),
|
|
1515
|
+
title: asString(data.task_title) || asString(data.title) || "Task",
|
|
1516
|
+
status: asString(data.status) || "pending"
|
|
1517
|
+
};
|
|
1518
|
+
upsertTask(plan, taskUpdate);
|
|
1519
|
+
}
|
|
1520
|
+
if (Array.isArray(message.children)) {
|
|
1521
|
+
for (const child of message.children) {
|
|
1522
|
+
if (child.type !== "plan") continue;
|
|
1523
|
+
const key = child.path || message.path || message.id || `plan-child:${child.id}:${timestamp}`;
|
|
1524
|
+
const plan = ensurePlan(plans, key, child.planTitle || message.content || "Plan", timestamp);
|
|
1525
|
+
plan.title = child.planTitle || plan.title;
|
|
1526
|
+
plan.status = child.status || plan.status;
|
|
1527
|
+
plan.requiresApproval = plan.status === "pending_approval";
|
|
1528
|
+
if (Array.isArray(child.tasks) && child.tasks.length > 0) {
|
|
1529
|
+
plan.tasks = child.tasks.map((task) => ({
|
|
1530
|
+
taskId: task.id,
|
|
1531
|
+
title: task.title,
|
|
1532
|
+
status: task.status,
|
|
1533
|
+
delegation: task.delegation ? {
|
|
1534
|
+
id: task.delegation.id,
|
|
1535
|
+
status: task.delegation.status,
|
|
1536
|
+
agentRef: task.delegation.agentRef,
|
|
1537
|
+
flowInstanceId: task.delegation.flowInstanceId,
|
|
1538
|
+
branch: task.delegation.branch,
|
|
1539
|
+
baseBranch: task.delegation.baseBranch,
|
|
1540
|
+
result: task.delegation.result,
|
|
1541
|
+
error: task.delegation.error
|
|
1542
|
+
} : void 0
|
|
1543
|
+
}));
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
for (const plan of plans.values()) {
|
|
1549
|
+
if (!TERMINAL_PLAN_STATUSES.has(plan.status) && plan.tasks.length > 0 && plan.tasks.every((task) => TERMINAL_TASK_STATUSES.has(task.status))) {
|
|
1550
|
+
plan.status = "completed";
|
|
1551
|
+
plan.requiresApproval = false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return [...plans.values()].sort((a, b) => {
|
|
1555
|
+
const aTime = Date.parse(a.updatedAt || "");
|
|
1556
|
+
const bTime = Date.parse(b.updatedAt || "");
|
|
1557
|
+
return (Number.isNaN(bTime) ? 0 : bTime) - (Number.isNaN(aTime) ? 0 : aTime);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/utils/chat-events.ts
|
|
1562
|
+
function isRecoveredDoneEvent(event) {
|
|
1563
|
+
if (event.type !== "done") return false;
|
|
1564
|
+
const done = event;
|
|
1565
|
+
if (done.recovered !== true) return false;
|
|
1566
|
+
return typeof done.content !== "string" || done.content.trim().length === 0;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/stores/conversation-store.ts
|
|
1570
|
+
var ConversationStore = class {
|
|
1571
|
+
constructor(options) {
|
|
1572
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
1573
|
+
// Persistent SSE subscription
|
|
1574
|
+
this.subscription = null;
|
|
1575
|
+
this._messages = [];
|
|
1576
|
+
this._isStreaming = false;
|
|
1577
|
+
this._isWaiting = false;
|
|
1578
|
+
this._streamingText = "";
|
|
1579
|
+
this._error = null;
|
|
1580
|
+
this._activeToolCalls = [];
|
|
1581
|
+
this._isLoading = false;
|
|
1582
|
+
this._conversationType = null;
|
|
1583
|
+
this._streamingTimer = null;
|
|
1584
|
+
this._watchdogTimer = null;
|
|
1585
|
+
this.manager = options.database.conversations;
|
|
1586
|
+
this._conversationPath = options.conversationPath ?? null;
|
|
1587
|
+
this.createOptions = options.createOptions;
|
|
1588
|
+
this.onEvent = options.onEvent;
|
|
1589
|
+
this._streamingTimeoutMs = options.streamingTimeoutMs ?? 12e4;
|
|
1590
|
+
this._watchdogIntervalMs = options.watchdogIntervalMs ?? 3e4;
|
|
1591
|
+
if (this._conversationPath) {
|
|
1592
|
+
this.ensureSubscription();
|
|
1593
|
+
} else if (this.createOptions?.anonymous) {
|
|
1594
|
+
void this.resumeAnonymous();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
anonymousOptions() {
|
|
1598
|
+
const a = this.createOptions?.anonymous;
|
|
1599
|
+
if (!a) return null;
|
|
1600
|
+
return a === true ? {} : a;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Resume the visitor conversation this tab already has (the session secret
|
|
1604
|
+
* and conversation id live in sessionStorage), and load its history. A tab
|
|
1605
|
+
* without one starts nothing until the first message.
|
|
1606
|
+
*/
|
|
1607
|
+
async resumeAnonymous() {
|
|
1608
|
+
const opts = this.anonymousOptions();
|
|
1609
|
+
if (!opts || !this.createOptions || this._conversationPath) return;
|
|
1610
|
+
const agent = this.createOptions.participant;
|
|
1611
|
+
try {
|
|
1612
|
+
if (!this.manager.hasStoredAnonymous(agent, opts)) return;
|
|
1613
|
+
const convo = await this.manager.startAnonymous(agent, opts);
|
|
1614
|
+
if (this._conversationPath) return;
|
|
1615
|
+
this._conversationPath = convo.conversationPath;
|
|
1616
|
+
this._conversationType = convo.type;
|
|
1617
|
+
this.ensureSubscription();
|
|
1618
|
+
await this.loadMessages();
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
logger.debug("[ConversationStore] No visitor conversation to resume", err);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
// ==========================================================================
|
|
1624
|
+
// Subscription
|
|
1625
|
+
// ==========================================================================
|
|
1626
|
+
/**
|
|
1627
|
+
* Subscribe to state changes.
|
|
1628
|
+
* Returns an unsubscribe function.
|
|
1629
|
+
*/
|
|
1630
|
+
subscribe(callback) {
|
|
1631
|
+
this.subscribers.add(callback);
|
|
1632
|
+
callback(this.getSnapshot());
|
|
1633
|
+
return () => {
|
|
1634
|
+
this.subscribers.delete(callback);
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Get the current state snapshot.
|
|
1639
|
+
*/
|
|
1640
|
+
getSnapshot() {
|
|
1641
|
+
return {
|
|
1642
|
+
conversation: this._conversationPath ? { conversationPath: this._conversationPath, type: this._conversationType || "ai_chat" } : null,
|
|
1643
|
+
messages: [...this._messages],
|
|
1644
|
+
isStreaming: this._isStreaming,
|
|
1645
|
+
isWaiting: this._isWaiting,
|
|
1646
|
+
streamingText: this._streamingText,
|
|
1647
|
+
error: this._error,
|
|
1648
|
+
activeToolCalls: [...this._activeToolCalls],
|
|
1649
|
+
plans: projectPlansFromMessages(this._messages),
|
|
1650
|
+
isLoading: this._isLoading,
|
|
1651
|
+
conversationPath: this._conversationPath
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
// ==========================================================================
|
|
1655
|
+
// Actions
|
|
1656
|
+
// ==========================================================================
|
|
1657
|
+
/**
|
|
1658
|
+
* Send a user message and stream the AI response.
|
|
1659
|
+
*
|
|
1660
|
+
* If no conversation exists yet and `createOptions` was provided,
|
|
1661
|
+
* the conversation is created automatically on the first call.
|
|
1662
|
+
*/
|
|
1663
|
+
async sendMessage(content) {
|
|
1664
|
+
if (this._isStreaming) {
|
|
1665
|
+
logger.warn("[ConversationStore] Cannot send message while streaming");
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
logger.info("[ConversationStore] Sending message", { path: this._conversationPath });
|
|
1669
|
+
this._error = null;
|
|
1670
|
+
if (!this._conversationPath) {
|
|
1671
|
+
if (!this.createOptions) {
|
|
1672
|
+
throw new Error("No conversationPath and no createOptions provided");
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const anonymous = this.anonymousOptions();
|
|
1676
|
+
const convo = anonymous ? await this.manager.startAnonymous(this.createOptions.participant, anonymous) : await this.manager.create({
|
|
1677
|
+
participant: this.createOptions.participant,
|
|
1678
|
+
input: this.createOptions.input
|
|
1679
|
+
});
|
|
1680
|
+
this._conversationPath = convo.conversationPath;
|
|
1681
|
+
this._conversationType = convo.type;
|
|
1682
|
+
for (const event of convo.initialEvents ?? []) {
|
|
1683
|
+
this.onEvent?.(event);
|
|
1684
|
+
this.handleEvent(event);
|
|
1685
|
+
}
|
|
1686
|
+
this.notify();
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
1689
|
+
this.notify();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const userMessage = {
|
|
1694
|
+
role: "user",
|
|
1695
|
+
content,
|
|
1696
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1697
|
+
};
|
|
1698
|
+
this._messages.push(userMessage);
|
|
1699
|
+
this._isStreaming = true;
|
|
1700
|
+
this._isWaiting = false;
|
|
1701
|
+
this._streamingText = "";
|
|
1702
|
+
this._activeToolCalls = [];
|
|
1703
|
+
this.resetStreamingTimer();
|
|
1704
|
+
this.startWatchdog();
|
|
1705
|
+
this.notify();
|
|
1706
|
+
try {
|
|
1707
|
+
this.ensureSubscription();
|
|
1708
|
+
await this.subscription.waitUntilConnected();
|
|
1709
|
+
await this.manager.createUserMessage(this._conversationPath, content);
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
logger.error("[ConversationStore] Send failed", err);
|
|
1712
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
1713
|
+
this._isStreaming = false;
|
|
1714
|
+
this.clearStreamingTimer();
|
|
1715
|
+
this.stopWatchdog();
|
|
1716
|
+
this.notify();
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Load messages from the persisted node tree.
|
|
1721
|
+
* Useful for loading history on page reload.
|
|
1722
|
+
*/
|
|
1723
|
+
async loadMessages() {
|
|
1724
|
+
if (!this._conversationPath) return [];
|
|
1725
|
+
this._isLoading = true;
|
|
1726
|
+
this.notify();
|
|
1727
|
+
try {
|
|
1728
|
+
const messages = await this.manager.getMessages(this._conversationPath);
|
|
1729
|
+
logger.debug(`[ConversationStore] Loaded ${messages.length} messages`);
|
|
1730
|
+
if (messages.length > 0) {
|
|
1731
|
+
this._messages = messages;
|
|
1732
|
+
this._activeToolCalls = [];
|
|
1733
|
+
}
|
|
1734
|
+
return messages;
|
|
1735
|
+
} finally {
|
|
1736
|
+
this._isLoading = false;
|
|
1737
|
+
this.notify();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Approve a pending plan.
|
|
1742
|
+
*/
|
|
1743
|
+
async approvePlan(planPath) {
|
|
1744
|
+
await this.manager.approvePlan(planPath);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Reject a pending plan.
|
|
1748
|
+
*/
|
|
1749
|
+
async rejectPlan(planPath, feedback) {
|
|
1750
|
+
await this.manager.rejectPlan(planPath, feedback);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Mark a single message as read.
|
|
1754
|
+
*/
|
|
1755
|
+
async markMessageAsRead(messagePath) {
|
|
1756
|
+
await this.manager.markMessageAsRead(messagePath);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Stop the current streaming response.
|
|
1760
|
+
*/
|
|
1761
|
+
stop() {
|
|
1762
|
+
this._isStreaming = false;
|
|
1763
|
+
this.clearStreamingTimer();
|
|
1764
|
+
this.stopWatchdog();
|
|
1765
|
+
this.notify();
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Clean up the store and release resources.
|
|
1769
|
+
*/
|
|
1770
|
+
destroy() {
|
|
1771
|
+
this.stop();
|
|
1772
|
+
this.clearStreamingTimer();
|
|
1773
|
+
this.stopWatchdog();
|
|
1774
|
+
this.subscription?.unsubscribe();
|
|
1775
|
+
this.subscription = null;
|
|
1776
|
+
this.subscribers.clear();
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Get the conversation path.
|
|
1780
|
+
*/
|
|
1781
|
+
getConversationPath() {
|
|
1782
|
+
return this._conversationPath;
|
|
1783
|
+
}
|
|
1784
|
+
// ==========================================================================
|
|
1785
|
+
// Internal
|
|
1786
|
+
// ==========================================================================
|
|
1787
|
+
ensureSubscription() {
|
|
1788
|
+
if (this.subscription) return;
|
|
1789
|
+
if (!this._conversationPath) return;
|
|
1790
|
+
this.subscription = this.manager.subscribe(
|
|
1791
|
+
this._conversationPath,
|
|
1792
|
+
(event) => {
|
|
1793
|
+
this.onEvent?.(event);
|
|
1794
|
+
this.handleEvent(event);
|
|
1795
|
+
}
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
handleEvent(event) {
|
|
1799
|
+
logger.debug("[ConversationStore] SSE:", event.type);
|
|
1800
|
+
if (this._isStreaming) {
|
|
1801
|
+
this.resetStreamingTimer();
|
|
1802
|
+
}
|
|
1803
|
+
switch (event.type) {
|
|
1804
|
+
case "text_chunk":
|
|
1805
|
+
this.markStreamingActivity();
|
|
1806
|
+
this._streamingText += event.text;
|
|
1807
|
+
this.notify();
|
|
1808
|
+
break;
|
|
1809
|
+
case "thought_chunk":
|
|
1810
|
+
this.markStreamingActivity();
|
|
1811
|
+
this.notify();
|
|
1812
|
+
break;
|
|
1813
|
+
case "assistant_message":
|
|
1814
|
+
this._messages.push(event.message);
|
|
1815
|
+
this._streamingText = "";
|
|
1816
|
+
this.notify();
|
|
1817
|
+
break;
|
|
1818
|
+
case "tool_call_started": {
|
|
1819
|
+
this.markStreamingActivity();
|
|
1820
|
+
logger.debug("[ConversationStore] Tool started:", event.functionName);
|
|
1821
|
+
const info = {
|
|
1822
|
+
id: event.toolCallId,
|
|
1823
|
+
functionName: event.functionName,
|
|
1824
|
+
arguments: event.arguments,
|
|
1825
|
+
status: "running"
|
|
1826
|
+
};
|
|
1827
|
+
this._activeToolCalls.push(info);
|
|
1828
|
+
this.notify();
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
case "tool_call_completed": {
|
|
1832
|
+
this.markStreamingActivity();
|
|
1833
|
+
logger.debug("[ConversationStore] Tool completed:", event.toolCallId);
|
|
1834
|
+
const idx = this._activeToolCalls.findIndex((tc) => tc.id === event.toolCallId);
|
|
1835
|
+
if (idx >= 0) {
|
|
1836
|
+
this._activeToolCalls[idx] = {
|
|
1837
|
+
...this._activeToolCalls[idx],
|
|
1838
|
+
status: event.error ? "failed" : "completed",
|
|
1839
|
+
result: event.result,
|
|
1840
|
+
durationMs: event.durationMs
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
this.notify();
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
case "waiting": {
|
|
1847
|
+
logger.info("[ConversationStore] Waiting for input");
|
|
1848
|
+
this.flushStreamingText(event.timestamp);
|
|
1849
|
+
this._isStreaming = false;
|
|
1850
|
+
this._isWaiting = true;
|
|
1851
|
+
this._activeToolCalls = [];
|
|
1852
|
+
this.clearStreamingTimer();
|
|
1853
|
+
this.stopWatchdog();
|
|
1854
|
+
this.notify();
|
|
1855
|
+
this.deferredReload();
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
case "done": {
|
|
1859
|
+
if (isRecoveredDoneEvent(event)) {
|
|
1860
|
+
logger.debug("[ConversationStore] Ignoring recovered done event");
|
|
1861
|
+
this.reconcileRecoveredDone();
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
const doneEvent = event;
|
|
1865
|
+
if (doneEvent.dispatchPhase && doneEvent.dispatchPhase !== "terminal") {
|
|
1866
|
+
logger.debug("[ConversationStore] Ignoring non-terminal done event");
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
logger.info("[ConversationStore] Turn done");
|
|
1870
|
+
this.flushStreamingText(event.timestamp);
|
|
1871
|
+
if (doneEvent.content && !this.lastMessageIsAssistant()) {
|
|
1872
|
+
this._messages.push({
|
|
1873
|
+
role: doneEvent.role || "assistant",
|
|
1874
|
+
content: doneEvent.content,
|
|
1875
|
+
timestamp: event.timestamp,
|
|
1876
|
+
senderDisplayName: doneEvent.senderDisplayName,
|
|
1877
|
+
finishReason: doneEvent.finishReason
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
this._isStreaming = false;
|
|
1881
|
+
this._isWaiting = true;
|
|
1882
|
+
this._activeToolCalls = [];
|
|
1883
|
+
this.clearStreamingTimer();
|
|
1884
|
+
this.stopWatchdog();
|
|
1885
|
+
this.notify();
|
|
1886
|
+
this.deferredReload();
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
case "completed": {
|
|
1890
|
+
this.flushStreamingText(event.timestamp);
|
|
1891
|
+
if (event.messages && event.messages.length > 0) {
|
|
1892
|
+
this._messages = event.messages;
|
|
1893
|
+
}
|
|
1894
|
+
this._isStreaming = false;
|
|
1895
|
+
this._isWaiting = false;
|
|
1896
|
+
this._activeToolCalls = [];
|
|
1897
|
+
this.clearStreamingTimer();
|
|
1898
|
+
this.stopWatchdog();
|
|
1899
|
+
this.notify();
|
|
1900
|
+
this.deferredReload();
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
case "failed":
|
|
1904
|
+
logger.error("[ConversationStore] Turn failed", event.error);
|
|
1905
|
+
this._error = event.error;
|
|
1906
|
+
this._isStreaming = false;
|
|
1907
|
+
this._isWaiting = false;
|
|
1908
|
+
this._streamingText = "";
|
|
1909
|
+
this._activeToolCalls = [];
|
|
1910
|
+
this.clearStreamingTimer();
|
|
1911
|
+
this.stopWatchdog();
|
|
1912
|
+
this.notify();
|
|
1913
|
+
break;
|
|
1914
|
+
case "conversation_created":
|
|
1915
|
+
this._conversationPath = event.conversationPath;
|
|
1916
|
+
this.notify();
|
|
1917
|
+
break;
|
|
1918
|
+
case "message_saved":
|
|
1919
|
+
this.silentReload().catch(() => {
|
|
1920
|
+
});
|
|
1921
|
+
this.notify();
|
|
1922
|
+
break;
|
|
1923
|
+
case "message_delivered":
|
|
1924
|
+
this._messages.push(event.message);
|
|
1925
|
+
this.notify();
|
|
1926
|
+
break;
|
|
1927
|
+
case "log": {
|
|
1928
|
+
const logEvent = event;
|
|
1929
|
+
const currentLevel = getLogLevel();
|
|
1930
|
+
const levelMap = {
|
|
1931
|
+
debug: 4 /* Debug */,
|
|
1932
|
+
info: 3 /* Info */,
|
|
1933
|
+
warn: 2 /* Warn */,
|
|
1934
|
+
error: 1 /* Error */
|
|
1935
|
+
};
|
|
1936
|
+
const eventLevel = levelMap[logEvent.level] ?? 4 /* Debug */;
|
|
1937
|
+
if (currentLevel >= eventLevel) {
|
|
1938
|
+
const fn = logEvent.level === "error" ? console.error : logEvent.level === "warn" ? console.warn : logEvent.level === "debug" ? console.debug : console.info;
|
|
1939
|
+
fn("[server]", logEvent.message);
|
|
1940
|
+
}
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Agent activity observed while the store thought the turn was over.
|
|
1947
|
+
*
|
|
1948
|
+
* This happens when execution continues WITHOUT a user message — plan
|
|
1949
|
+
* approval in approve_then_auto / step_by_step (approvePlan() returns an
|
|
1950
|
+
* enqueue receipt and the agent starts executing), or agent-initiated
|
|
1951
|
+
* follow-ups. The events arrive over the persistent live subscription
|
|
1952
|
+
* (no history replay), so activity is always "now": re-enter streaming
|
|
1953
|
+
* state so UIs show progress and the end-of-turn handling (done/waiting
|
|
1954
|
+
* → deferred reload) runs again for this continuation.
|
|
1955
|
+
*/
|
|
1956
|
+
markStreamingActivity() {
|
|
1957
|
+
if (this._isStreaming) return;
|
|
1958
|
+
logger.debug("[ConversationStore] Agent activity while idle \u2014 re-entering streaming state");
|
|
1959
|
+
this._isStreaming = true;
|
|
1960
|
+
this._isWaiting = false;
|
|
1961
|
+
this._streamingText = "";
|
|
1962
|
+
this.resetStreamingTimer();
|
|
1963
|
+
this.startWatchdog();
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* If accumulated streaming text hasn't been captured in an assistant_message,
|
|
1967
|
+
* synthesize one.
|
|
1968
|
+
*/
|
|
1969
|
+
flushStreamingText(timestamp) {
|
|
1970
|
+
if (this._streamingText && !this.lastMessageIsAssistant()) {
|
|
1971
|
+
this._messages.push({
|
|
1972
|
+
role: "assistant",
|
|
1973
|
+
content: this._streamingText,
|
|
1974
|
+
timestamp
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
this._streamingText = "";
|
|
1978
|
+
}
|
|
1979
|
+
lastMessageIsAssistant() {
|
|
1980
|
+
const last = this._messages[this._messages.length - 1];
|
|
1981
|
+
return last?.role === "assistant";
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* After a turn completes, reload canonical messages from the node tree
|
|
1985
|
+
* to catch any events missed during SSE gaps.
|
|
1986
|
+
*/
|
|
1987
|
+
deferredReload() {
|
|
1988
|
+
if (!this._conversationPath) return;
|
|
1989
|
+
logger.debug("[ConversationStore] Scheduling deferred reload");
|
|
1990
|
+
const attemptReload = (delay, retriesLeft) => {
|
|
1991
|
+
setTimeout(async () => {
|
|
1992
|
+
try {
|
|
1993
|
+
const prevCount = this._messages.length;
|
|
1994
|
+
await this.silentReload();
|
|
1995
|
+
if (this._messages.length === prevCount && retriesLeft > 0) {
|
|
1996
|
+
attemptReload(delay * 2, retriesLeft - 1);
|
|
1997
|
+
}
|
|
1998
|
+
} catch {
|
|
1999
|
+
}
|
|
2000
|
+
}, delay);
|
|
2001
|
+
};
|
|
2002
|
+
attemptReload(500, 2);
|
|
2003
|
+
}
|
|
2004
|
+
async silentReload() {
|
|
2005
|
+
if (!this._conversationPath) return;
|
|
2006
|
+
try {
|
|
2007
|
+
const fresh = await this.manager.getMessages(this._conversationPath);
|
|
2008
|
+
if (fresh.length === 0) return;
|
|
2009
|
+
let matchCount = 0;
|
|
2010
|
+
const limit = Math.min(this._messages.length, fresh.length);
|
|
2011
|
+
for (let i = 0; i < limit; i++) {
|
|
2012
|
+
if (this._messages[i].role !== fresh[i].role || this._messages[i].content !== fresh[i].content) break;
|
|
2013
|
+
matchCount++;
|
|
2014
|
+
}
|
|
2015
|
+
if (matchCount === this._messages.length && fresh.length === matchCount) {
|
|
2016
|
+
for (let i = 0; i < matchCount; i++) {
|
|
2017
|
+
const f = fresh[i];
|
|
2018
|
+
const e = this._messages[i];
|
|
2019
|
+
if (f.id) e.id = f.id;
|
|
2020
|
+
if (f.path) e.path = f.path;
|
|
2021
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
2022
|
+
if (f.data) e.data = f.data;
|
|
2023
|
+
if (f.children) e.children = f.children;
|
|
2024
|
+
if (f.finishReason) e.finishReason = f.finishReason;
|
|
2025
|
+
}
|
|
2026
|
+
await this.rebuildToolCallState();
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
if (matchCount === this._messages.length && fresh.length > matchCount) {
|
|
2030
|
+
for (let i = matchCount; i < fresh.length; i++) {
|
|
2031
|
+
this._messages.push(fresh[i]);
|
|
2032
|
+
}
|
|
2033
|
+
for (let i = 0; i < matchCount; i++) {
|
|
2034
|
+
const f = fresh[i];
|
|
2035
|
+
const e = this._messages[i];
|
|
2036
|
+
if (f.id) e.id = f.id;
|
|
2037
|
+
if (f.path) e.path = f.path;
|
|
2038
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
2039
|
+
if (f.data) e.data = f.data;
|
|
2040
|
+
if (f.children) e.children = f.children;
|
|
2041
|
+
}
|
|
2042
|
+
await this.rebuildToolCallState();
|
|
2043
|
+
this.notify();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (matchCount === fresh.length && fresh.length < this._messages.length) {
|
|
2047
|
+
for (let i = 0; i < matchCount; i++) {
|
|
2048
|
+
const f = fresh[i];
|
|
2049
|
+
const e = this._messages[i];
|
|
2050
|
+
if (f.id) e.id = f.id;
|
|
2051
|
+
if (f.path) e.path = f.path;
|
|
2052
|
+
if (f.messageType) e.messageType = f.messageType;
|
|
2053
|
+
if (f.data) e.data = f.data;
|
|
2054
|
+
if (f.children) e.children = f.children;
|
|
2055
|
+
}
|
|
2056
|
+
await this.rebuildToolCallState();
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
this._messages = fresh;
|
|
2060
|
+
await this.rebuildToolCallState();
|
|
2061
|
+
this.notify();
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
logger.debug("[ConversationStore] Silent reload failed:", err);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
// ==========================================================================
|
|
2067
|
+
// Streaming timeout (Fix A) — auto-recover on SSE inactivity
|
|
2068
|
+
// ==========================================================================
|
|
2069
|
+
resetStreamingTimer() {
|
|
2070
|
+
this.clearStreamingTimer();
|
|
2071
|
+
if (!this._isStreaming) return;
|
|
2072
|
+
this._streamingTimer = setTimeout(() => {
|
|
2073
|
+
if (this._isStreaming) {
|
|
2074
|
+
logger.warn("[ConversationStore] Streaming timeout \u2014 no SSE activity for", this._streamingTimeoutMs, "ms");
|
|
2075
|
+
this.recoverFromHang();
|
|
2076
|
+
}
|
|
2077
|
+
}, this._streamingTimeoutMs);
|
|
2078
|
+
}
|
|
2079
|
+
clearStreamingTimer() {
|
|
2080
|
+
if (this._streamingTimer) {
|
|
2081
|
+
clearTimeout(this._streamingTimer);
|
|
2082
|
+
this._streamingTimer = null;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
// ==========================================================================
|
|
2086
|
+
// Activity watchdog (Fix B) — periodic backend health check
|
|
2087
|
+
// ==========================================================================
|
|
2088
|
+
startWatchdog() {
|
|
2089
|
+
this.stopWatchdog();
|
|
2090
|
+
if (!this._conversationPath) return;
|
|
2091
|
+
this._watchdogTimer = setInterval(async () => {
|
|
2092
|
+
if (!this._isStreaming || !this._conversationPath) {
|
|
2093
|
+
this.stopWatchdog();
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
const health = await this.manager.checkTurnHealth(this._conversationPath);
|
|
2098
|
+
if (health === "done" && this._isStreaming) {
|
|
2099
|
+
logger.warn("[ConversationStore] Watchdog detected completed turn while still streaming \u2014 recovering");
|
|
2100
|
+
this.recoverFromHang();
|
|
2101
|
+
}
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
}, this._watchdogIntervalMs);
|
|
2105
|
+
}
|
|
2106
|
+
stopWatchdog() {
|
|
2107
|
+
if (this._watchdogTimer) {
|
|
2108
|
+
clearInterval(this._watchdogTimer);
|
|
2109
|
+
this._watchdogTimer = null;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
// ==========================================================================
|
|
2113
|
+
// Hang recovery (shared by timeout + watchdog)
|
|
2114
|
+
// ==========================================================================
|
|
2115
|
+
recoverFromHang() {
|
|
2116
|
+
logger.info("[ConversationStore] Recovering from hang");
|
|
2117
|
+
this.clearStreamingTimer();
|
|
2118
|
+
this.stopWatchdog();
|
|
2119
|
+
this._isStreaming = false;
|
|
2120
|
+
this._isWaiting = true;
|
|
2121
|
+
this._streamingText = "";
|
|
2122
|
+
this.notify();
|
|
2123
|
+
this.silentReload().catch(() => {
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Reconcile a recovered done event against persisted state.
|
|
2128
|
+
* If the backend turn is actually complete, finalize the local state.
|
|
2129
|
+
* Otherwise keep streaming active and let normal events continue.
|
|
2130
|
+
*/
|
|
2131
|
+
reconcileRecoveredDone() {
|
|
2132
|
+
if (!this._conversationPath) return;
|
|
2133
|
+
void this.manager.checkTurnHealth(this._conversationPath).then((health) => {
|
|
2134
|
+
if (health !== "done") return;
|
|
2135
|
+
if (!this._isStreaming) return;
|
|
2136
|
+
this._isStreaming = false;
|
|
2137
|
+
this._isWaiting = true;
|
|
2138
|
+
this._activeToolCalls = [];
|
|
2139
|
+
this.clearStreamingTimer();
|
|
2140
|
+
this.stopWatchdog();
|
|
2141
|
+
this.notify();
|
|
2142
|
+
this.deferredReload();
|
|
2143
|
+
}).catch(() => {
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
// ==========================================================================
|
|
2147
|
+
// Tool call state rebuild (Fix C) — reconstruct from node tree on reload
|
|
2148
|
+
// ==========================================================================
|
|
2149
|
+
async rebuildToolCallState() {
|
|
2150
|
+
if (!this._conversationPath) {
|
|
2151
|
+
this._activeToolCalls = [];
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
try {
|
|
2155
|
+
const pendingCalls = await this.manager.getActiveToolCalls(this._conversationPath);
|
|
2156
|
+
if (pendingCalls.length > 0) {
|
|
2157
|
+
this._activeToolCalls = pendingCalls.map((tc) => ({
|
|
2158
|
+
id: tc.id,
|
|
2159
|
+
functionName: tc.name,
|
|
2160
|
+
arguments: void 0,
|
|
2161
|
+
status: "running"
|
|
2162
|
+
}));
|
|
2163
|
+
if (!this._isStreaming) {
|
|
2164
|
+
this._isStreaming = true;
|
|
2165
|
+
this.resetStreamingTimer();
|
|
2166
|
+
this.startWatchdog();
|
|
2167
|
+
}
|
|
2168
|
+
} else {
|
|
2169
|
+
this._activeToolCalls = [];
|
|
2170
|
+
if (this._isStreaming) {
|
|
2171
|
+
const lastMsg = this._messages[this._messages.length - 1];
|
|
2172
|
+
const isLastAssistantDone = lastMsg?.role === "assistant" && (lastMsg.dispatchPhase ? lastMsg.dispatchPhase === "terminal" : !!lastMsg.finishReason && lastMsg.finishReason !== "tool_calls");
|
|
2173
|
+
if (isLastAssistantDone) {
|
|
2174
|
+
this._isStreaming = false;
|
|
2175
|
+
this._isWaiting = true;
|
|
2176
|
+
this.clearStreamingTimer();
|
|
2177
|
+
this.stopWatchdog();
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
this._activeToolCalls = [];
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
notify() {
|
|
2186
|
+
const snapshot = this.getSnapshot();
|
|
2187
|
+
for (const sub of this.subscribers) {
|
|
2188
|
+
try {
|
|
2189
|
+
sub(snapshot);
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
logger.error("Error in conversation store subscriber:", err);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
// src/utils/home-path.ts
|
|
2198
|
+
var ACCESS_CONTROL_PREFIX = "/raisin:access_control";
|
|
2199
|
+
function normalizeHomePath(home) {
|
|
2200
|
+
if (home === null || home === void 0) return null;
|
|
2201
|
+
let path = home.trim();
|
|
2202
|
+
if (path.length === 0) return null;
|
|
2203
|
+
if (path === ACCESS_CONTROL_PREFIX) return "/";
|
|
2204
|
+
if (path.startsWith(`${ACCESS_CONTROL_PREFIX}/`)) {
|
|
2205
|
+
path = path.slice(ACCESS_CONTROL_PREFIX.length);
|
|
2206
|
+
}
|
|
2207
|
+
while (path.length > 1 && path.endsWith("/")) {
|
|
2208
|
+
path = path.slice(0, -1);
|
|
2209
|
+
}
|
|
2210
|
+
return path;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/stores/conversation-list-store.ts
|
|
2214
|
+
function buildChatsSubscriptionFilters(userHome) {
|
|
2215
|
+
const home = normalizeHomePath(userHome) ?? userHome;
|
|
2216
|
+
return {
|
|
2217
|
+
workspace: "raisin:access_control",
|
|
2218
|
+
path: `${home}/inbox/chats/**`,
|
|
2219
|
+
event_types: ["node:created", "node:updated"]
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
var ConversationListStore = class {
|
|
2223
|
+
constructor(options) {
|
|
2224
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
2225
|
+
// Internal state
|
|
2226
|
+
this._conversations = [];
|
|
2227
|
+
this._isLoading = false;
|
|
2228
|
+
this._error = null;
|
|
2229
|
+
// Cached ConversationStore instances
|
|
2230
|
+
this._stores = /* @__PURE__ */ new Map();
|
|
2231
|
+
// Real-time subscription
|
|
2232
|
+
this._realtimeSubscription = null;
|
|
2233
|
+
this.database = options.database;
|
|
2234
|
+
this.type = options.type;
|
|
2235
|
+
this.realtime = options.realtime ?? false;
|
|
2236
|
+
}
|
|
2237
|
+
// ==========================================================================
|
|
2238
|
+
// Subscription
|
|
2239
|
+
// ==========================================================================
|
|
2240
|
+
subscribe(callback) {
|
|
2241
|
+
this.subscribers.add(callback);
|
|
2242
|
+
callback(this.getSnapshot());
|
|
2243
|
+
return () => {
|
|
2244
|
+
this.subscribers.delete(callback);
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
getSnapshot() {
|
|
2248
|
+
return {
|
|
2249
|
+
conversations: [...this._conversations],
|
|
2250
|
+
totalUnreadCount: this._conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0),
|
|
2251
|
+
isLoading: this._isLoading,
|
|
2252
|
+
error: this._error
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
// ==========================================================================
|
|
2256
|
+
// Actions
|
|
2257
|
+
// ==========================================================================
|
|
2258
|
+
/**
|
|
2259
|
+
* Load conversations from the server.
|
|
2260
|
+
*/
|
|
2261
|
+
async load() {
|
|
2262
|
+
this._isLoading = true;
|
|
2263
|
+
this._error = null;
|
|
2264
|
+
this.notify();
|
|
2265
|
+
try {
|
|
2266
|
+
const conversations = await this.database.conversations.list({
|
|
2267
|
+
type: this.type
|
|
2268
|
+
});
|
|
2269
|
+
logger.debug(`[ConversationListStore] Loaded ${conversations.length} conversations`);
|
|
2270
|
+
this._conversations = conversations;
|
|
2271
|
+
if (this.realtime) {
|
|
2272
|
+
this.startRealtimeSubscription();
|
|
2273
|
+
}
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
logger.error("[ConversationListStore] Failed to load conversations", err);
|
|
2276
|
+
this._error = err instanceof Error ? err.message : String(err);
|
|
2277
|
+
} finally {
|
|
2278
|
+
this._isLoading = false;
|
|
2279
|
+
this.notify();
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Create a new conversation.
|
|
2284
|
+
*/
|
|
2285
|
+
async createConversation(options) {
|
|
2286
|
+
const convo = await this.database.conversations.create(options);
|
|
2287
|
+
const listItem = {
|
|
2288
|
+
id: convo.id,
|
|
2289
|
+
type: convo.type,
|
|
2290
|
+
conversationPath: convo.conversationPath,
|
|
2291
|
+
conversationWorkspace: convo.conversationWorkspace,
|
|
2292
|
+
agentRef: convo.agentRef,
|
|
2293
|
+
participants: convo.participants,
|
|
2294
|
+
unreadCount: 0,
|
|
2295
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2296
|
+
};
|
|
2297
|
+
this._conversations = [listItem, ...this._conversations];
|
|
2298
|
+
this.notify();
|
|
2299
|
+
return listItem;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Delete a conversation.
|
|
2303
|
+
*/
|
|
2304
|
+
async deleteConversation(conversationPath) {
|
|
2305
|
+
const store = this._stores.get(conversationPath);
|
|
2306
|
+
if (store) {
|
|
2307
|
+
store.destroy();
|
|
2308
|
+
this._stores.delete(conversationPath);
|
|
2309
|
+
}
|
|
2310
|
+
await this.database.conversations.delete(conversationPath);
|
|
2311
|
+
this._conversations = this._conversations.filter((c) => c.conversationPath !== conversationPath);
|
|
2312
|
+
this.notify();
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Mark a conversation as read.
|
|
2316
|
+
*/
|
|
2317
|
+
async markAsRead(conversationPath) {
|
|
2318
|
+
await this.database.conversations.markAsRead(conversationPath);
|
|
2319
|
+
this._conversations = this._conversations.map(
|
|
2320
|
+
(c) => c.conversationPath === conversationPath ? { ...c, unreadCount: 0 } : c
|
|
2321
|
+
);
|
|
2322
|
+
this.notify();
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Get or create a ConversationStore for a specific conversation.
|
|
2326
|
+
* Caches instances so repeated calls return the same store.
|
|
2327
|
+
*/
|
|
2328
|
+
getConversationStore(conversationPath, overrides) {
|
|
2329
|
+
let store = this._stores.get(conversationPath);
|
|
2330
|
+
if (!store) {
|
|
2331
|
+
store = new ConversationStore({
|
|
2332
|
+
database: this.database,
|
|
2333
|
+
conversationPath,
|
|
2334
|
+
...overrides
|
|
2335
|
+
});
|
|
2336
|
+
this._stores.set(conversationPath, store);
|
|
2337
|
+
}
|
|
2338
|
+
return store;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Clean up all resources.
|
|
2342
|
+
*/
|
|
2343
|
+
destroy() {
|
|
2344
|
+
this._realtimeSubscription?.unsubscribe();
|
|
2345
|
+
this._realtimeSubscription = null;
|
|
2346
|
+
for (const store of this._stores.values()) {
|
|
2347
|
+
store.destroy();
|
|
2348
|
+
}
|
|
2349
|
+
this._stores.clear();
|
|
2350
|
+
this.subscribers.clear();
|
|
2351
|
+
}
|
|
2352
|
+
// ==========================================================================
|
|
2353
|
+
// Internal
|
|
2354
|
+
// ==========================================================================
|
|
2355
|
+
async startRealtimeSubscription() {
|
|
2356
|
+
if (this._realtimeSubscription) return;
|
|
2357
|
+
logger.debug("[ConversationListStore] Starting realtime subscription");
|
|
2358
|
+
try {
|
|
2359
|
+
const workspace = this.database.workspace("raisin:access_control");
|
|
2360
|
+
const events = workspace.events();
|
|
2361
|
+
const result = await this.database.executeSql(
|
|
2362
|
+
`SELECT RAISIN_CURRENT_USER()->>'path'::String as home`
|
|
2363
|
+
);
|
|
2364
|
+
const rows = result.rows;
|
|
2365
|
+
const userHome = rows?.[0]?.home;
|
|
2366
|
+
if (!userHome) return;
|
|
2367
|
+
this._realtimeSubscription = await events.subscribe(
|
|
2368
|
+
buildChatsSubscriptionFilters(userHome),
|
|
2369
|
+
async (event) => {
|
|
2370
|
+
const eventData = event;
|
|
2371
|
+
const nodeId = eventData.payload?.node_id;
|
|
2372
|
+
if (!nodeId) return;
|
|
2373
|
+
try {
|
|
2374
|
+
const nodeResult = await this.database.executeSql(
|
|
2375
|
+
`SELECT id, path, node_type, properties, created_at, updated_at FROM 'raisin:access_control' WHERE id = $1 LIMIT 1`,
|
|
2376
|
+
[nodeId]
|
|
2377
|
+
);
|
|
2378
|
+
const row = nodeResult.rows?.[0];
|
|
2379
|
+
if (!row || row.node_type !== "raisin:Conversation") return;
|
|
2380
|
+
const convPath = row.path;
|
|
2381
|
+
const props = row.properties ?? {};
|
|
2382
|
+
const item = {
|
|
2383
|
+
id: row.id,
|
|
2384
|
+
type: props.conversation_type ?? "ai_chat",
|
|
2385
|
+
conversationPath: convPath,
|
|
2386
|
+
conversationWorkspace: "raisin:access_control",
|
|
2387
|
+
agentRef: props.agent_ref,
|
|
2388
|
+
participants: props.participants,
|
|
2389
|
+
unreadCount: props.unread_count ?? 0,
|
|
2390
|
+
lastMessage: props.last_message,
|
|
2391
|
+
updatedAt: row.updated_at ?? row.created_at
|
|
2392
|
+
};
|
|
2393
|
+
const existingIdx = this._conversations.findIndex((c) => c.conversationPath === convPath);
|
|
2394
|
+
if (existingIdx >= 0) {
|
|
2395
|
+
this._conversations.splice(existingIdx, 1);
|
|
2396
|
+
}
|
|
2397
|
+
this._conversations = [item, ...this._conversations];
|
|
2398
|
+
this.notify();
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
logger.debug("Failed to process conversation event:", err);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
logger.debug("Failed to start realtime subscription:", err);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
notify() {
|
|
2409
|
+
const snapshot = this.getSnapshot();
|
|
2410
|
+
for (const sub of this.subscribers) {
|
|
2411
|
+
try {
|
|
2412
|
+
sub(snapshot);
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
logger.error("Error in conversation list store subscriber:", err);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2420
|
+
// src/integrations/svelte-conversation.ts
|
|
2421
|
+
function createConversationAdapter(options) {
|
|
2422
|
+
const store = new ConversationStore(options);
|
|
2423
|
+
return {
|
|
2424
|
+
/** The underlying ConversationStore instance */
|
|
2425
|
+
store,
|
|
2426
|
+
/** Get the current snapshot */
|
|
2427
|
+
getSnapshot: () => store.getSnapshot(),
|
|
2428
|
+
/** Subscribe to snapshot changes. Returns unsubscribe function. */
|
|
2429
|
+
subscribe: (cb) => store.subscribe(cb),
|
|
2430
|
+
/** Send a user message */
|
|
2431
|
+
sendMessage: (content) => store.sendMessage(content),
|
|
2432
|
+
/** Load persisted messages */
|
|
2433
|
+
loadMessages: () => store.loadMessages(),
|
|
2434
|
+
/** Approve a pending plan */
|
|
2435
|
+
approvePlan: (planPath) => store.approvePlan(planPath),
|
|
2436
|
+
/** Reject a pending plan */
|
|
2437
|
+
rejectPlan: (planPath, feedback) => store.rejectPlan(planPath, feedback),
|
|
2438
|
+
/** Mark a message as read */
|
|
2439
|
+
markMessageAsRead: (messagePath) => store.markMessageAsRead(messagePath),
|
|
2440
|
+
/** Stop streaming */
|
|
2441
|
+
stop: () => store.stop(),
|
|
2442
|
+
/** Get the conversation path */
|
|
2443
|
+
getConversationPath: () => store.getConversationPath(),
|
|
2444
|
+
/** Destroy the store and release resources */
|
|
2445
|
+
destroy: () => store.destroy()
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function createConversationListAdapter(options) {
|
|
2449
|
+
const store = new ConversationListStore(options);
|
|
2450
|
+
return {
|
|
2451
|
+
/** The underlying ConversationListStore instance */
|
|
2452
|
+
store,
|
|
2453
|
+
/** Get the current snapshot */
|
|
2454
|
+
getSnapshot: () => store.getSnapshot(),
|
|
2455
|
+
/** Subscribe to snapshot changes. Returns unsubscribe function. */
|
|
2456
|
+
subscribe: (cb) => store.subscribe(cb),
|
|
2457
|
+
/** Load conversations from the server */
|
|
2458
|
+
load: () => store.load(),
|
|
2459
|
+
/** Create a new conversation */
|
|
2460
|
+
createConversation: (opts) => store.createConversation(opts),
|
|
2461
|
+
/** Delete a conversation */
|
|
2462
|
+
deleteConversation: (path) => store.deleteConversation(path),
|
|
2463
|
+
/** Mark a conversation as read */
|
|
2464
|
+
markAsRead: (path) => store.markAsRead(path),
|
|
2465
|
+
/** Get or create a ConversationStore for a specific path */
|
|
2466
|
+
getConversationStore: (path) => store.getConversationStore(path),
|
|
2467
|
+
/** Destroy the store and release resources */
|
|
2468
|
+
destroy: () => store.destroy()
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2472
|
+
0 && (module.exports = {
|
|
2473
|
+
RAISIN_CONTEXT_KEY,
|
|
2474
|
+
createAuthAdapter,
|
|
2475
|
+
createConnectionAdapter,
|
|
2476
|
+
createConversationAdapter,
|
|
2477
|
+
createConversationListAdapter,
|
|
2478
|
+
createFlowAdapter,
|
|
2479
|
+
createSqlAdapter,
|
|
2480
|
+
createSubscriptionAdapter
|
|
2481
|
+
});
|
|
2482
|
+
//# sourceMappingURL=svelte.js.map
|