@ecodrix/erix-client 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +159 -2
- package/dist/index.js +239 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
24
24
|
[key: string]: JsonValue;
|
|
25
25
|
};
|
|
26
26
|
export interface ErixClientOptions {
|
|
27
|
-
/** Full URL of the deployed erix-store instance, e.g. https://erix
|
|
27
|
+
/** Full URL of the deployed erix-store instance, e.g. https://api.erix.ecodrix.com */
|
|
28
28
|
baseUrl: string;
|
|
29
29
|
/** Shared secret generated at deploy time — must match ERIX_API_KEY on the server */
|
|
30
30
|
apiKey: string;
|
|
@@ -61,6 +61,32 @@ export interface EnqueueOptionsV2 {
|
|
|
61
61
|
clientCode?: string;
|
|
62
62
|
metadata?: Record<string, JsonValue>;
|
|
63
63
|
}
|
|
64
|
+
export interface CacheSetOptions {
|
|
65
|
+
/** TTL in milliseconds */
|
|
66
|
+
ttl?: number;
|
|
67
|
+
/** Tags for bulk invalidation */
|
|
68
|
+
tags?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Stale-While-Revalidate window in milliseconds.
|
|
71
|
+
* The server serves the old value for this duration after TTL expires
|
|
72
|
+
* while refreshing in the background.
|
|
73
|
+
*/
|
|
74
|
+
staleFor?: number;
|
|
75
|
+
}
|
|
76
|
+
export interface SemanticGetResult<T = JsonValue> {
|
|
77
|
+
value: T;
|
|
78
|
+
key: string;
|
|
79
|
+
similarity: number;
|
|
80
|
+
isExact: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface QueueEventHandlers<T = JsonValue> {
|
|
83
|
+
onAdded?: (job: JobV2<T>) => void;
|
|
84
|
+
onActive?: (job: JobV2<T>) => void;
|
|
85
|
+
onCompleted?: (job: JobV2<T>) => void;
|
|
86
|
+
onFailed?: (job: JobV2<T>) => void;
|
|
87
|
+
onZombie?: (job: JobV2<T>) => void;
|
|
88
|
+
onError?: (err: Error) => void;
|
|
89
|
+
}
|
|
64
90
|
export declare class ErixClient {
|
|
65
91
|
private readonly baseUrl;
|
|
66
92
|
private readonly headers;
|
|
@@ -105,7 +131,7 @@ export declare class ErixClient {
|
|
|
105
131
|
/** Dequeue the oldest job from a named queue. Returns `null` if empty. */
|
|
106
132
|
pop: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
107
133
|
};
|
|
108
|
-
/** Advanced Queue (v2) — supports priority, delay, retries, and
|
|
134
|
+
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
109
135
|
queueV2: {
|
|
110
136
|
/** Enqueue a job into an advanced queue */
|
|
111
137
|
push: <T = JsonValue>(queueName: string, data: T, options?: EnqueueOptionsV2) => Promise<JobV2<T>>;
|
|
@@ -117,12 +143,47 @@ export declare class ErixClient {
|
|
|
117
143
|
fail: (jobId: string, error: string) => Promise<void>;
|
|
118
144
|
/** Update job progress (0-100) */
|
|
119
145
|
updateProgress: (jobId: string, progress: number) => Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Send a worker heartbeat to keep the job alive in the reaper.
|
|
148
|
+
* Call every 15–30 seconds from inside a long-running job handler.
|
|
149
|
+
* If silent for 60s the reaper will fail and requeue the job.
|
|
150
|
+
*/
|
|
151
|
+
heartbeat: (jobId: string) => Promise<void>;
|
|
120
152
|
/** Get job by ID */
|
|
121
153
|
get: <T = JsonValue>(jobId: string) => Promise<JobV2<T> | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Subscribe to queue events via Server-Sent Events.
|
|
156
|
+
* Returns an EventSource-like object — call `.close()` to unsubscribe.
|
|
157
|
+
*
|
|
158
|
+
* Workers should use this instead of polling `claim()` on an interval.
|
|
159
|
+
* On `job:added` or `job:active` events, call `claim()` to get the job.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* const sub = client.queueV2.subscribe('crm', {
|
|
163
|
+
* onAdded: () => processNextJob(),
|
|
164
|
+
* onError: (e) => console.error(e),
|
|
165
|
+
* });
|
|
166
|
+
* // later:
|
|
167
|
+
* sub.close();
|
|
168
|
+
*/
|
|
169
|
+
subscribe: <T = JsonValue>(queueName: string, handlers: QueueEventHandlers<T>) => {
|
|
170
|
+
close: () => void;
|
|
171
|
+
};
|
|
122
172
|
};
|
|
123
173
|
pubsub: {
|
|
124
174
|
/** Publish a message to a channel */
|
|
125
175
|
publish: (channel: string, message: JsonValue) => Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Subscribe to a pub/sub channel via Server-Sent Events.
|
|
178
|
+
* Returns a controller with a `.close()` method.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const sub = client.pubsub.subscribe('alerts', (msg) => console.log(msg));
|
|
182
|
+
* sub.close(); // unsubscribe
|
|
183
|
+
*/
|
|
184
|
+
subscribe: (channel: string, onMessage: (message: JsonValue) => void, onError?: (err: Error) => void) => {
|
|
185
|
+
close: () => void;
|
|
186
|
+
};
|
|
126
187
|
};
|
|
127
188
|
/**
|
|
128
189
|
* Check if an operation is within rate limits.
|
|
@@ -139,4 +200,100 @@ export declare class ErixClient {
|
|
|
139
200
|
/** Get all members of a set */
|
|
140
201
|
smembers: (key: string) => Promise<string[]>;
|
|
141
202
|
};
|
|
203
|
+
cache: {
|
|
204
|
+
/** Get a cached value */
|
|
205
|
+
get: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
206
|
+
/** Set a cached value with optional TTL, tags, and stale-while-revalidate */
|
|
207
|
+
set: (key: string, value: JsonValue, options?: CacheSetOptions) => Promise<void>;
|
|
208
|
+
/** Delete a cached key */
|
|
209
|
+
del: (key: string) => Promise<void>;
|
|
210
|
+
/** Invalidate all keys with a specific tag */
|
|
211
|
+
invalidateByTag: (tag: string) => Promise<number>;
|
|
212
|
+
/** Invalidate all keys matching multiple tags */
|
|
213
|
+
invalidateByTags: (tags: string[]) => Promise<number>;
|
|
214
|
+
/** Get cache statistics (hit rate, size, evictions) */
|
|
215
|
+
stats: () => Promise<unknown>;
|
|
216
|
+
};
|
|
217
|
+
semantic: {
|
|
218
|
+
/**
|
|
219
|
+
* Store a value with its embedding for similarity lookups.
|
|
220
|
+
* @param key Unique cache key
|
|
221
|
+
* @param text Text to embed (the query/question this entry answers)
|
|
222
|
+
* @param value The value to cache
|
|
223
|
+
* @param ttlMs Optional TTL in milliseconds
|
|
224
|
+
* @param tags Optional tags for bulk invalidation
|
|
225
|
+
*/
|
|
226
|
+
set: (key: string, text: string, value: JsonValue, ttlMs?: number, tags?: string[]) => Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Exact key lookup in semantic cache.
|
|
229
|
+
*/
|
|
230
|
+
get: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
231
|
+
/**
|
|
232
|
+
* Find the most similar cached entry to a query string.
|
|
233
|
+
* Returns null if no entry exceeds the similarity threshold.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Stored: "What are your pricing plans?" → { starter: '$29', pro: '$99' }
|
|
237
|
+
* // Query: "How much does it cost?"
|
|
238
|
+
* const result = await client.semantic.search("How much does it cost?");
|
|
239
|
+
* // → { value: {...}, similarity: 0.97, isExact: false }
|
|
240
|
+
*/
|
|
241
|
+
search: <T = JsonValue>(query: string, threshold?: number) => Promise<SemanticGetResult<T> | null>;
|
|
242
|
+
/** Invalidate all semantic cache entries with a specific tag */
|
|
243
|
+
invalidateByTag: (tag: string) => Promise<number>;
|
|
244
|
+
/** Delete a single semantic cache entry */
|
|
245
|
+
del: (key: string) => Promise<void>;
|
|
246
|
+
/** Get semantic cache stats */
|
|
247
|
+
stats: () => Promise<{
|
|
248
|
+
success: boolean;
|
|
249
|
+
size: number;
|
|
250
|
+
keys: string[];
|
|
251
|
+
}>;
|
|
252
|
+
};
|
|
253
|
+
analytics: {
|
|
254
|
+
/**
|
|
255
|
+
* Get live usage counts for this tenant (from in-memory buffer).
|
|
256
|
+
* Updated in real time — no waiting for the 10s DB flush.
|
|
257
|
+
*/
|
|
258
|
+
usage: () => Promise<Record<string, number>>;
|
|
259
|
+
/**
|
|
260
|
+
* Get anomaly detector stats — mean, stddev, and sample count
|
|
261
|
+
* for each monitored metric of this tenant.
|
|
262
|
+
*/
|
|
263
|
+
anomalies: () => Promise<{
|
|
264
|
+
success: boolean;
|
|
265
|
+
metrics: Array<{
|
|
266
|
+
metric: string;
|
|
267
|
+
mean: number;
|
|
268
|
+
stddev: number;
|
|
269
|
+
n: number;
|
|
270
|
+
}>;
|
|
271
|
+
}>;
|
|
272
|
+
/**
|
|
273
|
+
* Subscribe to real-time anomaly alerts via SSE.
|
|
274
|
+
* The server fires an event whenever a metric exceeds 3σ.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* const sub = client.analytics.subscribeAlerts((alert) => {
|
|
278
|
+
* console.log(`ALERT: ${alert.message}`);
|
|
279
|
+
* });
|
|
280
|
+
* sub.close();
|
|
281
|
+
*/
|
|
282
|
+
subscribeAlerts: (onAlert: (alert: {
|
|
283
|
+
metric: string;
|
|
284
|
+
current: number;
|
|
285
|
+
mean: number;
|
|
286
|
+
zScore: number;
|
|
287
|
+
message: string;
|
|
288
|
+
}) => void, onError?: (err: Error) => void) => {
|
|
289
|
+
close: () => void;
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Opens a persistent SSE connection and dispatches named events to a handler.
|
|
294
|
+
* Returns a controller with `.close()` to terminate the connection.
|
|
295
|
+
*
|
|
296
|
+
* Uses native fetch with a ReadableStream reader — works in Node 18+.
|
|
297
|
+
*/
|
|
298
|
+
private openEventStream;
|
|
142
299
|
}
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,7 @@ export class ErixClient {
|
|
|
90
90
|
return this.list.lpop(`q:${name}`);
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
|
-
/** Advanced Queue (v2) — supports priority, delay, retries, and
|
|
93
|
+
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
94
94
|
this.queueV2 = {
|
|
95
95
|
/** Enqueue a job into an advanced queue */
|
|
96
96
|
push: async (queueName, data, options = {}) => {
|
|
@@ -114,11 +114,49 @@ export class ErixClient {
|
|
|
114
114
|
updateProgress: async (jobId, progress) => {
|
|
115
115
|
await this.req("PATCH", `/queue/v2/jobs/${jobId}/progress`, { progress });
|
|
116
116
|
},
|
|
117
|
+
/**
|
|
118
|
+
* Send a worker heartbeat to keep the job alive in the reaper.
|
|
119
|
+
* Call every 15–30 seconds from inside a long-running job handler.
|
|
120
|
+
* If silent for 60s the reaper will fail and requeue the job.
|
|
121
|
+
*/
|
|
122
|
+
heartbeat: async (jobId) => {
|
|
123
|
+
await this.req("PATCH", `/queue/v2/jobs/${jobId}/heartbeat`);
|
|
124
|
+
},
|
|
117
125
|
/** Get job by ID */
|
|
118
126
|
get: async (jobId) => {
|
|
119
127
|
const res = await this.req("GET", `/queue/v2/jobs/${jobId}`);
|
|
120
128
|
return res.job;
|
|
121
129
|
},
|
|
130
|
+
/**
|
|
131
|
+
* Subscribe to queue events via Server-Sent Events.
|
|
132
|
+
* Returns an EventSource-like object — call `.close()` to unsubscribe.
|
|
133
|
+
*
|
|
134
|
+
* Workers should use this instead of polling `claim()` on an interval.
|
|
135
|
+
* On `job:added` or `job:active` events, call `claim()` to get the job.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* const sub = client.queueV2.subscribe('crm', {
|
|
139
|
+
* onAdded: () => processNextJob(),
|
|
140
|
+
* onError: (e) => console.error(e),
|
|
141
|
+
* });
|
|
142
|
+
* // later:
|
|
143
|
+
* sub.close();
|
|
144
|
+
*/
|
|
145
|
+
subscribe: (queueName, handlers) => {
|
|
146
|
+
return this.openEventStream(`/queue/v2/${queueName}/events`, (event, data) => {
|
|
147
|
+
const job = data;
|
|
148
|
+
if (event === "job:added")
|
|
149
|
+
handlers.onAdded?.(job);
|
|
150
|
+
else if (event === "job:active")
|
|
151
|
+
handlers.onActive?.(job);
|
|
152
|
+
else if (event === "job:completed")
|
|
153
|
+
handlers.onCompleted?.(job);
|
|
154
|
+
else if (event === "job:failed")
|
|
155
|
+
handlers.onFailed?.(job);
|
|
156
|
+
else if (event === "job:zombie")
|
|
157
|
+
handlers.onZombie?.(job);
|
|
158
|
+
}, handlers.onError);
|
|
159
|
+
},
|
|
122
160
|
};
|
|
123
161
|
// ─── PubSub ───────────────────────────────────────────────────────────
|
|
124
162
|
this.pubsub = {
|
|
@@ -126,6 +164,17 @@ export class ErixClient {
|
|
|
126
164
|
publish: async (channel, message) => {
|
|
127
165
|
await this.req("POST", "/pubsub/publish", { channel, message });
|
|
128
166
|
},
|
|
167
|
+
/**
|
|
168
|
+
* Subscribe to a pub/sub channel via Server-Sent Events.
|
|
169
|
+
* Returns a controller with a `.close()` method.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* const sub = client.pubsub.subscribe('alerts', (msg) => console.log(msg));
|
|
173
|
+
* sub.close(); // unsubscribe
|
|
174
|
+
*/
|
|
175
|
+
subscribe: (channel, onMessage, onError) => {
|
|
176
|
+
return this.openEventStream(`/pubsub/${encodeURIComponent(channel)}/stream`, (_event, data) => onMessage(data), onError);
|
|
177
|
+
},
|
|
129
178
|
};
|
|
130
179
|
// ─── Set ──────────────────────────────────────────────────────────────
|
|
131
180
|
this.set_ = {
|
|
@@ -139,6 +188,139 @@ export class ErixClient {
|
|
|
139
188
|
return data.members;
|
|
140
189
|
},
|
|
141
190
|
};
|
|
191
|
+
// ─── Cache (advanced) ────────────────────────────────────────────────
|
|
192
|
+
this.cache = {
|
|
193
|
+
/** Get a cached value */
|
|
194
|
+
get: async (key) => {
|
|
195
|
+
try {
|
|
196
|
+
const res = await this.req("GET", `/cache/${encodeURIComponent(key)}`);
|
|
197
|
+
return res.value ?? null;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
/** Set a cached value with optional TTL, tags, and stale-while-revalidate */
|
|
204
|
+
set: async (key, value, options = {}) => {
|
|
205
|
+
await this.req("POST", `/cache/${encodeURIComponent(key)}`, {
|
|
206
|
+
value,
|
|
207
|
+
...options,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
/** Delete a cached key */
|
|
211
|
+
del: async (key) => {
|
|
212
|
+
await this.req("DELETE", `/cache/${encodeURIComponent(key)}`);
|
|
213
|
+
},
|
|
214
|
+
/** Invalidate all keys with a specific tag */
|
|
215
|
+
invalidateByTag: async (tag) => {
|
|
216
|
+
const res = await this.req("DELETE", `/cache/tags/${encodeURIComponent(tag)}`);
|
|
217
|
+
return res.count;
|
|
218
|
+
},
|
|
219
|
+
/** Invalidate all keys matching multiple tags */
|
|
220
|
+
invalidateByTags: async (tags) => {
|
|
221
|
+
const res = await this.req("POST", "/cache/tags/invalidate", { tags });
|
|
222
|
+
return res.count;
|
|
223
|
+
},
|
|
224
|
+
/** Get cache statistics (hit rate, size, evictions) */
|
|
225
|
+
stats: async () => {
|
|
226
|
+
const res = await this.req("GET", "/cache/_stats");
|
|
227
|
+
return res.stats;
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
// ─── Semantic Cache (AI layer) ────────────────────────────────────────
|
|
231
|
+
this.semantic = {
|
|
232
|
+
/**
|
|
233
|
+
* Store a value with its embedding for similarity lookups.
|
|
234
|
+
* @param key Unique cache key
|
|
235
|
+
* @param text Text to embed (the query/question this entry answers)
|
|
236
|
+
* @param value The value to cache
|
|
237
|
+
* @param ttlMs Optional TTL in milliseconds
|
|
238
|
+
* @param tags Optional tags for bulk invalidation
|
|
239
|
+
*/
|
|
240
|
+
set: async (key, text, value, ttlMs, tags) => {
|
|
241
|
+
await this.req("POST", `/semantic/${encodeURIComponent(key)}`, {
|
|
242
|
+
text,
|
|
243
|
+
value,
|
|
244
|
+
ttlMs,
|
|
245
|
+
tags,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
/**
|
|
249
|
+
* Exact key lookup in semantic cache.
|
|
250
|
+
*/
|
|
251
|
+
get: async (key) => {
|
|
252
|
+
try {
|
|
253
|
+
const res = await this.req("GET", `/semantic/${encodeURIComponent(key)}`);
|
|
254
|
+
return res.value ?? null;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
/**
|
|
261
|
+
* Find the most similar cached entry to a query string.
|
|
262
|
+
* Returns null if no entry exceeds the similarity threshold.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* // Stored: "What are your pricing plans?" → { starter: '$29', pro: '$99' }
|
|
266
|
+
* // Query: "How much does it cost?"
|
|
267
|
+
* const result = await client.semantic.search("How much does it cost?");
|
|
268
|
+
* // → { value: {...}, similarity: 0.97, isExact: false }
|
|
269
|
+
*/
|
|
270
|
+
search: async (query, threshold) => {
|
|
271
|
+
try {
|
|
272
|
+
const res = await this.req("POST", "/semantic/search", { query, threshold });
|
|
273
|
+
return res;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
/** Invalidate all semantic cache entries with a specific tag */
|
|
280
|
+
invalidateByTag: async (tag) => {
|
|
281
|
+
const res = await this.req("DELETE", `/semantic/tags/${encodeURIComponent(tag)}`);
|
|
282
|
+
return res.count;
|
|
283
|
+
},
|
|
284
|
+
/** Delete a single semantic cache entry */
|
|
285
|
+
del: async (key) => {
|
|
286
|
+
await this.req("DELETE", `/semantic/${encodeURIComponent(key)}`);
|
|
287
|
+
},
|
|
288
|
+
/** Get semantic cache stats */
|
|
289
|
+
stats: async () => {
|
|
290
|
+
return this.req("GET", "/semantic/_stats");
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
// ─── Analytics ────────────────────────────────────────────────────────
|
|
294
|
+
this.analytics = {
|
|
295
|
+
/**
|
|
296
|
+
* Get live usage counts for this tenant (from in-memory buffer).
|
|
297
|
+
* Updated in real time — no waiting for the 10s DB flush.
|
|
298
|
+
*/
|
|
299
|
+
usage: async () => {
|
|
300
|
+
const res = await this.req("GET", "/analytics/usage");
|
|
301
|
+
return res.usage;
|
|
302
|
+
},
|
|
303
|
+
/**
|
|
304
|
+
* Get anomaly detector stats — mean, stddev, and sample count
|
|
305
|
+
* for each monitored metric of this tenant.
|
|
306
|
+
*/
|
|
307
|
+
anomalies: async () => {
|
|
308
|
+
return this.req("GET", "/analytics/anomalies");
|
|
309
|
+
},
|
|
310
|
+
/**
|
|
311
|
+
* Subscribe to real-time anomaly alerts via SSE.
|
|
312
|
+
* The server fires an event whenever a metric exceeds 3σ.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* const sub = client.analytics.subscribeAlerts((alert) => {
|
|
316
|
+
* console.log(`ALERT: ${alert.message}`);
|
|
317
|
+
* });
|
|
318
|
+
* sub.close();
|
|
319
|
+
*/
|
|
320
|
+
subscribeAlerts: (onAlert, onError) => {
|
|
321
|
+
return this.openEventStream("/analytics/anomalies/stream", (_event, data) => onAlert(data), onError);
|
|
322
|
+
},
|
|
323
|
+
};
|
|
142
324
|
this.baseUrl = options.baseUrl.replace(/\/$/, ""); // strip trailing slash
|
|
143
325
|
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
144
326
|
this.headers = {
|
|
@@ -223,4 +405,60 @@ export class ErixClient {
|
|
|
223
405
|
window,
|
|
224
406
|
});
|
|
225
407
|
}
|
|
408
|
+
// ─── Internal SSE helper ─────────────────────────────────────────────
|
|
409
|
+
/**
|
|
410
|
+
* Opens a persistent SSE connection and dispatches named events to a handler.
|
|
411
|
+
* Returns a controller with `.close()` to terminate the connection.
|
|
412
|
+
*
|
|
413
|
+
* Uses native fetch with a ReadableStream reader — works in Node 18+.
|
|
414
|
+
*/
|
|
415
|
+
openEventStream(path, onEvent, onError) {
|
|
416
|
+
const controller = new AbortController();
|
|
417
|
+
const url = `${this.baseUrl}${path}`;
|
|
418
|
+
const run = async () => {
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch(url, {
|
|
421
|
+
headers: this.headers,
|
|
422
|
+
signal: controller.signal,
|
|
423
|
+
});
|
|
424
|
+
if (!res.ok || !res.body) {
|
|
425
|
+
throw new Error(`[erix-store] SSE ${path} → ${res.status}: ${res.statusText}`);
|
|
426
|
+
}
|
|
427
|
+
const reader = res.body.getReader();
|
|
428
|
+
const decoder = new TextDecoder();
|
|
429
|
+
let buffer = "";
|
|
430
|
+
let currentEvent = "message";
|
|
431
|
+
while (true) {
|
|
432
|
+
const { done, value } = await reader.read();
|
|
433
|
+
if (done)
|
|
434
|
+
break;
|
|
435
|
+
buffer += decoder.decode(value, { stream: true });
|
|
436
|
+
const lines = buffer.split("\n");
|
|
437
|
+
buffer = lines.pop() ?? "";
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
if (line.startsWith("event:")) {
|
|
440
|
+
currentEvent = line.slice(6).trim();
|
|
441
|
+
}
|
|
442
|
+
else if (line.startsWith("data:")) {
|
|
443
|
+
try {
|
|
444
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
445
|
+
onEvent(currentEvent, data);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// non-JSON data line, skip
|
|
449
|
+
}
|
|
450
|
+
currentEvent = "message";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
if (!controller.signal.aborted) {
|
|
457
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
run();
|
|
462
|
+
return { close: () => controller.abort() };
|
|
463
|
+
}
|
|
226
464
|
}
|
package/package.json
CHANGED