@ecodrix/erix-client 1.2.0 → 1.4.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/dist/index.d.ts +407 -10
- package/dist/index.js +631 -11
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* import { ErixClient } from '@ecodrix/erix-client'
|
|
11
11
|
*
|
|
12
12
|
* const store = new ErixClient({
|
|
13
|
-
* baseUrl: 'https://
|
|
13
|
+
* baseUrl: 'https://store.ecodrix.com',
|
|
14
14
|
* apiKey: process.env.ERIX_API_KEY!,
|
|
15
15
|
* tenantId: 'org_abc123',
|
|
16
16
|
* })
|
|
@@ -28,7 +28,7 @@ export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
28
28
|
[key: string]: JsonValue;
|
|
29
29
|
};
|
|
30
30
|
export interface ErixClientOptions {
|
|
31
|
-
/** Full URL of the deployed erix-store instance, e.g. https://
|
|
31
|
+
/** Full URL of the deployed erix-store instance, e.g. https://store.ecodrix.com */
|
|
32
32
|
baseUrl: string;
|
|
33
33
|
/** Shared secret generated at deploy time — must match ERIX_API_KEY on the server */
|
|
34
34
|
apiKey: string;
|
|
@@ -122,6 +122,52 @@ export declare class ErixClient {
|
|
|
122
122
|
body?: unknown;
|
|
123
123
|
params?: Record<string, string>;
|
|
124
124
|
}>): Promise<unknown[]>;
|
|
125
|
+
/**
|
|
126
|
+
* Begin a new transaction.
|
|
127
|
+
*
|
|
128
|
+
* Returns a builder you can queue commands onto with `.cmd(name, ...args)`,
|
|
129
|
+
* then send with `.exec()`. Every queued command runs atomically inside
|
|
130
|
+
* a single event-loop tick on the server — no other client request can
|
|
131
|
+
* interleave between them.
|
|
132
|
+
*
|
|
133
|
+
* Per-command failures are captured into the result array but do not
|
|
134
|
+
* abort the batch (matching Redis MULTI/EXEC).
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const results = await client
|
|
139
|
+
* .multi()
|
|
140
|
+
* .cmd("SET", "k", "v")
|
|
141
|
+
* .cmd("EXPIRE", "k", 60)
|
|
142
|
+
* .cmd("GET", "k")
|
|
143
|
+
* .exec();
|
|
144
|
+
* // → [{ ok: true, value: "OK" }, { ok: true, value: 1 }, { ok: true, value: "v" }]
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
multi(): Transaction;
|
|
148
|
+
/** Alias for {@link multi}. */
|
|
149
|
+
tx(): Transaction;
|
|
150
|
+
/** @internal — called by Transaction.exec(). Keeps tx wire format private. */
|
|
151
|
+
_execTransaction(commands: TransactionCommand[]): Promise<TransactionResult[]>;
|
|
152
|
+
/**
|
|
153
|
+
* Run a single Redis-style command synchronously.
|
|
154
|
+
*
|
|
155
|
+
* Lightweight escape hatch for verbs we have not surfaced as named
|
|
156
|
+
* methods. Forwards the call through `/tx/exec` as a one-element
|
|
157
|
+
* transaction and unwraps the per-command result.
|
|
158
|
+
*
|
|
159
|
+
* Throws on per-command errors (`WRONGTYPE`, unknown verb, validation)
|
|
160
|
+
* so it composes naturally with `try`/`catch`. For multi-command
|
|
161
|
+
* batches use {@link multi} instead — that surface keeps each command's
|
|
162
|
+
* error isolated.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* const newLen = await client.cmd<number>("APPEND", "log", "entry");
|
|
167
|
+
* const all = await client.cmd<Record<string,string>>("HGETALL", "user");
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
cmd<T = unknown>(name: string, ...args: unknown[]): Promise<T>;
|
|
125
171
|
/**
|
|
126
172
|
* Close the transport and release resources.
|
|
127
173
|
*/
|
|
@@ -131,6 +177,138 @@ export declare class ErixClient {
|
|
|
131
177
|
status: string;
|
|
132
178
|
uptime: number;
|
|
133
179
|
}>;
|
|
180
|
+
server: {
|
|
181
|
+
/** Round-trip ping. Resolves to `true` on success. */
|
|
182
|
+
ping: () => Promise<boolean>;
|
|
183
|
+
/**
|
|
184
|
+
* Server-wide info — version, memory, total keys, and a per-tenant
|
|
185
|
+
* key breakdown for the calling tenant.
|
|
186
|
+
*/
|
|
187
|
+
info: () => Promise<{
|
|
188
|
+
server: {
|
|
189
|
+
version: string;
|
|
190
|
+
node: string;
|
|
191
|
+
uptime_seconds: number;
|
|
192
|
+
};
|
|
193
|
+
memory: {
|
|
194
|
+
rss: number;
|
|
195
|
+
heap_used: number;
|
|
196
|
+
heap_total: number;
|
|
197
|
+
external: number;
|
|
198
|
+
/** Approximate accounted bytes (not RSS). See server INFO docs. */
|
|
199
|
+
used_memory: number;
|
|
200
|
+
/** Configured cap; 0 = disabled. */
|
|
201
|
+
maxmemory: number;
|
|
202
|
+
maxmemory_policy: "noeviction" | "allkeys-lru" | "volatile-lru";
|
|
203
|
+
evicted_keys: number;
|
|
204
|
+
};
|
|
205
|
+
keyspace: {
|
|
206
|
+
total_keys: number;
|
|
207
|
+
tenant: {
|
|
208
|
+
id: string;
|
|
209
|
+
keys: {
|
|
210
|
+
string: number;
|
|
211
|
+
hash: number;
|
|
212
|
+
list: number;
|
|
213
|
+
set: number;
|
|
214
|
+
zset: number;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
}>;
|
|
219
|
+
/** Number of keys in the calling tenant's namespace. */
|
|
220
|
+
dbsize: () => Promise<number>;
|
|
221
|
+
/** Whether `key` exists in the calling tenant's namespace. */
|
|
222
|
+
exists: (key: string) => Promise<boolean>;
|
|
223
|
+
/** The data type of `key`, or `null` if it does not exist. */
|
|
224
|
+
type: (key: string) => Promise<"string" | "hash" | "list" | "set" | "zset" | null>;
|
|
225
|
+
/**
|
|
226
|
+
* List keys matching a glob pattern. Tenant-scoped — never returns
|
|
227
|
+
* keys from other tenants. Default pattern `*` returns every key.
|
|
228
|
+
*
|
|
229
|
+
* Supported glob syntax: `*`, `?`, `[abc]`, `[^abc]`, `[a-z]`, `\\<char>`.
|
|
230
|
+
*/
|
|
231
|
+
keys: (pattern?: string) => Promise<string[]>;
|
|
232
|
+
/**
|
|
233
|
+
* Wipe every key in the calling tenant's namespace.
|
|
234
|
+
* Returns the number of keys deleted. There is intentionally no
|
|
235
|
+
* `flushall` — tenants cannot affect each other.
|
|
236
|
+
*/
|
|
237
|
+
flushdb: () => Promise<number>;
|
|
238
|
+
/**
|
|
239
|
+
* Set a TTL (in seconds) on an existing key.
|
|
240
|
+
* @returns true if the TTL was applied, false if the key didn't exist.
|
|
241
|
+
*/
|
|
242
|
+
expire: (key: string, ttlSeconds: number) => Promise<boolean>;
|
|
243
|
+
/**
|
|
244
|
+
* Remaining TTL in seconds. Redis convention:
|
|
245
|
+
* -2 → key does not exist
|
|
246
|
+
* -1 → key exists but has no TTL
|
|
247
|
+
* >0 → seconds remaining
|
|
248
|
+
*/
|
|
249
|
+
ttl: (key: string) => Promise<number>;
|
|
250
|
+
/**
|
|
251
|
+
* Remove the TTL from a key. Returns true if a TTL was removed,
|
|
252
|
+
* false if the key did not exist or had no TTL.
|
|
253
|
+
*/
|
|
254
|
+
persist: (key: string) => Promise<boolean>;
|
|
255
|
+
/**
|
|
256
|
+
* Read the slow-command log for the calling tenant. Newest entries
|
|
257
|
+
* first. Pass `count` to cap the result.
|
|
258
|
+
*
|
|
259
|
+
* Each entry's `args` are truncated server-side (max 32 args, 128
|
|
260
|
+
* chars each) so an MSET/HSET with a giant payload doesn't bloat
|
|
261
|
+
* the log buffer.
|
|
262
|
+
*/
|
|
263
|
+
slowlogGet: (count?: number) => Promise<Array<{
|
|
264
|
+
id: number;
|
|
265
|
+
timestamp: number;
|
|
266
|
+
durationUs: number;
|
|
267
|
+
command: string;
|
|
268
|
+
args: string[];
|
|
269
|
+
source: "single" | "transaction";
|
|
270
|
+
}>>;
|
|
271
|
+
/** Number of slowlog entries currently stored for this tenant. */
|
|
272
|
+
slowlogLen: () => Promise<number>;
|
|
273
|
+
/** Drop this tenant's slowlog entries. Returns the number dropped. */
|
|
274
|
+
slowlogReset: () => Promise<number>;
|
|
275
|
+
/**
|
|
276
|
+
* Read runtime tunables. Pass a glob pattern to filter; default `*`
|
|
277
|
+
* returns every registered parameter.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* const all = await client.server.configGet();
|
|
281
|
+
* const memory = await client.server.configGet("max*");
|
|
282
|
+
*/
|
|
283
|
+
configGet: (pattern?: string) => Promise<Array<{
|
|
284
|
+
name: string;
|
|
285
|
+
value: string;
|
|
286
|
+
}>>;
|
|
287
|
+
/**
|
|
288
|
+
* Update a runtime tunable.
|
|
289
|
+
*
|
|
290
|
+
* Server-side, this command is only accepted from the calling tenant
|
|
291
|
+
* if it matches the server's `ERIX_ADMIN_TENANT_ID`. Other tenants
|
|
292
|
+
* receive an error result. Resolves to `true` on success and throws
|
|
293
|
+
* on failure (matching `client.cmd` semantics).
|
|
294
|
+
*/
|
|
295
|
+
configSet: (name: string, value: string | number) => Promise<true>;
|
|
296
|
+
/**
|
|
297
|
+
* Trigger a non-blocking snapshot save (admin tenant only).
|
|
298
|
+
* Resolves immediately with the server's reply once the work is
|
|
299
|
+
* dispatched — actual persistence happens in the background.
|
|
300
|
+
*/
|
|
301
|
+
bgsave: () => Promise<string>;
|
|
302
|
+
/**
|
|
303
|
+
* Rewrite the AOF in place (admin tenant only).
|
|
304
|
+
*
|
|
305
|
+
* The server walks the live in-memory state and emits a compact
|
|
306
|
+
* sequence of writes equivalent to it, then atomically replaces
|
|
307
|
+
* the on-disk log. The verb is admin-gated because the rewrite
|
|
308
|
+
* cost is bounded by the number of live keys.
|
|
309
|
+
*/
|
|
310
|
+
bgrewriteaof: () => Promise<string>;
|
|
311
|
+
};
|
|
134
312
|
/**
|
|
135
313
|
* Store any JSON value under a key.
|
|
136
314
|
* @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
|
|
@@ -142,6 +320,38 @@ export declare class ErixClient {
|
|
|
142
320
|
get<T = JsonValue>(key: string): Promise<T | null>;
|
|
143
321
|
/** Delete a key */
|
|
144
322
|
del(key: string): Promise<void>;
|
|
323
|
+
/**
|
|
324
|
+
* Atomic increment. Creates the counter at 1 when missing.
|
|
325
|
+
* Throws on non-integer values or overflow.
|
|
326
|
+
*/
|
|
327
|
+
incr(key: string): Promise<number>;
|
|
328
|
+
/** Atomic decrement. Creates the counter at -1 when missing. */
|
|
329
|
+
decr(key: string): Promise<number>;
|
|
330
|
+
/** Atomic increment by `delta`. */
|
|
331
|
+
incrBy(key: string, delta: number): Promise<number>;
|
|
332
|
+
/** Atomic decrement by `delta`. */
|
|
333
|
+
decrBy(key: string, delta: number): Promise<number>;
|
|
334
|
+
/** Append `value` to the existing string at `key`. Returns the new length. */
|
|
335
|
+
append(key: string, value: string): Promise<number>;
|
|
336
|
+
/** Length of the string at `key`. 0 for missing keys. */
|
|
337
|
+
strlen(key: string): Promise<number>;
|
|
338
|
+
/**
|
|
339
|
+
* Set multiple keys atomically. Either all writes succeed or none do
|
|
340
|
+
* (e.g. on WRONGTYPE for any of the target keys). Clears any prior TTL.
|
|
341
|
+
*/
|
|
342
|
+
mset(entries: Record<string, JsonValue>): Promise<void>;
|
|
343
|
+
/** Get multiple keys at once. Missing/non-string keys come back as `null`. */
|
|
344
|
+
mget<T = JsonValue>(keys: string[]): Promise<Array<T | null>>;
|
|
345
|
+
/**
|
|
346
|
+
* Atomic compare-and-set: returns the previous value (or `null`) and
|
|
347
|
+
* stores `value`. Clears any prior TTL.
|
|
348
|
+
*/
|
|
349
|
+
getSet<T = JsonValue>(key: string, value: JsonValue): Promise<T | null>;
|
|
350
|
+
/**
|
|
351
|
+
* Set only if the key does not already exist. Returns true on success,
|
|
352
|
+
* false when the key was already taken (any type — no WRONGTYPE).
|
|
353
|
+
*/
|
|
354
|
+
setNx(key: string, value: JsonValue): Promise<boolean>;
|
|
145
355
|
hash: {
|
|
146
356
|
/** Set a field in a hash */
|
|
147
357
|
hset: (key: string, field: string, value: JsonValue) => Promise<void>;
|
|
@@ -149,19 +359,95 @@ export declare class ErixClient {
|
|
|
149
359
|
hget: <T = JsonValue>(key: string, field: string) => Promise<T | null>;
|
|
150
360
|
/** Get all fields of a hash */
|
|
151
361
|
hgetall: (key: string) => Promise<Record<string, string>>;
|
|
362
|
+
/** Set multiple fields atomically (HMSET). */
|
|
363
|
+
hmset: (key: string, fields: Record<string, JsonValue>) => Promise<void>;
|
|
364
|
+
/** Get multiple fields at once. Missing fields return `null`. */
|
|
365
|
+
hmget: <T = JsonValue>(key: string, fields: string[]) => Promise<Array<T | null>>;
|
|
366
|
+
/** Whether `field` exists in the hash. */
|
|
367
|
+
hexists: (key: string, field: string) => Promise<boolean>;
|
|
368
|
+
/** All field names in the hash (no order guarantee). */
|
|
369
|
+
hkeys: (key: string) => Promise<string[]>;
|
|
370
|
+
/** All field values in the hash (no order guarantee). */
|
|
371
|
+
hvals: (key: string) => Promise<string[]>;
|
|
372
|
+
/** Number of fields in the hash. */
|
|
373
|
+
hlen: (key: string) => Promise<number>;
|
|
374
|
+
/** Delete one or more fields. Returns the count actually removed. */
|
|
375
|
+
hdel: (key: string, ...fields: string[]) => Promise<number>;
|
|
376
|
+
/**
|
|
377
|
+
* Atomic increment of a hash field by `delta`. Creates the field
|
|
378
|
+
* at `delta` when missing. Throws on non-integer values.
|
|
379
|
+
*/
|
|
380
|
+
hincrBy: (key: string, field: string, delta: number) => Promise<number>;
|
|
152
381
|
};
|
|
153
382
|
list: {
|
|
154
|
-
/** Prepend a value to a list */
|
|
155
|
-
lpush: (key: string, value: JsonValue) => Promise<
|
|
156
|
-
/** Append a value to a list */
|
|
157
|
-
rpush: (key: string, value: JsonValue) => Promise<
|
|
383
|
+
/** Prepend a value to a list. Returns the new list length. */
|
|
384
|
+
lpush: (key: string, value: JsonValue) => Promise<number>;
|
|
385
|
+
/** Append a value to a list. Returns the new list length. */
|
|
386
|
+
rpush: (key: string, value: JsonValue) => Promise<number>;
|
|
158
387
|
/** Pop from the left (FIFO front) */
|
|
159
388
|
lpop: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
389
|
+
/** Pop from the right (LIFO back) */
|
|
390
|
+
rpop: <T = JsonValue>(key: string) => Promise<T | null>;
|
|
391
|
+
/** List length. Returns 0 for missing keys. */
|
|
392
|
+
llen: (key: string) => Promise<number>;
|
|
393
|
+
/**
|
|
394
|
+
* Element at `index` (negative indices count from the tail).
|
|
395
|
+
* Returns `null` if out of bounds.
|
|
396
|
+
*/
|
|
397
|
+
lindex: <T = JsonValue>(key: string, index: number) => Promise<T | null>;
|
|
398
|
+
/**
|
|
399
|
+
* Inclusive range from `start` to `stop`, supporting negative indices
|
|
400
|
+
* (e.g. -1 = last element). Values stored as JSON are parsed; raw strings
|
|
401
|
+
* pass through unchanged.
|
|
402
|
+
*/
|
|
403
|
+
lrange: <T = JsonValue>(key: string, start: number, stop: number) => Promise<T[]>;
|
|
404
|
+
/**
|
|
405
|
+
* Remove up to `count` occurrences of `value`.
|
|
406
|
+
* count > 0 → remove from head
|
|
407
|
+
* count < 0 → remove from tail
|
|
408
|
+
* count = 0 → remove all
|
|
409
|
+
* Returns the number of elements removed.
|
|
410
|
+
*/
|
|
411
|
+
lrem: (key: string, count: number, value: JsonValue) => Promise<number>;
|
|
412
|
+
/**
|
|
413
|
+
* Trim the list to the inclusive range [start, stop].
|
|
414
|
+
* Negative indices count from the tail. Drops the key if the range
|
|
415
|
+
* leaves the list empty.
|
|
416
|
+
*/
|
|
417
|
+
ltrim: (key: string, start: number, stop: number) => Promise<void>;
|
|
160
418
|
};
|
|
161
419
|
queue: {
|
|
162
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Enqueue a value with optional priority. Higher priority dequeues first;
|
|
422
|
+
* ties resolve FIFO.
|
|
423
|
+
*
|
|
424
|
+
* The value is JSON-serialized server-side; pass any JSON-compatible
|
|
425
|
+
* shape and `dequeue` returns the parsed result.
|
|
426
|
+
*
|
|
427
|
+
* @returns the new queue length
|
|
428
|
+
*/
|
|
429
|
+
enqueue: (name: string, value: JsonValue, priority?: number) => Promise<number>;
|
|
430
|
+
/**
|
|
431
|
+
* Pop the highest-priority entry. Returns `null` when the queue is empty.
|
|
432
|
+
*/
|
|
433
|
+
dequeue: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
434
|
+
/** Inspect the next entry without removing it. */
|
|
435
|
+
peek: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
436
|
+
/** Number of entries in the queue. 0 for empty/missing queues. */
|
|
437
|
+
len: (name: string) => Promise<number>;
|
|
438
|
+
/** Drop every entry. Returns the number cleared. */
|
|
439
|
+
clear: (name: string) => Promise<number>;
|
|
440
|
+
/**
|
|
441
|
+
* Legacy push — kept for source compatibility with SDK 1.2.x. Forwards
|
|
442
|
+
* to {@link enqueue} with priority 0. Prefer `enqueue` in new code.
|
|
443
|
+
* @deprecated Use `enqueue` instead.
|
|
444
|
+
*/
|
|
163
445
|
push: (name: string, data: JsonValue) => Promise<void>;
|
|
164
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* Legacy pop — kept for source compatibility with SDK 1.2.x. Forwards
|
|
448
|
+
* to {@link dequeue}. Prefer `dequeue` in new code.
|
|
449
|
+
* @deprecated Use `dequeue` instead.
|
|
450
|
+
*/
|
|
165
451
|
pop: <T = JsonValue>(name: string) => Promise<T | null>;
|
|
166
452
|
};
|
|
167
453
|
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
@@ -204,8 +490,12 @@ export declare class ErixClient {
|
|
|
204
490
|
};
|
|
205
491
|
};
|
|
206
492
|
pubsub: {
|
|
207
|
-
/**
|
|
208
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Publish a message to a channel.
|
|
495
|
+
* @returns the number of subscribers that received the message
|
|
496
|
+
* (exact + pattern combined).
|
|
497
|
+
*/
|
|
498
|
+
publish: (channel: string, message: JsonValue) => Promise<number>;
|
|
209
499
|
/**
|
|
210
500
|
* Subscribe to a pub/sub channel via Server-Sent Events.
|
|
211
501
|
* Returns a controller with a `.close()` method.
|
|
@@ -217,6 +507,31 @@ export declare class ErixClient {
|
|
|
217
507
|
subscribe: (channel: string, onMessage: (message: JsonValue) => void, onError?: (err: Error) => void) => {
|
|
218
508
|
close: () => void;
|
|
219
509
|
};
|
|
510
|
+
/**
|
|
511
|
+
* Pattern subscribe (PSUBSCRIBE). The handler is invoked once per
|
|
512
|
+
* message, with the matched channel name (tenant prefix stripped).
|
|
513
|
+
*
|
|
514
|
+
* Supports the same glob syntax as `client.server.keys`:
|
|
515
|
+
* `*`, `?`, `[abc]`, `[^abc]`, `[a-z]`, `\\<char>`.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* const sub = client.pubsub.psubscribe("alerts:*", (msg, channel) => {
|
|
519
|
+
* console.log(`[${channel}]`, msg);
|
|
520
|
+
* });
|
|
521
|
+
* sub.close();
|
|
522
|
+
*/
|
|
523
|
+
psubscribe: (pattern: string, onMessage: (message: JsonValue, channel: string) => void, onError?: (err: Error) => void) => {
|
|
524
|
+
close: () => void;
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* List exact-subscriber channels in this tenant's namespace,
|
|
528
|
+
* optionally filtered by a glob pattern (default `*`).
|
|
529
|
+
*/
|
|
530
|
+
channels: (pattern?: string) => Promise<string[]>;
|
|
531
|
+
/** Per-channel exact-subscriber counts (0 for channels with none). */
|
|
532
|
+
numsub: (channels: string[]) => Promise<Record<string, number>>;
|
|
533
|
+
/** Number of distinct active patterns (server-wide; matches Redis). */
|
|
534
|
+
numpat: () => Promise<number>;
|
|
220
535
|
};
|
|
221
536
|
/**
|
|
222
537
|
* Check if an operation is within rate limits.
|
|
@@ -232,6 +547,40 @@ export declare class ErixClient {
|
|
|
232
547
|
}>;
|
|
233
548
|
/** Get all members of a set */
|
|
234
549
|
smembers: (key: string) => Promise<string[]>;
|
|
550
|
+
/** Remove one or more members. Returns the count actually removed. */
|
|
551
|
+
srem: (key: string, ...members: string[]) => Promise<number>;
|
|
552
|
+
/** Whether `member` is in the set. */
|
|
553
|
+
sismember: (key: string, member: string) => Promise<boolean>;
|
|
554
|
+
/** Number of members in the set. */
|
|
555
|
+
scard: (key: string) => Promise<number>;
|
|
556
|
+
/** Intersection across the supplied sets. */
|
|
557
|
+
sinter: (...keys: string[]) => Promise<string[]>;
|
|
558
|
+
/** Union across the supplied sets. */
|
|
559
|
+
sunion: (...keys: string[]) => Promise<string[]>;
|
|
560
|
+
/** Members in the first set that are not in any of the others. */
|
|
561
|
+
sdiff: (...keys: string[]) => Promise<string[]>;
|
|
562
|
+
};
|
|
563
|
+
zset: {
|
|
564
|
+
/** Add or update a member's score. Returns the count of newly added members. */
|
|
565
|
+
zadd: (key: string, score: number, member: string) => Promise<number>;
|
|
566
|
+
/** Inclusive range by 0-based rank (lowest score first). Negative indices supported. */
|
|
567
|
+
zrange: (key: string, start: number, stop: number) => Promise<string[]>;
|
|
568
|
+
/** Same as zrange but in descending score order. */
|
|
569
|
+
zrevrange: (key: string, start: number, stop: number) => Promise<string[]>;
|
|
570
|
+
/** Members whose score is in [min, max], ascending. */
|
|
571
|
+
zrangeByScore: (key: string, min: number, max: number) => Promise<string[]>;
|
|
572
|
+
/** Score for a member; `null` if missing. */
|
|
573
|
+
zscore: (key: string, member: string) => Promise<number | null>;
|
|
574
|
+
/** 0-based rank of `member`; `null` if missing. */
|
|
575
|
+
zrank: (key: string, member: string) => Promise<number | null>;
|
|
576
|
+
/** Number of members in the sorted set. */
|
|
577
|
+
zcard: (key: string) => Promise<number>;
|
|
578
|
+
/** Number of members whose score is in [min, max]. */
|
|
579
|
+
zcount: (key: string, min: number, max: number) => Promise<number>;
|
|
580
|
+
/** Increment a member's score by `delta`. Returns the new score. */
|
|
581
|
+
zincrBy: (key: string, delta: number, member: string) => Promise<number>;
|
|
582
|
+
/** Remove one or more members. Returns the count actually removed. */
|
|
583
|
+
zrem: (key: string, ...members: string[]) => Promise<number>;
|
|
235
584
|
};
|
|
236
585
|
cache: {
|
|
237
586
|
/** Get a cached value */
|
|
@@ -330,3 +679,51 @@ export declare class ErixClient {
|
|
|
330
679
|
*/
|
|
331
680
|
private openEventStream;
|
|
332
681
|
}
|
|
682
|
+
/** Wire shape for a single command inside a transaction. */
|
|
683
|
+
export interface TransactionCommand {
|
|
684
|
+
/** Verb name, case-insensitive. */
|
|
685
|
+
name: string;
|
|
686
|
+
/** Positional arguments, mostly mirroring Redis's argument order. */
|
|
687
|
+
args: unknown[];
|
|
688
|
+
}
|
|
689
|
+
/** Result of a single command — `ok: true` carries the return value. */
|
|
690
|
+
export type TransactionResult = {
|
|
691
|
+
ok: true;
|
|
692
|
+
value: unknown;
|
|
693
|
+
} | {
|
|
694
|
+
ok: false;
|
|
695
|
+
error: string;
|
|
696
|
+
code?: string;
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* Builder returned by {@link ErixClient.multi}.
|
|
700
|
+
*
|
|
701
|
+
* Chain `.cmd(name, ...args)` for each command, then `.exec()` to run
|
|
702
|
+
* them atomically server-side, or `.discard()` to drop the batch
|
|
703
|
+
* locally without sending a request.
|
|
704
|
+
*/
|
|
705
|
+
export declare class Transaction {
|
|
706
|
+
private readonly client;
|
|
707
|
+
private commands;
|
|
708
|
+
private finalized;
|
|
709
|
+
constructor(client: ErixClient);
|
|
710
|
+
/** Number of commands queued. */
|
|
711
|
+
get size(): number;
|
|
712
|
+
/**
|
|
713
|
+
* Queue a command. Returns `this` for chaining.
|
|
714
|
+
*
|
|
715
|
+
* The verb is validated server-side; unknown verbs come back as a
|
|
716
|
+
* per-command error in the result array, not a thrown exception.
|
|
717
|
+
*/
|
|
718
|
+
cmd(name: string, ...args: unknown[]): this;
|
|
719
|
+
/** Drop the queued commands locally. No server request is issued. */
|
|
720
|
+
discard(): void;
|
|
721
|
+
/**
|
|
722
|
+
* Send the queued commands to the server and run them atomically.
|
|
723
|
+
*
|
|
724
|
+
* Per-command errors are captured into the result array; the call as
|
|
725
|
+
* a whole still resolves successfully unless the network or auth
|
|
726
|
+
* layer fails.
|
|
727
|
+
*/
|
|
728
|
+
exec(): Promise<TransactionResult[]>;
|
|
729
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* import { ErixClient } from '@ecodrix/erix-client'
|
|
11
11
|
*
|
|
12
12
|
* const store = new ErixClient({
|
|
13
|
-
* baseUrl: 'https://
|
|
13
|
+
* baseUrl: 'https://store.ecodrix.com',
|
|
14
14
|
* apiKey: process.env.ERIX_API_KEY!,
|
|
15
15
|
* tenantId: 'org_abc123',
|
|
16
16
|
* })
|
|
@@ -26,6 +26,145 @@ export { WebSocketTransport } from "./transports/WebSocketTransport.js";
|
|
|
26
26
|
// ─── Client ─────────────────────────────────────────────────────────────────
|
|
27
27
|
export class ErixClient {
|
|
28
28
|
constructor(options) {
|
|
29
|
+
// ─── Server Commands (Redis-style) ────────────────────────────────────
|
|
30
|
+
this.server = {
|
|
31
|
+
/** Round-trip ping. Resolves to `true` on success. */
|
|
32
|
+
ping: async () => {
|
|
33
|
+
const res = await this.req("GET", "/server/ping");
|
|
34
|
+
return res.pong === true;
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Server-wide info — version, memory, total keys, and a per-tenant
|
|
38
|
+
* key breakdown for the calling tenant.
|
|
39
|
+
*/
|
|
40
|
+
info: async () => {
|
|
41
|
+
return this.req("GET", "/server/info");
|
|
42
|
+
},
|
|
43
|
+
/** Number of keys in the calling tenant's namespace. */
|
|
44
|
+
dbsize: async () => {
|
|
45
|
+
const res = await this.req("GET", "/server/dbsize");
|
|
46
|
+
return res.size;
|
|
47
|
+
},
|
|
48
|
+
/** Whether `key` exists in the calling tenant's namespace. */
|
|
49
|
+
exists: async (key) => {
|
|
50
|
+
const res = await this.req("GET", "/server/exists", undefined, { key });
|
|
51
|
+
return res.exists;
|
|
52
|
+
},
|
|
53
|
+
/** The data type of `key`, or `null` if it does not exist. */
|
|
54
|
+
type: async (key) => {
|
|
55
|
+
const res = await this.req("GET", "/server/type", undefined, { key });
|
|
56
|
+
return res.type;
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* List keys matching a glob pattern. Tenant-scoped — never returns
|
|
60
|
+
* keys from other tenants. Default pattern `*` returns every key.
|
|
61
|
+
*
|
|
62
|
+
* Supported glob syntax: `*`, `?`, `[abc]`, `[^abc]`, `[a-z]`, `\\<char>`.
|
|
63
|
+
*/
|
|
64
|
+
keys: async (pattern = "*") => {
|
|
65
|
+
const res = await this.req("GET", "/server/keys", undefined, { pattern });
|
|
66
|
+
return res.keys;
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Wipe every key in the calling tenant's namespace.
|
|
70
|
+
* Returns the number of keys deleted. There is intentionally no
|
|
71
|
+
* `flushall` — tenants cannot affect each other.
|
|
72
|
+
*/
|
|
73
|
+
flushdb: async () => {
|
|
74
|
+
const res = await this.req("POST", "/server/flushdb");
|
|
75
|
+
return res.flushed;
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* Set a TTL (in seconds) on an existing key.
|
|
79
|
+
* @returns true if the TTL was applied, false if the key didn't exist.
|
|
80
|
+
*/
|
|
81
|
+
expire: async (key, ttlSeconds) => {
|
|
82
|
+
const res = await this.req("POST", "/server/expire", {
|
|
83
|
+
key,
|
|
84
|
+
ttl: ttlSeconds,
|
|
85
|
+
});
|
|
86
|
+
return res.applied;
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* Remaining TTL in seconds. Redis convention:
|
|
90
|
+
* -2 → key does not exist
|
|
91
|
+
* -1 → key exists but has no TTL
|
|
92
|
+
* >0 → seconds remaining
|
|
93
|
+
*/
|
|
94
|
+
ttl: async (key) => {
|
|
95
|
+
const res = await this.req("GET", "/server/ttl", undefined, { key });
|
|
96
|
+
return res.ttl;
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Remove the TTL from a key. Returns true if a TTL was removed,
|
|
100
|
+
* false if the key did not exist or had no TTL.
|
|
101
|
+
*/
|
|
102
|
+
persist: async (key) => {
|
|
103
|
+
const res = await this.req("POST", "/server/persist", { key });
|
|
104
|
+
return res.removed;
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Read the slow-command log for the calling tenant. Newest entries
|
|
108
|
+
* first. Pass `count` to cap the result.
|
|
109
|
+
*
|
|
110
|
+
* Each entry's `args` are truncated server-side (max 32 args, 128
|
|
111
|
+
* chars each) so an MSET/HSET with a giant payload doesn't bloat
|
|
112
|
+
* the log buffer.
|
|
113
|
+
*/
|
|
114
|
+
slowlogGet: async (count = 128) => {
|
|
115
|
+
return this.cmd("SLOWLOG", "GET", count);
|
|
116
|
+
},
|
|
117
|
+
/** Number of slowlog entries currently stored for this tenant. */
|
|
118
|
+
slowlogLen: async () => {
|
|
119
|
+
return this.cmd("SLOWLOG", "LEN");
|
|
120
|
+
},
|
|
121
|
+
/** Drop this tenant's slowlog entries. Returns the number dropped. */
|
|
122
|
+
slowlogReset: async () => {
|
|
123
|
+
return this.cmd("SLOWLOG", "RESET");
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Read runtime tunables. Pass a glob pattern to filter; default `*`
|
|
127
|
+
* returns every registered parameter.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const all = await client.server.configGet();
|
|
131
|
+
* const memory = await client.server.configGet("max*");
|
|
132
|
+
*/
|
|
133
|
+
configGet: async (pattern = "*") => {
|
|
134
|
+
return this.cmd("CONFIG", "GET", pattern);
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* Update a runtime tunable.
|
|
138
|
+
*
|
|
139
|
+
* Server-side, this command is only accepted from the calling tenant
|
|
140
|
+
* if it matches the server's `ERIX_ADMIN_TENANT_ID`. Other tenants
|
|
141
|
+
* receive an error result. Resolves to `true` on success and throws
|
|
142
|
+
* on failure (matching `client.cmd` semantics).
|
|
143
|
+
*/
|
|
144
|
+
configSet: async (name, value) => {
|
|
145
|
+
await this.cmd("CONFIG", "SET", name, String(value));
|
|
146
|
+
return true;
|
|
147
|
+
},
|
|
148
|
+
/**
|
|
149
|
+
* Trigger a non-blocking snapshot save (admin tenant only).
|
|
150
|
+
* Resolves immediately with the server's reply once the work is
|
|
151
|
+
* dispatched — actual persistence happens in the background.
|
|
152
|
+
*/
|
|
153
|
+
bgsave: async () => {
|
|
154
|
+
return this.cmd("BGSAVE");
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Rewrite the AOF in place (admin tenant only).
|
|
158
|
+
*
|
|
159
|
+
* The server walks the live in-memory state and emits a compact
|
|
160
|
+
* sequence of writes equivalent to it, then atomically replaces
|
|
161
|
+
* the on-disk log. The verb is admin-gated because the rewrite
|
|
162
|
+
* cost is bounded by the number of live keys.
|
|
163
|
+
*/
|
|
164
|
+
bgrewriteaof: async () => {
|
|
165
|
+
return this.cmd("BGREWRITEAOF");
|
|
166
|
+
},
|
|
167
|
+
};
|
|
29
168
|
// ─── Hash ─────────────────────────────────────────────────────────────
|
|
30
169
|
this.hash = {
|
|
31
170
|
/** Set a field in a hash */
|
|
@@ -53,22 +192,74 @@ export class ErixClient {
|
|
|
53
192
|
const data = await this.req("GET", "/hash/hgetall", undefined, { key });
|
|
54
193
|
return data.data;
|
|
55
194
|
},
|
|
195
|
+
/** Set multiple fields atomically (HMSET). */
|
|
196
|
+
hmset: async (key, fields) => {
|
|
197
|
+
const args = [key];
|
|
198
|
+
for (const [f, v] of Object.entries(fields)) {
|
|
199
|
+
args.push(f, JSON.stringify(v));
|
|
200
|
+
}
|
|
201
|
+
await this.cmd("HMSET", ...args);
|
|
202
|
+
},
|
|
203
|
+
/** Get multiple fields at once. Missing fields return `null`. */
|
|
204
|
+
hmget: async (key, fields) => {
|
|
205
|
+
const raw = await this.cmd("HMGET", key, ...fields);
|
|
206
|
+
return raw.map((v) => {
|
|
207
|
+
if (v === null)
|
|
208
|
+
return null;
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(v);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return v;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
/** Whether `field` exists in the hash. */
|
|
218
|
+
hexists: async (key, field) => {
|
|
219
|
+
const r = await this.cmd("HEXISTS", key, field);
|
|
220
|
+
return r === 1;
|
|
221
|
+
},
|
|
222
|
+
/** All field names in the hash (no order guarantee). */
|
|
223
|
+
hkeys: async (key) => {
|
|
224
|
+
return this.cmd("HKEYS", key);
|
|
225
|
+
},
|
|
226
|
+
/** All field values in the hash (no order guarantee). */
|
|
227
|
+
hvals: async (key) => {
|
|
228
|
+
return this.cmd("HVALS", key);
|
|
229
|
+
},
|
|
230
|
+
/** Number of fields in the hash. */
|
|
231
|
+
hlen: async (key) => {
|
|
232
|
+
return this.cmd("HLEN", key);
|
|
233
|
+
},
|
|
234
|
+
/** Delete one or more fields. Returns the count actually removed. */
|
|
235
|
+
hdel: async (key, ...fields) => {
|
|
236
|
+
return this.cmd("HDEL", key, ...fields);
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* Atomic increment of a hash field by `delta`. Creates the field
|
|
240
|
+
* at `delta` when missing. Throws on non-integer values.
|
|
241
|
+
*/
|
|
242
|
+
hincrBy: async (key, field, delta) => {
|
|
243
|
+
return this.cmd("HINCRBY", key, field, delta);
|
|
244
|
+
},
|
|
56
245
|
};
|
|
57
246
|
// ─── List ─────────────────────────────────────────────────────────────
|
|
58
247
|
this.list = {
|
|
59
|
-
/** Prepend a value to a list */
|
|
248
|
+
/** Prepend a value to a list. Returns the new list length. */
|
|
60
249
|
lpush: async (key, value) => {
|
|
61
|
-
await this.req("POST", "/list/lpush", {
|
|
250
|
+
const res = await this.req("POST", "/list/lpush", {
|
|
62
251
|
key,
|
|
63
252
|
value: JSON.stringify(value),
|
|
64
253
|
});
|
|
254
|
+
return res.length;
|
|
65
255
|
},
|
|
66
|
-
/** Append a value to a list */
|
|
256
|
+
/** Append a value to a list. Returns the new list length. */
|
|
67
257
|
rpush: async (key, value) => {
|
|
68
|
-
await this.req("POST", "/list/rpush", {
|
|
258
|
+
const res = await this.req("POST", "/list/rpush", {
|
|
69
259
|
key,
|
|
70
260
|
value: JSON.stringify(value),
|
|
71
261
|
});
|
|
262
|
+
return res.length;
|
|
72
263
|
},
|
|
73
264
|
/** Pop from the left (FIFO front) */
|
|
74
265
|
lpop: async (key) => {
|
|
@@ -82,16 +273,148 @@ export class ErixClient {
|
|
|
82
273
|
return data.value;
|
|
83
274
|
}
|
|
84
275
|
},
|
|
276
|
+
/** Pop from the right (LIFO back) */
|
|
277
|
+
rpop: async (key) => {
|
|
278
|
+
const data = await this.req("GET", "/list/rpop", undefined, { key });
|
|
279
|
+
if (!data.value)
|
|
280
|
+
return null;
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(data.value);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return data.value;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
/** List length. Returns 0 for missing keys. */
|
|
289
|
+
llen: async (key) => {
|
|
290
|
+
const data = await this.req("GET", "/list/llen", undefined, { key });
|
|
291
|
+
return data.length;
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* Element at `index` (negative indices count from the tail).
|
|
295
|
+
* Returns `null` if out of bounds.
|
|
296
|
+
*/
|
|
297
|
+
lindex: async (key, index) => {
|
|
298
|
+
const data = await this.req("GET", "/list/lindex", undefined, { key, index: String(index) });
|
|
299
|
+
if (!data.value)
|
|
300
|
+
return null;
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(data.value);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return data.value;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
/**
|
|
309
|
+
* Inclusive range from `start` to `stop`, supporting negative indices
|
|
310
|
+
* (e.g. -1 = last element). Values stored as JSON are parsed; raw strings
|
|
311
|
+
* pass through unchanged.
|
|
312
|
+
*/
|
|
313
|
+
lrange: async (key, start, stop) => {
|
|
314
|
+
const data = await this.req("GET", "/list/lrange", undefined, { key, start: String(start), stop: String(stop) });
|
|
315
|
+
return data.values.map((v) => {
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(v);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return v;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
/**
|
|
325
|
+
* Remove up to `count` occurrences of `value`.
|
|
326
|
+
* count > 0 → remove from head
|
|
327
|
+
* count < 0 → remove from tail
|
|
328
|
+
* count = 0 → remove all
|
|
329
|
+
* Returns the number of elements removed.
|
|
330
|
+
*/
|
|
331
|
+
lrem: async (key, count, value) => {
|
|
332
|
+
const data = await this.req("POST", "/list/lrem", {
|
|
333
|
+
key,
|
|
334
|
+
count,
|
|
335
|
+
value: JSON.stringify(value),
|
|
336
|
+
});
|
|
337
|
+
return data.removed;
|
|
338
|
+
},
|
|
339
|
+
/**
|
|
340
|
+
* Trim the list to the inclusive range [start, stop].
|
|
341
|
+
* Negative indices count from the tail. Drops the key if the range
|
|
342
|
+
* leaves the list empty.
|
|
343
|
+
*/
|
|
344
|
+
ltrim: async (key, start, stop) => {
|
|
345
|
+
await this.req("POST", "/list/ltrim", { key, start, stop });
|
|
346
|
+
},
|
|
85
347
|
};
|
|
86
348
|
// ─── Queue (FIFO job queue, built on List) ────────────────────────────
|
|
87
349
|
this.queue = {
|
|
88
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Enqueue a value with optional priority. Higher priority dequeues first;
|
|
352
|
+
* ties resolve FIFO.
|
|
353
|
+
*
|
|
354
|
+
* The value is JSON-serialized server-side; pass any JSON-compatible
|
|
355
|
+
* shape and `dequeue` returns the parsed result.
|
|
356
|
+
*
|
|
357
|
+
* @returns the new queue length
|
|
358
|
+
*/
|
|
359
|
+
enqueue: async (name, value, priority = 0) => {
|
|
360
|
+
const res = await this.req("POST", "/q/enqueue", { name, value: JSON.stringify(value), priority });
|
|
361
|
+
return res.length;
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* Pop the highest-priority entry. Returns `null` when the queue is empty.
|
|
365
|
+
*/
|
|
366
|
+
dequeue: async (name) => {
|
|
367
|
+
const res = await this.req("POST", "/q/dequeue", { name });
|
|
368
|
+
if (res.value === null)
|
|
369
|
+
return null;
|
|
370
|
+
try {
|
|
371
|
+
return JSON.parse(res.value);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return res.value;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
/** Inspect the next entry without removing it. */
|
|
378
|
+
peek: async (name) => {
|
|
379
|
+
const res = await this.req("GET", "/q/peek", undefined, { name });
|
|
380
|
+
if (res.value === null)
|
|
381
|
+
return null;
|
|
382
|
+
try {
|
|
383
|
+
return JSON.parse(res.value);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return res.value;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
/** Number of entries in the queue. 0 for empty/missing queues. */
|
|
390
|
+
len: async (name) => {
|
|
391
|
+
const res = await this.req("GET", "/q/len", undefined, { name });
|
|
392
|
+
return res.length;
|
|
393
|
+
},
|
|
394
|
+
/** Drop every entry. Returns the number cleared. */
|
|
395
|
+
clear: async (name) => {
|
|
396
|
+
const res = await this.req("POST", "/q/clear", { name });
|
|
397
|
+
return res.cleared;
|
|
398
|
+
},
|
|
399
|
+
/**
|
|
400
|
+
* Legacy push — kept for source compatibility with SDK 1.2.x. Forwards
|
|
401
|
+
* to {@link enqueue} with priority 0. Prefer `enqueue` in new code.
|
|
402
|
+
* @deprecated Use `enqueue` instead.
|
|
403
|
+
*/
|
|
89
404
|
push: async (name, data) => {
|
|
90
|
-
await this.
|
|
405
|
+
await this.req("POST", "/q/enqueue", {
|
|
406
|
+
name,
|
|
407
|
+
value: JSON.stringify(data),
|
|
408
|
+
priority: 0,
|
|
409
|
+
});
|
|
91
410
|
},
|
|
92
|
-
/**
|
|
411
|
+
/**
|
|
412
|
+
* Legacy pop — kept for source compatibility with SDK 1.2.x. Forwards
|
|
413
|
+
* to {@link dequeue}. Prefer `dequeue` in new code.
|
|
414
|
+
* @deprecated Use `dequeue` instead.
|
|
415
|
+
*/
|
|
93
416
|
pop: async (name) => {
|
|
94
|
-
return this.
|
|
417
|
+
return this.queue.dequeue(name);
|
|
95
418
|
},
|
|
96
419
|
};
|
|
97
420
|
/** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
|
|
@@ -164,9 +487,14 @@ export class ErixClient {
|
|
|
164
487
|
};
|
|
165
488
|
// ─── PubSub ───────────────────────────────────────────────────────────
|
|
166
489
|
this.pubsub = {
|
|
167
|
-
/**
|
|
490
|
+
/**
|
|
491
|
+
* Publish a message to a channel.
|
|
492
|
+
* @returns the number of subscribers that received the message
|
|
493
|
+
* (exact + pattern combined).
|
|
494
|
+
*/
|
|
168
495
|
publish: async (channel, message) => {
|
|
169
|
-
await this.req("POST", "/pubsub/publish", { channel, message });
|
|
496
|
+
const res = await this.req("POST", "/pubsub/publish", { channel, message });
|
|
497
|
+
return res.delivered;
|
|
170
498
|
},
|
|
171
499
|
/**
|
|
172
500
|
* Subscribe to a pub/sub channel via Server-Sent Events.
|
|
@@ -179,6 +507,43 @@ export class ErixClient {
|
|
|
179
507
|
subscribe: (channel, onMessage, onError) => {
|
|
180
508
|
return this.openEventStream(`/pubsub/${encodeURIComponent(channel)}/stream`, (_event, data) => onMessage(data), onError);
|
|
181
509
|
},
|
|
510
|
+
/**
|
|
511
|
+
* Pattern subscribe (PSUBSCRIBE). The handler is invoked once per
|
|
512
|
+
* message, with the matched channel name (tenant prefix stripped).
|
|
513
|
+
*
|
|
514
|
+
* Supports the same glob syntax as `client.server.keys`:
|
|
515
|
+
* `*`, `?`, `[abc]`, `[^abc]`, `[a-z]`, `\\<char>`.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* const sub = client.pubsub.psubscribe("alerts:*", (msg, channel) => {
|
|
519
|
+
* console.log(`[${channel}]`, msg);
|
|
520
|
+
* });
|
|
521
|
+
* sub.close();
|
|
522
|
+
*/
|
|
523
|
+
psubscribe: (pattern, onMessage, onError) => {
|
|
524
|
+
return this.openEventStream(`/pubsub/p/${encodeURIComponent(pattern)}/stream`, (_event, data) => {
|
|
525
|
+
const payload = data;
|
|
526
|
+
onMessage(payload.message, payload.channel);
|
|
527
|
+
}, onError);
|
|
528
|
+
},
|
|
529
|
+
/**
|
|
530
|
+
* List exact-subscriber channels in this tenant's namespace,
|
|
531
|
+
* optionally filtered by a glob pattern (default `*`).
|
|
532
|
+
*/
|
|
533
|
+
channels: async (pattern = "*") => {
|
|
534
|
+
const res = await this.req("GET", "/pubsub/channels", undefined, { pattern });
|
|
535
|
+
return res.channels;
|
|
536
|
+
},
|
|
537
|
+
/** Per-channel exact-subscriber counts (0 for channels with none). */
|
|
538
|
+
numsub: async (channels) => {
|
|
539
|
+
const res = await this.req("POST", "/pubsub/numsub", { channels });
|
|
540
|
+
return res.counts;
|
|
541
|
+
},
|
|
542
|
+
/** Number of distinct active patterns (server-wide; matches Redis). */
|
|
543
|
+
numpat: async () => {
|
|
544
|
+
const res = await this.req("GET", "/pubsub/numpat");
|
|
545
|
+
return res.count;
|
|
546
|
+
},
|
|
182
547
|
};
|
|
183
548
|
// ─── Set ──────────────────────────────────────────────────────────────
|
|
184
549
|
this.set_ = {
|
|
@@ -191,6 +556,74 @@ export class ErixClient {
|
|
|
191
556
|
const data = await this.req("GET", "/set/smembers", undefined, { key });
|
|
192
557
|
return data.members;
|
|
193
558
|
},
|
|
559
|
+
/** Remove one or more members. Returns the count actually removed. */
|
|
560
|
+
srem: async (key, ...members) => {
|
|
561
|
+
return this.cmd("SREM", key, ...members);
|
|
562
|
+
},
|
|
563
|
+
/** Whether `member` is in the set. */
|
|
564
|
+
sismember: async (key, member) => {
|
|
565
|
+
const r = await this.cmd("SISMEMBER", key, member);
|
|
566
|
+
return r === 1;
|
|
567
|
+
},
|
|
568
|
+
/** Number of members in the set. */
|
|
569
|
+
scard: async (key) => {
|
|
570
|
+
return this.cmd("SCARD", key);
|
|
571
|
+
},
|
|
572
|
+
/** Intersection across the supplied sets. */
|
|
573
|
+
sinter: async (...keys) => {
|
|
574
|
+
return this.cmd("SINTER", ...keys);
|
|
575
|
+
},
|
|
576
|
+
/** Union across the supplied sets. */
|
|
577
|
+
sunion: async (...keys) => {
|
|
578
|
+
return this.cmd("SUNION", ...keys);
|
|
579
|
+
},
|
|
580
|
+
/** Members in the first set that are not in any of the others. */
|
|
581
|
+
sdiff: async (...keys) => {
|
|
582
|
+
return this.cmd("SDIFF", ...keys);
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
// ─── Sorted Set ───────────────────────────────────────────────────────
|
|
586
|
+
this.zset = {
|
|
587
|
+
/** Add or update a member's score. Returns the count of newly added members. */
|
|
588
|
+
zadd: async (key, score, member) => {
|
|
589
|
+
return this.cmd("ZADD", key, score, member);
|
|
590
|
+
},
|
|
591
|
+
/** Inclusive range by 0-based rank (lowest score first). Negative indices supported. */
|
|
592
|
+
zrange: async (key, start, stop) => {
|
|
593
|
+
return this.cmd("ZRANGE", key, start, stop);
|
|
594
|
+
},
|
|
595
|
+
/** Same as zrange but in descending score order. */
|
|
596
|
+
zrevrange: async (key, start, stop) => {
|
|
597
|
+
return this.cmd("ZREVRANGE", key, start, stop);
|
|
598
|
+
},
|
|
599
|
+
/** Members whose score is in [min, max], ascending. */
|
|
600
|
+
zrangeByScore: async (key, min, max) => {
|
|
601
|
+
return this.cmd("ZRANGEBYSCORE", key, min, max);
|
|
602
|
+
},
|
|
603
|
+
/** Score for a member; `null` if missing. */
|
|
604
|
+
zscore: async (key, member) => {
|
|
605
|
+
return this.cmd("ZSCORE", key, member);
|
|
606
|
+
},
|
|
607
|
+
/** 0-based rank of `member`; `null` if missing. */
|
|
608
|
+
zrank: async (key, member) => {
|
|
609
|
+
return this.cmd("ZRANK", key, member);
|
|
610
|
+
},
|
|
611
|
+
/** Number of members in the sorted set. */
|
|
612
|
+
zcard: async (key) => {
|
|
613
|
+
return this.cmd("ZCARD", key);
|
|
614
|
+
},
|
|
615
|
+
/** Number of members whose score is in [min, max]. */
|
|
616
|
+
zcount: async (key, min, max) => {
|
|
617
|
+
return this.cmd("ZCOUNT", key, min, max);
|
|
618
|
+
},
|
|
619
|
+
/** Increment a member's score by `delta`. Returns the new score. */
|
|
620
|
+
zincrBy: async (key, delta, member) => {
|
|
621
|
+
return this.cmd("ZINCRBY", key, delta, member);
|
|
622
|
+
},
|
|
623
|
+
/** Remove one or more members. Returns the count actually removed. */
|
|
624
|
+
zrem: async (key, ...members) => {
|
|
625
|
+
return this.cmd("ZREM", key, ...members);
|
|
626
|
+
},
|
|
194
627
|
};
|
|
195
628
|
// ─── Cache (advanced) ────────────────────────────────────────────────
|
|
196
629
|
this.cache = {
|
|
@@ -390,6 +823,68 @@ export class ErixClient {
|
|
|
390
823
|
async pipeline(requests) {
|
|
391
824
|
return this.transport.pipeline(requests);
|
|
392
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Begin a new transaction.
|
|
828
|
+
*
|
|
829
|
+
* Returns a builder you can queue commands onto with `.cmd(name, ...args)`,
|
|
830
|
+
* then send with `.exec()`. Every queued command runs atomically inside
|
|
831
|
+
* a single event-loop tick on the server — no other client request can
|
|
832
|
+
* interleave between them.
|
|
833
|
+
*
|
|
834
|
+
* Per-command failures are captured into the result array but do not
|
|
835
|
+
* abort the batch (matching Redis MULTI/EXEC).
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```ts
|
|
839
|
+
* const results = await client
|
|
840
|
+
* .multi()
|
|
841
|
+
* .cmd("SET", "k", "v")
|
|
842
|
+
* .cmd("EXPIRE", "k", 60)
|
|
843
|
+
* .cmd("GET", "k")
|
|
844
|
+
* .exec();
|
|
845
|
+
* // → [{ ok: true, value: "OK" }, { ok: true, value: 1 }, { ok: true, value: "v" }]
|
|
846
|
+
* ```
|
|
847
|
+
*/
|
|
848
|
+
multi() {
|
|
849
|
+
return new Transaction(this);
|
|
850
|
+
}
|
|
851
|
+
/** Alias for {@link multi}. */
|
|
852
|
+
tx() {
|
|
853
|
+
return this.multi();
|
|
854
|
+
}
|
|
855
|
+
/** @internal — called by Transaction.exec(). Keeps tx wire format private. */
|
|
856
|
+
async _execTransaction(commands) {
|
|
857
|
+
const res = await this.req("POST", "/tx/exec", { commands });
|
|
858
|
+
return res.results;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Run a single Redis-style command synchronously.
|
|
862
|
+
*
|
|
863
|
+
* Lightweight escape hatch for verbs we have not surfaced as named
|
|
864
|
+
* methods. Forwards the call through `/tx/exec` as a one-element
|
|
865
|
+
* transaction and unwraps the per-command result.
|
|
866
|
+
*
|
|
867
|
+
* Throws on per-command errors (`WRONGTYPE`, unknown verb, validation)
|
|
868
|
+
* so it composes naturally with `try`/`catch`. For multi-command
|
|
869
|
+
* batches use {@link multi} instead — that surface keeps each command's
|
|
870
|
+
* error isolated.
|
|
871
|
+
*
|
|
872
|
+
* @example
|
|
873
|
+
* ```ts
|
|
874
|
+
* const newLen = await client.cmd<number>("APPEND", "log", "entry");
|
|
875
|
+
* const all = await client.cmd<Record<string,string>>("HGETALL", "user");
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
async cmd(name, ...args) {
|
|
879
|
+
const [result] = await this._execTransaction([{ name, args }]);
|
|
880
|
+
if (!result.ok) {
|
|
881
|
+
const err = new Error(`[erix-store] ${name}: ${result.error}`);
|
|
882
|
+
if (result.code)
|
|
883
|
+
err.code = result.code;
|
|
884
|
+
throw err;
|
|
885
|
+
}
|
|
886
|
+
return result.value;
|
|
887
|
+
}
|
|
393
888
|
/**
|
|
394
889
|
* Close the transport and release resources.
|
|
395
890
|
*/
|
|
@@ -431,6 +926,81 @@ export class ErixClient {
|
|
|
431
926
|
async del(key) {
|
|
432
927
|
await this.req("DELETE", "/core/del", { key });
|
|
433
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Atomic increment. Creates the counter at 1 when missing.
|
|
931
|
+
* Throws on non-integer values or overflow.
|
|
932
|
+
*/
|
|
933
|
+
async incr(key) {
|
|
934
|
+
return this.cmd("INCR", key);
|
|
935
|
+
}
|
|
936
|
+
/** Atomic decrement. Creates the counter at -1 when missing. */
|
|
937
|
+
async decr(key) {
|
|
938
|
+
return this.cmd("DECR", key);
|
|
939
|
+
}
|
|
940
|
+
/** Atomic increment by `delta`. */
|
|
941
|
+
async incrBy(key, delta) {
|
|
942
|
+
return this.cmd("INCRBY", key, delta);
|
|
943
|
+
}
|
|
944
|
+
/** Atomic decrement by `delta`. */
|
|
945
|
+
async decrBy(key, delta) {
|
|
946
|
+
return this.cmd("DECRBY", key, delta);
|
|
947
|
+
}
|
|
948
|
+
/** Append `value` to the existing string at `key`. Returns the new length. */
|
|
949
|
+
async append(key, value) {
|
|
950
|
+
return this.cmd("APPEND", key, value);
|
|
951
|
+
}
|
|
952
|
+
/** Length of the string at `key`. 0 for missing keys. */
|
|
953
|
+
async strlen(key) {
|
|
954
|
+
return this.cmd("STRLEN", key);
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Set multiple keys atomically. Either all writes succeed or none do
|
|
958
|
+
* (e.g. on WRONGTYPE for any of the target keys). Clears any prior TTL.
|
|
959
|
+
*/
|
|
960
|
+
async mset(entries) {
|
|
961
|
+
const args = [];
|
|
962
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
963
|
+
args.push(k, JSON.stringify(v));
|
|
964
|
+
}
|
|
965
|
+
await this.cmd("MSET", ...args);
|
|
966
|
+
}
|
|
967
|
+
/** Get multiple keys at once. Missing/non-string keys come back as `null`. */
|
|
968
|
+
async mget(keys) {
|
|
969
|
+
const raw = await this.cmd("MGET", ...keys);
|
|
970
|
+
return raw.map((v) => {
|
|
971
|
+
if (v === null)
|
|
972
|
+
return null;
|
|
973
|
+
try {
|
|
974
|
+
return JSON.parse(v);
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
return v;
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Atomic compare-and-set: returns the previous value (or `null`) and
|
|
983
|
+
* stores `value`. Clears any prior TTL.
|
|
984
|
+
*/
|
|
985
|
+
async getSet(key, value) {
|
|
986
|
+
const prev = await this.cmd("GETSET", key, JSON.stringify(value));
|
|
987
|
+
if (prev === null)
|
|
988
|
+
return null;
|
|
989
|
+
try {
|
|
990
|
+
return JSON.parse(prev);
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return prev;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Set only if the key does not already exist. Returns true on success,
|
|
998
|
+
* false when the key was already taken (any type — no WRONGTYPE).
|
|
999
|
+
*/
|
|
1000
|
+
async setNx(key, value) {
|
|
1001
|
+
const r = await this.cmd("SETNX", key, JSON.stringify(value));
|
|
1002
|
+
return r === 1;
|
|
1003
|
+
}
|
|
434
1004
|
// ─── Rate Limiter ─────────────────────────────────────────────────────
|
|
435
1005
|
/**
|
|
436
1006
|
* Check if an operation is within rate limits.
|
|
@@ -502,6 +1072,56 @@ export class ErixClient {
|
|
|
502
1072
|
return { close: () => controller.abort() };
|
|
503
1073
|
}
|
|
504
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Builder returned by {@link ErixClient.multi}.
|
|
1077
|
+
*
|
|
1078
|
+
* Chain `.cmd(name, ...args)` for each command, then `.exec()` to run
|
|
1079
|
+
* them atomically server-side, or `.discard()` to drop the batch
|
|
1080
|
+
* locally without sending a request.
|
|
1081
|
+
*/
|
|
1082
|
+
export class Transaction {
|
|
1083
|
+
constructor(client) {
|
|
1084
|
+
this.client = client;
|
|
1085
|
+
this.commands = [];
|
|
1086
|
+
this.finalized = false;
|
|
1087
|
+
}
|
|
1088
|
+
/** Number of commands queued. */
|
|
1089
|
+
get size() {
|
|
1090
|
+
return this.commands.length;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Queue a command. Returns `this` for chaining.
|
|
1094
|
+
*
|
|
1095
|
+
* The verb is validated server-side; unknown verbs come back as a
|
|
1096
|
+
* per-command error in the result array, not a thrown exception.
|
|
1097
|
+
*/
|
|
1098
|
+
cmd(name, ...args) {
|
|
1099
|
+
if (this.finalized) {
|
|
1100
|
+
throw new Error("[erix-store] Transaction already finalized — create a new one with client.multi()");
|
|
1101
|
+
}
|
|
1102
|
+
this.commands.push({ name, args });
|
|
1103
|
+
return this;
|
|
1104
|
+
}
|
|
1105
|
+
/** Drop the queued commands locally. No server request is issued. */
|
|
1106
|
+
discard() {
|
|
1107
|
+
this.finalized = true;
|
|
1108
|
+
this.commands = [];
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Send the queued commands to the server and run them atomically.
|
|
1112
|
+
*
|
|
1113
|
+
* Per-command errors are captured into the result array; the call as
|
|
1114
|
+
* a whole still resolves successfully unless the network or auth
|
|
1115
|
+
* layer fails.
|
|
1116
|
+
*/
|
|
1117
|
+
async exec() {
|
|
1118
|
+
if (this.finalized) {
|
|
1119
|
+
throw new Error("[erix-store] Transaction already finalized");
|
|
1120
|
+
}
|
|
1121
|
+
this.finalized = true;
|
|
1122
|
+
return this.client._execTransaction(this.commands);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
505
1125
|
// ─── AutoTransport (internal) ────────────────────────────────────────────
|
|
506
1126
|
/**
|
|
507
1127
|
* Internal transport that tries WebSocket first and falls back to HTTP
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecodrix/erix-client",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Type-safe HTTP client for
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Type-safe HTTP client for ERIX Store — serverless cache, queues, and real-time messaging. Zero dependencies, uses native fetch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"rate-limit",
|
|
23
23
|
"pubsub",
|
|
24
24
|
"ecodrix",
|
|
25
|
-
"
|
|
25
|
+
"serverless-data",
|
|
26
26
|
"in-memory"
|
|
27
27
|
],
|
|
28
28
|
"author": "dhanesh125",
|