@ecodrix/erix-client 1.0.1 → 1.0.2
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 +77 -1
- package/dist/index.js +145 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,26 @@ 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 QueueEventHandlers<T = JsonValue> {
|
|
77
|
+
onAdded?: (job: JobV2<T>) => void;
|
|
78
|
+
onActive?: (job: JobV2<T>) => void;
|
|
79
|
+
onCompleted?: (job: JobV2<T>) => void;
|
|
80
|
+
onFailed?: (job: JobV2<T>) => void;
|
|
81
|
+
onZombie?: (job: JobV2<T>) => void;
|
|
82
|
+
onError?: (err: Error) => void;
|
|
83
|
+
}
|
|
64
84
|
export declare class ErixClient {
|
|
65
85
|
private readonly baseUrl;
|
|
66
86
|
private readonly headers;
|
|
@@ -105,7 +125,7 @@ export declare class ErixClient {
|
|
|
105
125
|
/** Dequeue the oldest job from a named queue. Returns `null` if empty. */
|
|
106
126
|
pop: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
107
127
|
};
|
|
108
|
-
/** Advanced Queue (v2) — supports priority, delay, retries, and
|
|
128
|
+
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
109
129
|
queueV2: {
|
|
110
130
|
/** Enqueue a job into an advanced queue */
|
|
111
131
|
push: <T = JsonValue>(queueName: string, data: T, options?: EnqueueOptionsV2) => Promise<JobV2<T>>;
|
|
@@ -117,12 +137,47 @@ export declare class ErixClient {
|
|
|
117
137
|
fail: (jobId: string, error: string) => Promise<void>;
|
|
118
138
|
/** Update job progress (0-100) */
|
|
119
139
|
updateProgress: (jobId: string, progress: number) => Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Send a worker heartbeat to keep the job alive in the reaper.
|
|
142
|
+
* Call every 15–30 seconds from inside a long-running job handler.
|
|
143
|
+
* If silent for 60s the reaper will fail and requeue the job.
|
|
144
|
+
*/
|
|
145
|
+
heartbeat: (jobId: string) => Promise<void>;
|
|
120
146
|
/** Get job by ID */
|
|
121
147
|
get: <T = JsonValue>(jobId: string) => Promise<JobV2<T> | null>;
|
|
148
|
+
/**
|
|
149
|
+
* Subscribe to queue events via Server-Sent Events.
|
|
150
|
+
* Returns an EventSource-like object — call `.close()` to unsubscribe.
|
|
151
|
+
*
|
|
152
|
+
* Workers should use this instead of polling `claim()` on an interval.
|
|
153
|
+
* On `job:added` or `job:active` events, call `claim()` to get the job.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* const sub = client.queueV2.subscribe('crm', {
|
|
157
|
+
* onAdded: () => processNextJob(),
|
|
158
|
+
* onError: (e) => console.error(e),
|
|
159
|
+
* });
|
|
160
|
+
* // later:
|
|
161
|
+
* sub.close();
|
|
162
|
+
*/
|
|
163
|
+
subscribe: <T = JsonValue>(queueName: string, handlers: QueueEventHandlers<T>) => {
|
|
164
|
+
close: () => void;
|
|
165
|
+
};
|
|
122
166
|
};
|
|
123
167
|
pubsub: {
|
|
124
168
|
/** Publish a message to a channel */
|
|
125
169
|
publish: (channel: string, message: JsonValue) => Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Subscribe to a pub/sub channel via Server-Sent Events.
|
|
172
|
+
* Returns a controller with a `.close()` method.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const sub = client.pubsub.subscribe('alerts', (msg) => console.log(msg));
|
|
176
|
+
* sub.close(); // unsubscribe
|
|
177
|
+
*/
|
|
178
|
+
subscribe: (channel: string, onMessage: (message: JsonValue) => void, onError?: (err: Error) => void) => {
|
|
179
|
+
close: () => void;
|
|
180
|
+
};
|
|
126
181
|
};
|
|
127
182
|
/**
|
|
128
183
|
* Check if an operation is within rate limits.
|
|
@@ -139,4 +194,25 @@ export declare class ErixClient {
|
|
|
139
194
|
/** Get all members of a set */
|
|
140
195
|
smembers: (key: string) => Promise<string[]>;
|
|
141
196
|
};
|
|
197
|
+
cache: {
|
|
198
|
+
/** Get a cached value */
|
|
199
|
+
get: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
200
|
+
/** Set a cached value with optional TTL, tags, and stale-while-revalidate */
|
|
201
|
+
set: (key: string, value: JsonValue, options?: CacheSetOptions) => Promise<void>;
|
|
202
|
+
/** Delete a cached key */
|
|
203
|
+
del: (key: string) => Promise<void>;
|
|
204
|
+
/** Invalidate all keys with a specific tag */
|
|
205
|
+
invalidateByTag: (tag: string) => Promise<number>;
|
|
206
|
+
/** Invalidate all keys matching multiple tags */
|
|
207
|
+
invalidateByTags: (tags: string[]) => Promise<number>;
|
|
208
|
+
/** Get cache statistics (hit rate, size, evictions) */
|
|
209
|
+
stats: () => Promise<unknown>;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Opens a persistent SSE connection and dispatches named events to a handler.
|
|
213
|
+
* Returns a controller with `.close()` to terminate the connection.
|
|
214
|
+
*
|
|
215
|
+
* Uses native fetch with a ReadableStream reader — works in Node 18+.
|
|
216
|
+
*/
|
|
217
|
+
private openEventStream;
|
|
142
218
|
}
|
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,45 @@ 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
|
+
};
|
|
142
230
|
this.baseUrl = options.baseUrl.replace(/\/$/, ""); // strip trailing slash
|
|
143
231
|
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
144
232
|
this.headers = {
|
|
@@ -223,4 +311,60 @@ export class ErixClient {
|
|
|
223
311
|
window,
|
|
224
312
|
});
|
|
225
313
|
}
|
|
314
|
+
// ─── Internal SSE helper ─────────────────────────────────────────────
|
|
315
|
+
/**
|
|
316
|
+
* Opens a persistent SSE connection and dispatches named events to a handler.
|
|
317
|
+
* Returns a controller with `.close()` to terminate the connection.
|
|
318
|
+
*
|
|
319
|
+
* Uses native fetch with a ReadableStream reader — works in Node 18+.
|
|
320
|
+
*/
|
|
321
|
+
openEventStream(path, onEvent, onError) {
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
const url = `${this.baseUrl}${path}`;
|
|
324
|
+
const run = async () => {
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch(url, {
|
|
327
|
+
headers: this.headers,
|
|
328
|
+
signal: controller.signal,
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok || !res.body) {
|
|
331
|
+
throw new Error(`[erix-store] SSE ${path} → ${res.status}: ${res.statusText}`);
|
|
332
|
+
}
|
|
333
|
+
const reader = res.body.getReader();
|
|
334
|
+
const decoder = new TextDecoder();
|
|
335
|
+
let buffer = "";
|
|
336
|
+
let currentEvent = "message";
|
|
337
|
+
while (true) {
|
|
338
|
+
const { done, value } = await reader.read();
|
|
339
|
+
if (done)
|
|
340
|
+
break;
|
|
341
|
+
buffer += decoder.decode(value, { stream: true });
|
|
342
|
+
const lines = buffer.split("\n");
|
|
343
|
+
buffer = lines.pop() ?? "";
|
|
344
|
+
for (const line of lines) {
|
|
345
|
+
if (line.startsWith("event:")) {
|
|
346
|
+
currentEvent = line.slice(6).trim();
|
|
347
|
+
}
|
|
348
|
+
else if (line.startsWith("data:")) {
|
|
349
|
+
try {
|
|
350
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
351
|
+
onEvent(currentEvent, data);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// non-JSON data line, skip
|
|
355
|
+
}
|
|
356
|
+
currentEvent = "message";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
if (!controller.signal.aborted) {
|
|
363
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
run();
|
|
368
|
+
return { close: () => controller.abort() };
|
|
369
|
+
}
|
|
226
370
|
}
|
package/package.json
CHANGED