@ecodrix/erix-client 1.0.0 → 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 +115 -1
- package/dist/index.js +175 -1
- package/package.json +5 -6
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,49 @@ export interface RateLimitResult {
|
|
|
38
38
|
remaining: number;
|
|
39
39
|
resetAt: number;
|
|
40
40
|
}
|
|
41
|
+
export interface JobV2<T = JsonValue> {
|
|
42
|
+
id: string;
|
|
43
|
+
queueName: string;
|
|
44
|
+
data: T;
|
|
45
|
+
status: "waiting" | "active" | "completed" | "failed" | "delayed";
|
|
46
|
+
attempts: number;
|
|
47
|
+
maxAttempts: number;
|
|
48
|
+
priority: number;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
runAt: string;
|
|
51
|
+
clientCode?: string;
|
|
52
|
+
progress?: number;
|
|
53
|
+
error?: string;
|
|
54
|
+
result?: JsonValue;
|
|
55
|
+
}
|
|
56
|
+
export interface EnqueueOptionsV2 {
|
|
57
|
+
priority?: number;
|
|
58
|
+
maxAttempts?: number;
|
|
59
|
+
delayMs?: number;
|
|
60
|
+
runAt?: Date | string;
|
|
61
|
+
clientCode?: string;
|
|
62
|
+
metadata?: Record<string, JsonValue>;
|
|
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
|
+
}
|
|
41
84
|
export declare class ErixClient {
|
|
42
85
|
private readonly baseUrl;
|
|
43
86
|
private readonly headers;
|
|
@@ -77,14 +120,64 @@ export declare class ErixClient {
|
|
|
77
120
|
lpop: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
78
121
|
};
|
|
79
122
|
queue: {
|
|
80
|
-
/** Enqueue a job payload into a named queue */
|
|
123
|
+
/** Enqueue a job payload into a named queue (simple list-based) */
|
|
81
124
|
push: (name: string, data: JsonValue) => Promise<void>;
|
|
82
125
|
/** Dequeue the oldest job from a named queue. Returns `null` if empty. */
|
|
83
126
|
pop: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
84
127
|
};
|
|
128
|
+
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
129
|
+
queueV2: {
|
|
130
|
+
/** Enqueue a job into an advanced queue */
|
|
131
|
+
push: <T = JsonValue>(queueName: string, data: T, options?: EnqueueOptionsV2) => Promise<JobV2<T>>;
|
|
132
|
+
/** Claim the next eligible job from the queue */
|
|
133
|
+
claim: <T = JsonValue>(queueName: string) => Promise<JobV2<T> | null>;
|
|
134
|
+
/** Mark a job as completed */
|
|
135
|
+
complete: (jobId: string, result?: JsonValue) => Promise<void>;
|
|
136
|
+
/** Mark a job as failed */
|
|
137
|
+
fail: (jobId: string, error: string) => Promise<void>;
|
|
138
|
+
/** Update job progress (0-100) */
|
|
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>;
|
|
146
|
+
/** Get job by ID */
|
|
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
|
+
};
|
|
166
|
+
};
|
|
85
167
|
pubsub: {
|
|
86
168
|
/** Publish a message to a channel */
|
|
87
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
|
+
};
|
|
88
181
|
};
|
|
89
182
|
/**
|
|
90
183
|
* Check if an operation is within rate limits.
|
|
@@ -101,4 +194,25 @@ export declare class ErixClient {
|
|
|
101
194
|
/** Get all members of a set */
|
|
102
195
|
smembers: (key: string) => Promise<string[]>;
|
|
103
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;
|
|
104
218
|
}
|
package/dist/index.js
CHANGED
|
@@ -81,7 +81,7 @@ export class ErixClient {
|
|
|
81
81
|
};
|
|
82
82
|
// ─── Queue (FIFO job queue, built on List) ────────────────────────────
|
|
83
83
|
this.queue = {
|
|
84
|
-
/** Enqueue a job payload into a named queue */
|
|
84
|
+
/** Enqueue a job payload into a named queue (simple list-based) */
|
|
85
85
|
push: async (name, data) => {
|
|
86
86
|
await this.list.rpush(`q:${name}`, data);
|
|
87
87
|
},
|
|
@@ -90,12 +90,91 @@ export class ErixClient {
|
|
|
90
90
|
return this.list.lpop(`q:${name}`);
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
|
+
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
94
|
+
this.queueV2 = {
|
|
95
|
+
/** Enqueue a job into an advanced queue */
|
|
96
|
+
push: async (queueName, data, options = {}) => {
|
|
97
|
+
const res = await this.req("POST", `/queue/v2/${queueName}/jobs`, { data, ...options });
|
|
98
|
+
return res.job;
|
|
99
|
+
},
|
|
100
|
+
/** Claim the next eligible job from the queue */
|
|
101
|
+
claim: async (queueName) => {
|
|
102
|
+
const res = await this.req("POST", `/queue/v2/${queueName}/claim`);
|
|
103
|
+
return res.job;
|
|
104
|
+
},
|
|
105
|
+
/** Mark a job as completed */
|
|
106
|
+
complete: async (jobId, result) => {
|
|
107
|
+
await this.req("POST", `/queue/v2/jobs/${jobId}/complete`, { result });
|
|
108
|
+
},
|
|
109
|
+
/** Mark a job as failed */
|
|
110
|
+
fail: async (jobId, error) => {
|
|
111
|
+
await this.req("POST", `/queue/v2/jobs/${jobId}/fail`, { error });
|
|
112
|
+
},
|
|
113
|
+
/** Update job progress (0-100) */
|
|
114
|
+
updateProgress: async (jobId, progress) => {
|
|
115
|
+
await this.req("PATCH", `/queue/v2/jobs/${jobId}/progress`, { progress });
|
|
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
|
+
},
|
|
125
|
+
/** Get job by ID */
|
|
126
|
+
get: async (jobId) => {
|
|
127
|
+
const res = await this.req("GET", `/queue/v2/jobs/${jobId}`);
|
|
128
|
+
return res.job;
|
|
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
|
+
},
|
|
160
|
+
};
|
|
93
161
|
// ─── PubSub ───────────────────────────────────────────────────────────
|
|
94
162
|
this.pubsub = {
|
|
95
163
|
/** Publish a message to a channel */
|
|
96
164
|
publish: async (channel, message) => {
|
|
97
165
|
await this.req("POST", "/pubsub/publish", { channel, message });
|
|
98
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
|
+
},
|
|
99
178
|
};
|
|
100
179
|
// ─── Set ──────────────────────────────────────────────────────────────
|
|
101
180
|
this.set_ = {
|
|
@@ -109,6 +188,45 @@ export class ErixClient {
|
|
|
109
188
|
return data.members;
|
|
110
189
|
},
|
|
111
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
|
+
};
|
|
112
230
|
this.baseUrl = options.baseUrl.replace(/\/$/, ""); // strip trailing slash
|
|
113
231
|
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
114
232
|
this.headers = {
|
|
@@ -193,4 +311,60 @@ export class ErixClient {
|
|
|
193
311
|
window,
|
|
194
312
|
});
|
|
195
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
|
+
}
|
|
196
370
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecodrix/erix-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Type-safe HTTP client for the erix-store in-memory data service. Zero dependencies — uses native fetch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,10 +15,6 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsc",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
18
|
"keywords": [
|
|
23
19
|
"erix-store",
|
|
24
20
|
"cache",
|
|
@@ -42,5 +38,8 @@
|
|
|
42
38
|
"repository": {
|
|
43
39
|
"type": "git",
|
|
44
40
|
"url": "https://github.com/dhanesh125/erix-store"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc"
|
|
45
44
|
}
|
|
46
|
-
}
|
|
45
|
+
}
|