@ecodrix/erix-client 1.0.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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @ecodrix/erix-client
2
+
3
+ Type-safe HTTP client for [erix-store](../README.md) — the ECODrIx in-memory data sidecar.
4
+
5
+ **Zero extra dependencies.** Uses native `fetch` (Node 18+).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @ecodrix/erix-client
11
+ # or: copy client/src/index.ts directly into your project
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```ts
17
+ import { ErixClient } from '@ecodrix/erix-client'
18
+
19
+ const store = new ErixClient({
20
+ baseUrl: process.env.ERIX_STORE_URL!, // https://erix-store.onrender.com
21
+ apiKey: process.env.ERIX_STORE_API_KEY!, // erix_xxxxxxxxxxxxxxxx
22
+ tenantId: req.user.orgId, // your tenant namespace
23
+ })
24
+
25
+ // Key / Value
26
+ await store.set('user:42', { name: 'Dhanesh' }, 3600) // TTL optional
27
+ const user = await store.get<User>('user:42')
28
+
29
+ // Queue
30
+ await store.queue.push('send-email', { to: 'a@b.com', template: 'welcome' })
31
+ const job = await store.queue.pop<EmailJob>('send-email')
32
+
33
+ // Rate limiter
34
+ const { allowed } = await store.rateLimit(`api:${orgId}`, 100, 60)
35
+
36
+ // Hash
37
+ await store.hash.hset('settings:org1', 'theme', 'dark')
38
+ const theme = await store.hash.hget<string>('settings:org1', 'theme')
39
+
40
+ // Health
41
+ const { status } = await store.ping()
42
+ ```
43
+
44
+ See [PRODUCTION.md](../PRODUCTION.md) for full patterns, multi-tenant setup, and troubleshooting.
45
+
46
+ ## API
47
+
48
+ | Method | Description |
49
+ |---|---|
50
+ | `store.set(key, value, ttl?)` | Store a JSON value with optional TTL in seconds |
51
+ | `store.get<T>(key)` | Retrieve value or `null` if missing/expired |
52
+ | `store.del(key)` | Delete a key |
53
+ | `store.hash.hset(key, field, value)` | Set a hash field |
54
+ | `store.hash.hget<T>(key, field)` | Get one hash field |
55
+ | `store.hash.hgetall(key)` | Get all fields of a hash |
56
+ | `store.list.lpush(key, value)` | Prepend to a list |
57
+ | `store.list.rpush(key, value)` | Append to a list |
58
+ | `store.list.lpop<T>(key)` | Pop from list front |
59
+ | `store.queue.push(name, data)` | Enqueue a job |
60
+ | `store.queue.pop<T>(name)` | Dequeue a job (`null` if empty) |
61
+ | `store.pubsub.publish(channel, msg)` | Publish an event |
62
+ | `store.rateLimit(key, limit, window)` | Check rate limit |
63
+ | `store.set_.sadd(key, member)` | Add to a set |
64
+ | `store.set_.smembers(key)` | Get all set members |
65
+ | `store.ping()` | Health check |
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @ecodrix/erix-client
3
+ *
4
+ * Type-safe HTTP client for the erix-store in-memory data service.
5
+ * Connects to a running erix-store instance without requiring the
6
+ * consumer to know any of the underlying HTTP route structure.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { ErixClient } from '@ecodrix/erix-client'
11
+ *
12
+ * const store = new ErixClient({
13
+ * baseUrl: 'https://erix-store.onrender.com',
14
+ * apiKey: process.env.ERIX_API_KEY!,
15
+ * tenantId: 'org_abc123',
16
+ * })
17
+ *
18
+ * await store.set('session:user1', { role: 'admin' }, 3600)
19
+ * const session = await store.get<{ role: string }>('session:user1')
20
+ * ```
21
+ */
22
+ /** Any value that can be serialized to and from JSON */
23
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
24
+ [key: string]: JsonValue;
25
+ };
26
+ export interface ErixClientOptions {
27
+ /** Full URL of the deployed erix-store instance, e.g. https://erix-store.onrender.com */
28
+ baseUrl: string;
29
+ /** Shared secret generated at deploy time — must match ERIX_API_KEY on the server */
30
+ apiKey: string;
31
+ /** Tenant namespace — all keys are automatically namespaced under this prefix */
32
+ tenantId: string;
33
+ /** Optional request timeout in milliseconds (default: 5000) */
34
+ timeoutMs?: number;
35
+ }
36
+ export interface RateLimitResult {
37
+ allowed: boolean;
38
+ remaining: number;
39
+ resetAt: number;
40
+ }
41
+ export declare class ErixClient {
42
+ private readonly baseUrl;
43
+ private readonly headers;
44
+ private readonly timeoutMs;
45
+ constructor(options: ErixClientOptions);
46
+ private req;
47
+ /** Check if the erix-store instance is reachable */
48
+ ping(): Promise<{
49
+ status: string;
50
+ uptime: number;
51
+ }>;
52
+ /**
53
+ * Store any JSON value under a key.
54
+ * @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
55
+ */
56
+ set(key: string, value: JsonValue, ttlSeconds?: number): Promise<void>;
57
+ /**
58
+ * Retrieve a value by key. Returns `null` if the key doesn't exist or has expired.
59
+ */
60
+ get<T = JsonValue>(key: string): Promise<T | null>;
61
+ /** Delete a key */
62
+ del(key: string): Promise<void>;
63
+ hash: {
64
+ /** Set a field in a hash */
65
+ hset: (key: string, field: string, value: JsonValue) => Promise<void>;
66
+ /** Get a single field from a hash */
67
+ hget: <T = JsonValue>(key: string, field: string) => Promise<T | null>;
68
+ /** Get all fields of a hash */
69
+ hgetall: (key: string) => Promise<Record<string, string>>;
70
+ };
71
+ list: {
72
+ /** Prepend a value to a list */
73
+ lpush: (key: string, value: JsonValue) => Promise<void>;
74
+ /** Append a value to a list */
75
+ rpush: (key: string, value: JsonValue) => Promise<void>;
76
+ /** Pop from the left (FIFO front) */
77
+ lpop: <T = JsonValue>(key: string) => Promise<T | null>;
78
+ };
79
+ queue: {
80
+ /** Enqueue a job payload into a named queue */
81
+ push: (name: string, data: JsonValue) => Promise<void>;
82
+ /** Dequeue the oldest job from a named queue. Returns `null` if empty. */
83
+ pop: <T = JsonValue>(name: string) => Promise<T | null>;
84
+ };
85
+ pubsub: {
86
+ /** Publish a message to a channel */
87
+ publish: (channel: string, message: JsonValue) => Promise<void>;
88
+ };
89
+ /**
90
+ * Check if an operation is within rate limits.
91
+ * @param key Identifier (e.g. `"api:org_123"`)
92
+ * @param limit Maximum allowed operations in the window
93
+ * @param window Window duration in seconds
94
+ */
95
+ rateLimit(key: string, limit: number, window: number): Promise<RateLimitResult>;
96
+ set_: {
97
+ /** Add a member to a set */
98
+ sadd: (key: string, value: string) => Promise<{
99
+ added: number;
100
+ }>;
101
+ /** Get all members of a set */
102
+ smembers: (key: string) => Promise<string[]>;
103
+ };
104
+ }
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @ecodrix/erix-client
3
+ *
4
+ * Type-safe HTTP client for the erix-store in-memory data service.
5
+ * Connects to a running erix-store instance without requiring the
6
+ * consumer to know any of the underlying HTTP route structure.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { ErixClient } from '@ecodrix/erix-client'
11
+ *
12
+ * const store = new ErixClient({
13
+ * baseUrl: 'https://erix-store.onrender.com',
14
+ * apiKey: process.env.ERIX_API_KEY!,
15
+ * tenantId: 'org_abc123',
16
+ * })
17
+ *
18
+ * await store.set('session:user1', { role: 'admin' }, 3600)
19
+ * const session = await store.get<{ role: string }>('session:user1')
20
+ * ```
21
+ */
22
+ // ─── Client ─────────────────────────────────────────────────────────────────
23
+ export class ErixClient {
24
+ constructor(options) {
25
+ // ─── Hash ─────────────────────────────────────────────────────────────
26
+ this.hash = {
27
+ /** Set a field in a hash */
28
+ hset: async (key, field, value) => {
29
+ await this.req("POST", "/hash/hset", {
30
+ key,
31
+ field,
32
+ value: JSON.stringify(value),
33
+ });
34
+ },
35
+ /** Get a single field from a hash */
36
+ hget: async (key, field) => {
37
+ const data = await this.req("GET", "/hash/hget", undefined, { key, field });
38
+ if (!data.value)
39
+ return null;
40
+ try {
41
+ return JSON.parse(data.value);
42
+ }
43
+ catch {
44
+ return data.value;
45
+ }
46
+ },
47
+ /** Get all fields of a hash */
48
+ hgetall: async (key) => {
49
+ const data = await this.req("GET", "/hash/hgetall", undefined, { key });
50
+ return data.data;
51
+ },
52
+ };
53
+ // ─── List ─────────────────────────────────────────────────────────────
54
+ this.list = {
55
+ /** Prepend a value to a list */
56
+ lpush: async (key, value) => {
57
+ await this.req("POST", "/list/lpush", {
58
+ key,
59
+ value: JSON.stringify(value),
60
+ });
61
+ },
62
+ /** Append a value to a list */
63
+ rpush: async (key, value) => {
64
+ await this.req("POST", "/list/rpush", {
65
+ key,
66
+ value: JSON.stringify(value),
67
+ });
68
+ },
69
+ /** Pop from the left (FIFO front) */
70
+ lpop: async (key) => {
71
+ const data = await this.req("GET", "/list/lpop", undefined, { key });
72
+ if (!data.value)
73
+ return null;
74
+ try {
75
+ return JSON.parse(data.value);
76
+ }
77
+ catch {
78
+ return data.value;
79
+ }
80
+ },
81
+ };
82
+ // ─── Queue (FIFO job queue, built on List) ────────────────────────────
83
+ this.queue = {
84
+ /** Enqueue a job payload into a named queue */
85
+ push: async (name, data) => {
86
+ await this.list.rpush(`q:${name}`, data);
87
+ },
88
+ /** Dequeue the oldest job from a named queue. Returns `null` if empty. */
89
+ pop: async (name) => {
90
+ return this.list.lpop(`q:${name}`);
91
+ },
92
+ };
93
+ // ─── PubSub ───────────────────────────────────────────────────────────
94
+ this.pubsub = {
95
+ /** Publish a message to a channel */
96
+ publish: async (channel, message) => {
97
+ await this.req("POST", "/pubsub/publish", { channel, message });
98
+ },
99
+ };
100
+ // ─── Set ──────────────────────────────────────────────────────────────
101
+ this.set_ = {
102
+ /** Add a member to a set */
103
+ sadd: async (key, value) => {
104
+ return this.req("POST", "/set/sadd", { key, value });
105
+ },
106
+ /** Get all members of a set */
107
+ smembers: async (key) => {
108
+ const data = await this.req("GET", "/set/smembers", undefined, { key });
109
+ return data.members;
110
+ },
111
+ };
112
+ this.baseUrl = options.baseUrl.replace(/\/$/, ""); // strip trailing slash
113
+ this.timeoutMs = options.timeoutMs ?? 5000;
114
+ this.headers = {
115
+ "x-erix-key": options.apiKey,
116
+ "x-tenant-id": options.tenantId,
117
+ "Content-Type": "application/json",
118
+ };
119
+ }
120
+ // ── Internal fetch helper ─────────────────────────────────────────────
121
+ async req(method, path, body, params) {
122
+ const url = new URL(`${this.baseUrl}${path}`);
123
+ if (params) {
124
+ for (const [k, v] of Object.entries(params)) {
125
+ url.searchParams.set(k, v);
126
+ }
127
+ }
128
+ const controller = new AbortController();
129
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
130
+ try {
131
+ const res = await fetch(url.toString(), {
132
+ method,
133
+ headers: this.headers,
134
+ body: body !== undefined ? JSON.stringify(body) : undefined,
135
+ signal: controller.signal,
136
+ });
137
+ if (!res.ok) {
138
+ const err = (await res.json().catch(() => ({})));
139
+ throw new Error(`[erix-store] ${method} ${path} → ${res.status}: ${err.error ?? res.statusText}`);
140
+ }
141
+ return (await res.json());
142
+ }
143
+ finally {
144
+ clearTimeout(timer);
145
+ }
146
+ }
147
+ // ─── Health ───────────────────────────────────────────────────────────
148
+ /** Check if the erix-store instance is reachable */
149
+ async ping() {
150
+ return this.req("GET", "/health");
151
+ }
152
+ // ─── Core: Key / Value ────────────────────────────────────────────────
153
+ /**
154
+ * Store any JSON value under a key.
155
+ * @param ttlSeconds Optional expiry in seconds. Omit for no expiry.
156
+ */
157
+ async set(key, value, ttlSeconds) {
158
+ await this.req("POST", "/core/set", {
159
+ key,
160
+ value: JSON.stringify(value),
161
+ ttl: ttlSeconds,
162
+ });
163
+ }
164
+ /**
165
+ * Retrieve a value by key. Returns `null` if the key doesn't exist or has expired.
166
+ */
167
+ async get(key) {
168
+ const data = await this.req("GET", "/core/get", undefined, { key });
169
+ if (!data.value)
170
+ return null;
171
+ try {
172
+ return JSON.parse(data.value);
173
+ }
174
+ catch {
175
+ return data.value;
176
+ }
177
+ }
178
+ /** Delete a key */
179
+ async del(key) {
180
+ await this.req("DELETE", "/core/del", { key });
181
+ }
182
+ // ─── Rate Limiter ─────────────────────────────────────────────────────
183
+ /**
184
+ * Check if an operation is within rate limits.
185
+ * @param key Identifier (e.g. `"api:org_123"`)
186
+ * @param limit Maximum allowed operations in the window
187
+ * @param window Window duration in seconds
188
+ */
189
+ async rateLimit(key, limit, window) {
190
+ return this.req("POST", "/ratelimit", {
191
+ key,
192
+ limit,
193
+ window,
194
+ });
195
+ }
196
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ecodrix/erix-client",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe HTTP client for the erix-store in-memory data service. Zero dependencies — uses native fetch.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "erix-store",
24
+ "cache",
25
+ "queue",
26
+ "rate-limit",
27
+ "pubsub",
28
+ "ecodrix",
29
+ "redis-alternative",
30
+ "in-memory"
31
+ ],
32
+ "author": "dhanesh125",
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "@types/node": "^20.12.7",
40
+ "typescript": "^5.4.5"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/dhanesh125/erix-store"
45
+ }
46
+ }