@ecodrix/erix-client 1.0.5 → 1.3.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 CHANGED
@@ -19,6 +19,10 @@
19
19
  * const session = await store.get<{ role: string }>('session:user1')
20
20
  * ```
21
21
  */
22
+ import type { TransportLayer, TransportMode } from "./transports/TransportLayer.js";
23
+ export { HttpTransport } from "./transports/HttpTransport.js";
24
+ export { WebSocketTransport } from "./transports/WebSocketTransport.js";
25
+ export type { TransportLayer, TransportMode };
22
26
  /** Any value that can be serialized to and from JSON */
23
27
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
24
28
  [key: string]: JsonValue;
@@ -32,6 +36,13 @@ export interface ErixClientOptions {
32
36
  tenantId: string;
33
37
  /** Optional request timeout in milliseconds (default: 5000) */
34
38
  timeoutMs?: number;
39
+ /**
40
+ * Transport mode:
41
+ * - "http": Use HTTP only (original behavior)
42
+ * - "ws": Use WebSocket only
43
+ * - "auto": Attempt WebSocket first, fall back to HTTP if unavailable (default)
44
+ */
45
+ transport?: "http" | "ws" | "auto";
35
46
  }
36
47
  export interface RateLimitResult {
37
48
  allowed: boolean;
@@ -91,13 +102,213 @@ export declare class ErixClient {
91
102
  private readonly baseUrl;
92
103
  private readonly headers;
93
104
  private readonly timeoutMs;
105
+ private readonly transportMode;
106
+ private transport;
94
107
  constructor(options: ErixClientOptions);
108
+ /**
109
+ * Create the appropriate transport based on the configured mode.
110
+ * In "auto" mode, starts with WebSocket and falls back to HTTP on failure.
111
+ */
112
+ private createTransport;
95
113
  private req;
114
+ /**
115
+ * Execute multiple operations in a single round-trip (pipeline).
116
+ * Over WebSocket, this sends all requests in one frame.
117
+ * Over HTTP, requests are executed concurrently via Promise.all.
118
+ */
119
+ pipeline(requests: Array<{
120
+ method: string;
121
+ path: string;
122
+ body?: unknown;
123
+ params?: Record<string, string>;
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>;
171
+ /**
172
+ * Close the transport and release resources.
173
+ */
174
+ close(): void;
96
175
  /** Check if the erix-store instance is reachable */
97
176
  ping(): Promise<{
98
177
  status: string;
99
178
  uptime: number;
100
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
+ };
101
312
  /**
102
313
  * Store any JSON value under a key.
103
314
  * @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
@@ -109,6 +320,38 @@ export declare class ErixClient {
109
320
  get<T = JsonValue>(key: string): Promise<T | null>;
110
321
  /** Delete a key */
111
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>;
112
355
  hash: {
113
356
  /** Set a field in a hash */
114
357
  hset: (key: string, field: string, value: JsonValue) => Promise<void>;
@@ -116,19 +359,95 @@ export declare class ErixClient {
116
359
  hget: <T = JsonValue>(key: string, field: string) => Promise<T | null>;
117
360
  /** Get all fields of a hash */
118
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>;
119
381
  };
120
382
  list: {
121
- /** Prepend a value to a list */
122
- lpush: (key: string, value: JsonValue) => Promise<void>;
123
- /** Append a value to a list */
124
- rpush: (key: string, value: JsonValue) => Promise<void>;
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>;
125
387
  /** Pop from the left (FIFO front) */
126
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>;
127
418
  };
128
419
  queue: {
129
- /** Enqueue a job payload into a named queue (simple list-based) */
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
+ */
130
445
  push: (name: string, data: JsonValue) => Promise<void>;
131
- /** Dequeue the oldest job from a named queue. Returns `null` if empty. */
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
+ */
132
451
  pop: <T = JsonValue>(name: string) => Promise<T | null>;
133
452
  };
134
453
  /** Advanced Queue (v2) — supports priority, delay, retries, heartbeat, and SSE push */
@@ -171,8 +490,12 @@ export declare class ErixClient {
171
490
  };
172
491
  };
173
492
  pubsub: {
174
- /** Publish a message to a channel */
175
- publish: (channel: string, message: JsonValue) => Promise<void>;
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>;
176
499
  /**
177
500
  * Subscribe to a pub/sub channel via Server-Sent Events.
178
501
  * Returns a controller with a `.close()` method.
@@ -184,6 +507,31 @@ export declare class ErixClient {
184
507
  subscribe: (channel: string, onMessage: (message: JsonValue) => void, onError?: (err: Error) => void) => {
185
508
  close: () => void;
186
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>;
187
535
  };
188
536
  /**
189
537
  * Check if an operation is within rate limits.
@@ -199,6 +547,40 @@ export declare class ErixClient {
199
547
  }>;
200
548
  /** Get all members of a set */
201
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>;
202
584
  };
203
585
  cache: {
204
586
  /** Get a cached value */
@@ -297,3 +679,51 @@ export declare class ErixClient {
297
679
  */
298
680
  private openEventStream;
299
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
+ }