@frost1994/agentic-core 1.0.0
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/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/client.cjs +6 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.ts +149 -0
- package/dist/client.js +6 -0
- package/dist/client.js.map +1 -0
- package/dist/index-CesPelb5.js +619 -0
- package/dist/index-CesPelb5.js.map +1 -0
- package/dist/index-DHqclwK8.cjs +618 -0
- package/dist/index-DHqclwK8.cjs.map +1 -0
- package/dist/index.cjs +605 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +904 -0
- package/dist/index.js +606 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +2 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.ts +298 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const ErrorCodes = {
|
|
3
|
+
NETWORK: "NETWORK",
|
|
4
|
+
TIMEOUT: "TIMEOUT",
|
|
5
|
+
AUTH: "AUTH",
|
|
6
|
+
FORBIDDEN: "FORBIDDEN",
|
|
7
|
+
RATE_LIMIT: "RATE_LIMIT",
|
|
8
|
+
MODEL_UNAVAILABLE: "MODEL_UNAVAILABLE",
|
|
9
|
+
TOOL_FAILED: "TOOL_FAILED",
|
|
10
|
+
SQL_FORBIDDEN: "SQL_FORBIDDEN",
|
|
11
|
+
SQL_SENSITIVE: "SQL_SENSITIVE",
|
|
12
|
+
PARSE: "PARSE",
|
|
13
|
+
ABORTED: "ABORTED",
|
|
14
|
+
UNKNOWN: "UNKNOWN"
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_RETRIABILITY = {
|
|
17
|
+
NETWORK: true,
|
|
18
|
+
TIMEOUT: true,
|
|
19
|
+
AUTH: false,
|
|
20
|
+
FORBIDDEN: false,
|
|
21
|
+
RATE_LIMIT: true,
|
|
22
|
+
MODEL_UNAVAILABLE: true,
|
|
23
|
+
TOOL_FAILED: false,
|
|
24
|
+
SQL_FORBIDDEN: false,
|
|
25
|
+
SQL_SENSITIVE: false,
|
|
26
|
+
PARSE: false,
|
|
27
|
+
ABORTED: false,
|
|
28
|
+
UNKNOWN: false
|
|
29
|
+
};
|
|
30
|
+
function createError(code, message, retriable, details) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = code;
|
|
33
|
+
error.retriable = retriable ?? DEFAULT_RETRIABILITY[code] ?? false;
|
|
34
|
+
error.details = details;
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
function isRetriable(error) {
|
|
38
|
+
if (error instanceof Error && "retriable" in error) {
|
|
39
|
+
return Boolean(error.retriable);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isAborted(error) {
|
|
44
|
+
if (error instanceof Error && "code" in error) {
|
|
45
|
+
return error.code === "ABORTED";
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function generateId(prefix = "") {
|
|
50
|
+
return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
51
|
+
}
|
|
52
|
+
function delay(ms) {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
55
|
+
function clamp(value, min, max) {
|
|
56
|
+
return Math.min(Math.max(value, min), max);
|
|
57
|
+
}
|
|
58
|
+
function isObject(value) {
|
|
59
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function isArray(value) {
|
|
62
|
+
return Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function isString(value) {
|
|
65
|
+
return typeof value === "string";
|
|
66
|
+
}
|
|
67
|
+
function isNumber(value) {
|
|
68
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
69
|
+
}
|
|
70
|
+
function isFunction(value) {
|
|
71
|
+
return typeof value === "function";
|
|
72
|
+
}
|
|
73
|
+
function debounce(fn, waitMs, immediate = false) {
|
|
74
|
+
let timeoutId = null;
|
|
75
|
+
let lastArgs = null;
|
|
76
|
+
let lastResult;
|
|
77
|
+
const invoke = () => {
|
|
78
|
+
timeoutId = null;
|
|
79
|
+
if (lastArgs) {
|
|
80
|
+
lastResult = fn(...lastArgs);
|
|
81
|
+
lastArgs = null;
|
|
82
|
+
}
|
|
83
|
+
return lastResult;
|
|
84
|
+
};
|
|
85
|
+
const debounced = (...args) => {
|
|
86
|
+
lastArgs = args;
|
|
87
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
88
|
+
if (immediate && !timeoutId) {
|
|
89
|
+
lastResult = fn(...args);
|
|
90
|
+
timeoutId = setTimeout(() => {
|
|
91
|
+
timeoutId = null;
|
|
92
|
+
}, waitMs);
|
|
93
|
+
return lastResult;
|
|
94
|
+
}
|
|
95
|
+
timeoutId = setTimeout(invoke, waitMs);
|
|
96
|
+
return void 0;
|
|
97
|
+
};
|
|
98
|
+
debounced.cancel = () => {
|
|
99
|
+
if (timeoutId) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
timeoutId = null;
|
|
102
|
+
}
|
|
103
|
+
lastArgs = null;
|
|
104
|
+
};
|
|
105
|
+
debounced.flush = () => {
|
|
106
|
+
if (timeoutId) {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
return invoke();
|
|
109
|
+
}
|
|
110
|
+
return lastResult;
|
|
111
|
+
};
|
|
112
|
+
return debounced;
|
|
113
|
+
}
|
|
114
|
+
function throttle(fn, waitMs, options = {}) {
|
|
115
|
+
const { leading = true, trailing = true } = options;
|
|
116
|
+
let timeoutId = null;
|
|
117
|
+
let lastArgs = null;
|
|
118
|
+
let lastResult;
|
|
119
|
+
let lastCallTime = 0;
|
|
120
|
+
const invoke = () => {
|
|
121
|
+
timeoutId = null;
|
|
122
|
+
if (lastArgs && trailing) {
|
|
123
|
+
lastResult = fn(...lastArgs);
|
|
124
|
+
lastArgs = null;
|
|
125
|
+
lastCallTime = Date.now();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const throttled = (...args) => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const remaining = waitMs - (now - lastCallTime);
|
|
131
|
+
lastArgs = args;
|
|
132
|
+
if (remaining <= 0 || remaining > waitMs) {
|
|
133
|
+
if (timeoutId) {
|
|
134
|
+
clearTimeout(timeoutId);
|
|
135
|
+
timeoutId = null;
|
|
136
|
+
}
|
|
137
|
+
if (leading) {
|
|
138
|
+
lastResult = fn(...args);
|
|
139
|
+
lastCallTime = now;
|
|
140
|
+
} else {
|
|
141
|
+
lastCallTime = now;
|
|
142
|
+
if (trailing) {
|
|
143
|
+
timeoutId = setTimeout(invoke, waitMs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return lastResult;
|
|
147
|
+
}
|
|
148
|
+
if (!timeoutId && trailing) {
|
|
149
|
+
timeoutId = setTimeout(invoke, remaining);
|
|
150
|
+
}
|
|
151
|
+
return lastResult;
|
|
152
|
+
};
|
|
153
|
+
throttled.cancel = () => {
|
|
154
|
+
if (timeoutId) {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
timeoutId = null;
|
|
157
|
+
}
|
|
158
|
+
lastArgs = null;
|
|
159
|
+
lastCallTime = 0;
|
|
160
|
+
};
|
|
161
|
+
return throttled;
|
|
162
|
+
}
|
|
163
|
+
function deepClone(value) {
|
|
164
|
+
if (value === null || typeof value !== "object") return value;
|
|
165
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
166
|
+
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
return value.map((item) => deepClone(item));
|
|
169
|
+
}
|
|
170
|
+
const cloned = {};
|
|
171
|
+
for (const key of Object.keys(value)) {
|
|
172
|
+
cloned[key] = deepClone(value[key]);
|
|
173
|
+
}
|
|
174
|
+
return cloned;
|
|
175
|
+
}
|
|
176
|
+
function formatDate(input, options) {
|
|
177
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
178
|
+
if (Number.isNaN(d.getTime())) return String(input);
|
|
179
|
+
return d.toLocaleString("zh-CN", {
|
|
180
|
+
year: "numeric",
|
|
181
|
+
month: "2-digit",
|
|
182
|
+
day: "2-digit",
|
|
183
|
+
hour: "2-digit",
|
|
184
|
+
minute: "2-digit",
|
|
185
|
+
second: "2-digit",
|
|
186
|
+
...options
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function formatDuration(ms) {
|
|
190
|
+
if (ms < 0) ms = 0;
|
|
191
|
+
const seconds = Math.floor(ms / 1e3);
|
|
192
|
+
const minutes = Math.floor(seconds / 60);
|
|
193
|
+
const hours = Math.floor(minutes / 60);
|
|
194
|
+
if (hours > 0) {
|
|
195
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
196
|
+
}
|
|
197
|
+
if (minutes > 0) {
|
|
198
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
199
|
+
}
|
|
200
|
+
if (seconds > 0) {
|
|
201
|
+
return `${seconds}s`;
|
|
202
|
+
}
|
|
203
|
+
return `${ms}ms`;
|
|
204
|
+
}
|
|
205
|
+
class SqlGuard {
|
|
206
|
+
constructor(options = {}) {
|
|
207
|
+
this.sensitiveTables = options.sensitiveTables ?? [];
|
|
208
|
+
this.allowedOperations = options.allowedOperations ?? ["SELECT"];
|
|
209
|
+
this.rejectComments = options.rejectComments ?? true;
|
|
210
|
+
this.rejectUnion = options.rejectUnion ?? true;
|
|
211
|
+
this.readOnlyAccount = options.readOnlyAccount ?? true;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate a SQL string against all guard rules.
|
|
215
|
+
*
|
|
216
|
+
* Checks (in order):
|
|
217
|
+
* 1. Read-only account enforcement
|
|
218
|
+
* 2. Comment detection (SQL injection vector)
|
|
219
|
+
* 3. UNION / stacked query detection
|
|
220
|
+
* 4. Operation type whitelist
|
|
221
|
+
* 5. Sensitive table access
|
|
222
|
+
*/
|
|
223
|
+
validate(sql) {
|
|
224
|
+
const normalized = sql.trim();
|
|
225
|
+
if (!normalized) {
|
|
226
|
+
return { allowed: false, reason: "SQL is empty" };
|
|
227
|
+
}
|
|
228
|
+
const upper = normalized.toUpperCase();
|
|
229
|
+
if (this.readOnlyAccount) {
|
|
230
|
+
const writeOps = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE", "REPLACE", "MERGE"];
|
|
231
|
+
for (const op of writeOps) {
|
|
232
|
+
if (this._hasWord(upper, op)) {
|
|
233
|
+
return {
|
|
234
|
+
allowed: false,
|
|
235
|
+
reason: `Write operation "${op}" is forbidden in read-only mode`
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (this.rejectComments) {
|
|
241
|
+
if (normalized.includes("--") || normalized.includes("/*") || normalized.includes("*/")) {
|
|
242
|
+
return {
|
|
243
|
+
allowed: false,
|
|
244
|
+
reason: "SQL comments are not allowed (injection risk)"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (this.rejectUnion) {
|
|
249
|
+
if (this._hasWord(upper, "UNION")) {
|
|
250
|
+
return {
|
|
251
|
+
allowed: false,
|
|
252
|
+
reason: "UNION queries are not allowed"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const semicolonCount = this._countSemicolonsOutsideStrings(normalized);
|
|
256
|
+
if (semicolonCount > 1) {
|
|
257
|
+
return {
|
|
258
|
+
allowed: false,
|
|
259
|
+
reason: "Stacked / multiple queries are not allowed"
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const detectedOp = this._detectOperation(upper);
|
|
264
|
+
if (detectedOp && !this.allowedOperations.includes(detectedOp)) {
|
|
265
|
+
return {
|
|
266
|
+
allowed: false,
|
|
267
|
+
reason: `SQL operation "${detectedOp}" is not in the allowed list: [${this.allowedOperations.join(", ")}]`
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
for (const table of this.sensitiveTables) {
|
|
271
|
+
const tablePattern = new RegExp(`\\b${this._escapeRegex(table)}\\b`, "i");
|
|
272
|
+
if (tablePattern.test(normalized)) {
|
|
273
|
+
return {
|
|
274
|
+
allowed: false,
|
|
275
|
+
reason: `Access to sensitive table "${table}" is forbidden`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { allowed: true, sanitizedSql: normalized };
|
|
280
|
+
}
|
|
281
|
+
/** Quick check: does the SQL contain any forbidden pattern? */
|
|
282
|
+
isSafe(sql) {
|
|
283
|
+
return this.validate(sql).allowed;
|
|
284
|
+
}
|
|
285
|
+
// -------------------------------------------------------------------------
|
|
286
|
+
// Private helpers
|
|
287
|
+
// -------------------------------------------------------------------------
|
|
288
|
+
/** Check if a whole word exists in the text. */
|
|
289
|
+
_hasWord(text, word) {
|
|
290
|
+
const re = new RegExp(`\\b${word}\\b`);
|
|
291
|
+
return re.test(text);
|
|
292
|
+
}
|
|
293
|
+
/** Detect the primary SQL operation (first keyword). */
|
|
294
|
+
_detectOperation(sql) {
|
|
295
|
+
const match = sql.match(/^\\s*(\\w+)/);
|
|
296
|
+
return match ? match[1].toUpperCase() : null;
|
|
297
|
+
}
|
|
298
|
+
/** Count semicolons that are outside quoted string literals. */
|
|
299
|
+
_countSemicolonsOutsideStrings(sql) {
|
|
300
|
+
let count = 0;
|
|
301
|
+
let inString = false;
|
|
302
|
+
let stringChar = null;
|
|
303
|
+
let escaped = false;
|
|
304
|
+
for (let i = 0; i < sql.length; i++) {
|
|
305
|
+
const ch = sql[i];
|
|
306
|
+
if (escaped) {
|
|
307
|
+
escaped = false;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === "\\") {
|
|
311
|
+
escaped = true;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!inString) {
|
|
315
|
+
if (ch === "'" || ch === '"') {
|
|
316
|
+
inString = true;
|
|
317
|
+
stringChar = ch;
|
|
318
|
+
} else if (ch === ";") {
|
|
319
|
+
count++;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
if (ch === stringChar) {
|
|
323
|
+
inString = false;
|
|
324
|
+
stringChar = null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return count;
|
|
329
|
+
}
|
|
330
|
+
_escapeRegex(str) {
|
|
331
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
class SseClient {
|
|
335
|
+
constructor() {
|
|
336
|
+
this.reader = null;
|
|
337
|
+
this.abortCtrl = null;
|
|
338
|
+
this.heartbeatTimer = null;
|
|
339
|
+
this.timeoutTimer = null;
|
|
340
|
+
this.reconnect = { attempt: 0, timer: null };
|
|
341
|
+
this.lastSeq = 0;
|
|
342
|
+
this.buffer = "";
|
|
343
|
+
this.status = "idle";
|
|
344
|
+
this.options = null;
|
|
345
|
+
this.destroyed = false;
|
|
346
|
+
}
|
|
347
|
+
/** Current connection status */
|
|
348
|
+
get connectionStatus() {
|
|
349
|
+
return this.status;
|
|
350
|
+
}
|
|
351
|
+
/** Current reconnect attempt count */
|
|
352
|
+
get reconnectAttempt() {
|
|
353
|
+
return this.reconnect.attempt;
|
|
354
|
+
}
|
|
355
|
+
/** Last acknowledged sequence number */
|
|
356
|
+
get currentSeq() {
|
|
357
|
+
return this.lastSeq;
|
|
358
|
+
}
|
|
359
|
+
// -------------------------------------------------------------------------
|
|
360
|
+
// Lifecycle
|
|
361
|
+
// -------------------------------------------------------------------------
|
|
362
|
+
async start(options) {
|
|
363
|
+
if (this.destroyed) {
|
|
364
|
+
throw createError("UNKNOWN", "SseClient has been destroyed", false);
|
|
365
|
+
}
|
|
366
|
+
this.options = options;
|
|
367
|
+
this.lastSeq = options.lastSeq ?? 0;
|
|
368
|
+
this._setStatus("connecting");
|
|
369
|
+
await this._connect();
|
|
370
|
+
}
|
|
371
|
+
/** Abort the current connection (does not destroy). */
|
|
372
|
+
abort() {
|
|
373
|
+
this._clearTimers();
|
|
374
|
+
this._cancelReader();
|
|
375
|
+
this._setStatus("closed");
|
|
376
|
+
}
|
|
377
|
+
/** Permanently destroy — no further connections allowed. */
|
|
378
|
+
destroy() {
|
|
379
|
+
this.destroyed = true;
|
|
380
|
+
this.abort();
|
|
381
|
+
this._clearReconnectTimer();
|
|
382
|
+
}
|
|
383
|
+
// -------------------------------------------------------------------------
|
|
384
|
+
// Connection
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
async _connect() {
|
|
387
|
+
var _a;
|
|
388
|
+
const opts = this.options;
|
|
389
|
+
if (!opts || this.destroyed) return;
|
|
390
|
+
this._clearReconnectTimer();
|
|
391
|
+
this.abortCtrl = new AbortController();
|
|
392
|
+
if (opts.signal) {
|
|
393
|
+
const onAbort = () => {
|
|
394
|
+
var _a2;
|
|
395
|
+
return (_a2 = this.abortCtrl) == null ? void 0 : _a2.abort();
|
|
396
|
+
};
|
|
397
|
+
if (opts.signal.aborted) {
|
|
398
|
+
onAbort();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
402
|
+
}
|
|
403
|
+
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
404
|
+
this.timeoutTimer = setTimeout(() => {
|
|
405
|
+
this._handleError(createError("TIMEOUT", `Request timed out after ${timeoutMs}ms`, true));
|
|
406
|
+
}, timeoutMs);
|
|
407
|
+
const isReconnect = this.reconnect.attempt > 0;
|
|
408
|
+
const resumeFields = isReconnect ? ((_a = opts.resume) == null ? void 0 : _a.call(opts)) ?? {} : {};
|
|
409
|
+
const resumeSeq = typeof resumeFields.lastSeq === "number" ? resumeFields.lastSeq : this.lastSeq;
|
|
410
|
+
try {
|
|
411
|
+
const res = await fetch(opts.url, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: {
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
Accept: "text/event-stream",
|
|
416
|
+
...opts.headers
|
|
417
|
+
},
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
...typeof opts.body === "object" && opts.body !== null ? opts.body : {},
|
|
420
|
+
...resumeFields,
|
|
421
|
+
lastSeq: resumeSeq
|
|
422
|
+
}),
|
|
423
|
+
signal: this.abortCtrl.signal
|
|
424
|
+
});
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
const status = res.status;
|
|
427
|
+
if (status === 401 || status === 403) {
|
|
428
|
+
throw createError("AUTH", `Authentication failed (${status})`, false);
|
|
429
|
+
}
|
|
430
|
+
if (status === 429) {
|
|
431
|
+
throw createError("RATE_LIMIT", "Rate limit exceeded", true);
|
|
432
|
+
}
|
|
433
|
+
throw createError("NETWORK", `HTTP ${status}: ${res.statusText}`, status >= 500);
|
|
434
|
+
}
|
|
435
|
+
if (!res.body) {
|
|
436
|
+
throw createError("NETWORK", "Response body is empty", true);
|
|
437
|
+
}
|
|
438
|
+
this._setStatus("streaming");
|
|
439
|
+
this._resetReconnect();
|
|
440
|
+
this._startHeartbeat(opts.heartbeatMs ?? 15e3);
|
|
441
|
+
this.reader = res.body.getReader();
|
|
442
|
+
const decoder = new TextDecoder();
|
|
443
|
+
while (true) {
|
|
444
|
+
const { done, value } = await this.reader.read();
|
|
445
|
+
if (done) break;
|
|
446
|
+
this._processChunk(decoder.decode(value, { stream: true }));
|
|
447
|
+
}
|
|
448
|
+
this._setStatus("closed");
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (this.destroyed) return;
|
|
451
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
452
|
+
this._setStatus("closed");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this._handleError(err instanceof Error ? err : new Error(String(err)));
|
|
456
|
+
} finally {
|
|
457
|
+
this._clearTimers();
|
|
458
|
+
this._cancelReader();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
// Stream parsing
|
|
463
|
+
// -------------------------------------------------------------------------
|
|
464
|
+
_processChunk(chunk) {
|
|
465
|
+
this.buffer += chunk;
|
|
466
|
+
const lines = this.buffer.split("\n");
|
|
467
|
+
this.buffer = lines.pop() ?? "";
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
this._processLine(line);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
_processLine(line) {
|
|
473
|
+
const trimmed = line.trim();
|
|
474
|
+
if (!trimmed) return;
|
|
475
|
+
if (trimmed.startsWith(":")) {
|
|
476
|
+
this._handleHeartbeat();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (trimmed.startsWith("data:")) {
|
|
480
|
+
const payload = trimmed.slice(5).trim();
|
|
481
|
+
if (!payload) return;
|
|
482
|
+
this._parseEvent(payload);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
_parseEvent(payload) {
|
|
487
|
+
var _a, _b, _c, _d;
|
|
488
|
+
try {
|
|
489
|
+
const event = JSON.parse(payload);
|
|
490
|
+
if (typeof event.seq === "number") {
|
|
491
|
+
this.lastSeq = event.seq;
|
|
492
|
+
}
|
|
493
|
+
if (event.type === "heartbeat") {
|
|
494
|
+
this._handleHeartbeat();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
(_b = (_a = this.options) == null ? void 0 : _a.onEvent) == null ? void 0 : _b.call(_a, event);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
(_d = (_c = this.options) == null ? void 0 : _c.onError) == null ? void 0 : _d.call(
|
|
500
|
+
_c,
|
|
501
|
+
createError("PARSE", `Failed to parse SSE payload: ${payload.slice(0, 200)}`, false, err)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// -------------------------------------------------------------------------
|
|
506
|
+
// Heartbeat
|
|
507
|
+
// -------------------------------------------------------------------------
|
|
508
|
+
_startHeartbeat(ms) {
|
|
509
|
+
this._clearHeartbeat();
|
|
510
|
+
this.heartbeatTimer = setTimeout(() => {
|
|
511
|
+
this._handleError(createError("TIMEOUT", "Heartbeat timeout — no data received", true));
|
|
512
|
+
}, ms * 2);
|
|
513
|
+
}
|
|
514
|
+
_handleHeartbeat() {
|
|
515
|
+
var _a, _b, _c;
|
|
516
|
+
this._startHeartbeat(((_a = this.options) == null ? void 0 : _a.heartbeatMs) ?? 15e3);
|
|
517
|
+
(_c = (_b = this.options) == null ? void 0 : _b.onEvent) == null ? void 0 : _c.call(_b, { type: "heartbeat", ts: Date.now() });
|
|
518
|
+
}
|
|
519
|
+
// -------------------------------------------------------------------------
|
|
520
|
+
// Error & Reconnect
|
|
521
|
+
// -------------------------------------------------------------------------
|
|
522
|
+
_handleError(error) {
|
|
523
|
+
var _a, _b, _c, _d, _e;
|
|
524
|
+
this._setStatus("error");
|
|
525
|
+
(_b = (_a = this.options) == null ? void 0 : _a.onError) == null ? void 0 : _b.call(_a, error);
|
|
526
|
+
const cfg = (_c = this.options) == null ? void 0 : _c.reconnect;
|
|
527
|
+
if (!cfg) return;
|
|
528
|
+
if (this.reconnect.attempt >= cfg.max) {
|
|
529
|
+
this._setStatus("error");
|
|
530
|
+
(_e = (_d = this.options) == null ? void 0 : _d.onError) == null ? void 0 : _e.call(
|
|
531
|
+
_d,
|
|
532
|
+
createError("NETWORK", `Max reconnect attempts (${cfg.max}) exceeded`, false)
|
|
533
|
+
);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this._scheduleReconnect(cfg);
|
|
537
|
+
}
|
|
538
|
+
_scheduleReconnect(cfg) {
|
|
539
|
+
this._setStatus("reconnecting");
|
|
540
|
+
const base = cfg.baseMs ?? 1e3;
|
|
541
|
+
const maxMs = cfg.maxMs ?? 3e4;
|
|
542
|
+
const delayMs = clamp(base * 2 ** this.reconnect.attempt, base, maxMs);
|
|
543
|
+
const jitter = delayMs * 0.25 * (Math.random() * 2 - 1);
|
|
544
|
+
const finalDelay = Math.max(0, Math.floor(delayMs + jitter));
|
|
545
|
+
this.reconnect.attempt++;
|
|
546
|
+
this.reconnect.timer = setTimeout(() => {
|
|
547
|
+
if (!this.destroyed) {
|
|
548
|
+
this._connect();
|
|
549
|
+
}
|
|
550
|
+
}, finalDelay);
|
|
551
|
+
}
|
|
552
|
+
_resetReconnect() {
|
|
553
|
+
this.reconnect.attempt = 0;
|
|
554
|
+
this._clearReconnectTimer();
|
|
555
|
+
}
|
|
556
|
+
// -------------------------------------------------------------------------
|
|
557
|
+
// Helpers
|
|
558
|
+
// -------------------------------------------------------------------------
|
|
559
|
+
_setStatus(next) {
|
|
560
|
+
var _a, _b;
|
|
561
|
+
if (this.status === next) return;
|
|
562
|
+
this.status = next;
|
|
563
|
+
(_b = (_a = this.options) == null ? void 0 : _a.onStatusChange) == null ? void 0 : _b.call(_a, next);
|
|
564
|
+
}
|
|
565
|
+
_cancelReader() {
|
|
566
|
+
if (this.reader) {
|
|
567
|
+
this.reader.cancel().catch(() => {
|
|
568
|
+
});
|
|
569
|
+
this.reader = null;
|
|
570
|
+
}
|
|
571
|
+
if (this.abortCtrl) {
|
|
572
|
+
try {
|
|
573
|
+
this.abortCtrl.abort();
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
this.abortCtrl = null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
_clearTimers() {
|
|
580
|
+
if (this.timeoutTimer) {
|
|
581
|
+
clearTimeout(this.timeoutTimer);
|
|
582
|
+
this.timeoutTimer = null;
|
|
583
|
+
}
|
|
584
|
+
this._clearHeartbeat();
|
|
585
|
+
}
|
|
586
|
+
_clearHeartbeat() {
|
|
587
|
+
if (this.heartbeatTimer) {
|
|
588
|
+
clearTimeout(this.heartbeatTimer);
|
|
589
|
+
this.heartbeatTimer = null;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
_clearReconnectTimer() {
|
|
593
|
+
if (this.reconnect.timer) {
|
|
594
|
+
clearTimeout(this.reconnect.timer);
|
|
595
|
+
this.reconnect.timer = null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
exports.ErrorCodes = ErrorCodes;
|
|
600
|
+
exports.SqlGuard = SqlGuard;
|
|
601
|
+
exports.SseClient = SseClient;
|
|
602
|
+
exports.clamp = clamp;
|
|
603
|
+
exports.createError = createError;
|
|
604
|
+
exports.debounce = debounce;
|
|
605
|
+
exports.deepClone = deepClone;
|
|
606
|
+
exports.delay = delay;
|
|
607
|
+
exports.formatDate = formatDate;
|
|
608
|
+
exports.formatDuration = formatDuration;
|
|
609
|
+
exports.generateId = generateId;
|
|
610
|
+
exports.isAborted = isAborted;
|
|
611
|
+
exports.isArray = isArray;
|
|
612
|
+
exports.isFunction = isFunction;
|
|
613
|
+
exports.isNumber = isNumber;
|
|
614
|
+
exports.isObject = isObject;
|
|
615
|
+
exports.isRetriable = isRetriable;
|
|
616
|
+
exports.isString = isString;
|
|
617
|
+
exports.throttle = throttle;
|
|
618
|
+
//# sourceMappingURL=index-DHqclwK8.cjs.map
|