@luxdb/sdk 0.2.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/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@luxdb/sdk",
3
+ "version": "0.2.0",
4
+ "description": "TypeScript client for Lux key-value store",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/mattyhogan/lux"
12
+ },
13
+ "keywords": ["lux", "redis", "key-value", "database", "sdk"]
14
+ }
package/src/client.ts ADDED
@@ -0,0 +1,314 @@
1
+ import { Socket } from "net";
2
+ import type { LuxConfig, RespValue } from "./types";
3
+ import { LuxConnectionError, LuxError } from "./errors";
4
+ import { LuxSubscriber } from "./subscriber";
5
+
6
+ export class Lux {
7
+ private socket: Socket | null = null;
8
+ private host: string;
9
+ private port: number;
10
+ private connected = false;
11
+ private chunks: Buffer[] = [];
12
+ private buf: Buffer<ArrayBufferLike> = Buffer.alloc(0);
13
+ private waiting: ((chunk: Buffer<ArrayBufferLike>) => void) | null = null;
14
+ private queue: Promise<any> = Promise.resolve();
15
+
16
+ constructor(config: LuxConfig) {
17
+ this.host = config.host;
18
+ this.port = config.port || 6379;
19
+ }
20
+
21
+ async connect(): Promise<void> {
22
+ if (this.connected) return;
23
+
24
+ return new Promise((resolve, reject) => {
25
+ this.socket = new Socket();
26
+
27
+ this.socket.on("connect", () => {
28
+ this.connected = true;
29
+ resolve();
30
+ });
31
+
32
+ this.socket.on("data", (data: Buffer) => {
33
+ if (this.waiting) {
34
+ const cb = this.waiting;
35
+ this.waiting = null;
36
+ cb(data);
37
+ } else {
38
+ this.chunks.push(data);
39
+ }
40
+ });
41
+
42
+ this.socket.on("error", (err) => {
43
+ if (!this.connected) reject(new LuxConnectionError(err.message));
44
+ });
45
+
46
+ this.socket.on("close", () => {
47
+ this.connected = false;
48
+ });
49
+
50
+ this.socket.setNoDelay(true);
51
+ this.socket.connect(this.port, this.host);
52
+ });
53
+ }
54
+
55
+ createSubscriber(): LuxSubscriber {
56
+ return new LuxSubscriber({ host: this.host, port: this.port });
57
+ }
58
+
59
+ disconnect(): void {
60
+ if (this.socket) {
61
+ this.socket.destroy();
62
+ this.socket = null;
63
+ this.connected = false;
64
+ }
65
+ }
66
+
67
+ private async fillBuffer(): Promise<void> {
68
+ if (this.chunks.length > 0) {
69
+ const pending = Buffer.concat(this.chunks);
70
+ this.chunks.length = 0;
71
+ this.buf = this.buf.length > 0 ? Buffer.concat([this.buf, pending]) : pending;
72
+ return;
73
+ }
74
+
75
+ const chunk = await new Promise<Buffer>((resolve) => {
76
+ this.waiting = resolve;
77
+ });
78
+ this.buf = this.buf.length > 0 ? Buffer.concat([this.buf, chunk]) : chunk;
79
+ }
80
+
81
+ private consume(n: number): void {
82
+ this.buf = this.buf.subarray(n);
83
+ }
84
+
85
+ private async readLine(): Promise<string> {
86
+ while (true) {
87
+ const idx = this.buf.indexOf("\r\n");
88
+ if (idx !== -1) {
89
+ const line = this.buf.subarray(0, idx).toString();
90
+ this.consume(idx + 2);
91
+ return line;
92
+ }
93
+ await this.fillBuffer();
94
+ }
95
+ }
96
+
97
+ private async readExact(n: number): Promise<Buffer> {
98
+ while (this.buf.length < n) {
99
+ await this.fillBuffer();
100
+ }
101
+ const result = this.buf.subarray(0, n);
102
+ this.consume(n);
103
+ return result;
104
+ }
105
+
106
+ private async readReply(): Promise<RespValue> {
107
+ const line = await this.readLine();
108
+ const type = line[0];
109
+ const payload = line.slice(1);
110
+
111
+ switch (type) {
112
+ case "+":
113
+ return payload;
114
+ case "-":
115
+ throw new LuxError(payload);
116
+ case ":":
117
+ return parseInt(payload, 10);
118
+ case "$": {
119
+ const len = parseInt(payload, 10);
120
+ if (len === -1) return null;
121
+ const data = await this.readExact(len + 2);
122
+ return data.subarray(0, len).toString();
123
+ }
124
+ case "*": {
125
+ const count = parseInt(payload, 10);
126
+ if (count === -1) return null;
127
+ if (count === 0) return [];
128
+ const arr: RespValue[] = [];
129
+ for (let i = 0; i < count; i++) {
130
+ arr.push(await this.readReply());
131
+ }
132
+ return arr;
133
+ }
134
+ default:
135
+ throw new LuxError(`unknown RESP type: ${type}`);
136
+ }
137
+ }
138
+
139
+ private encode(args: (string | number)[]): string {
140
+ let out = `*${args.length}\r\n`;
141
+ for (const arg of args) {
142
+ const s = String(arg);
143
+ out += `$${Buffer.byteLength(s)}\r\n${s}\r\n`;
144
+ }
145
+ return out;
146
+ }
147
+
148
+ async command(...args: (string | number)[]): Promise<RespValue> {
149
+ return this.enqueue(() => {
150
+ if (!this.socket || !this.connected) {
151
+ throw new LuxConnectionError("not connected");
152
+ }
153
+ this.socket.write(this.encode(args));
154
+ return this.readReply();
155
+ });
156
+ }
157
+
158
+ async pipeline(commands: (string | number)[][]): Promise<RespValue[]> {
159
+ return this.enqueue(async () => {
160
+ if (!this.socket || !this.connected) {
161
+ throw new LuxConnectionError("not connected");
162
+ }
163
+
164
+ let encoded = "";
165
+ for (const args of commands) {
166
+ encoded += this.encode(args);
167
+ }
168
+ this.socket.write(encoded);
169
+
170
+ const results: RespValue[] = [];
171
+ for (let i = 0; i < commands.length; i++) {
172
+ try {
173
+ results.push(await this.readReply());
174
+ } catch (e) {
175
+ results.push(null);
176
+ }
177
+ }
178
+ return results;
179
+ });
180
+ }
181
+
182
+ private enqueue<T>(fn: () => Promise<T>): Promise<T> {
183
+ const p = this.queue.then(fn, fn);
184
+ this.queue = p.catch(() => {});
185
+ return p;
186
+ }
187
+
188
+ async set(key: string, value: string, ttl?: number): Promise<string> {
189
+ const args: (string | number)[] = ["SET", key, value];
190
+ if (ttl !== undefined) args.push("EX", ttl);
191
+ return (await this.command(...args)) as string;
192
+ }
193
+
194
+ async get(key: string): Promise<string | null> {
195
+ return (await this.command("GET", key)) as string | null;
196
+ }
197
+
198
+ async del(...keys: string[]): Promise<number> {
199
+ return (await this.command("DEL", ...keys)) as number;
200
+ }
201
+
202
+ async exists(...keys: string[]): Promise<number> {
203
+ return (await this.command("EXISTS", ...keys)) as number;
204
+ }
205
+
206
+ async incr(key: string): Promise<number> {
207
+ return (await this.command("INCR", key)) as number;
208
+ }
209
+
210
+ async decr(key: string): Promise<number> {
211
+ return (await this.command("DECR", key)) as number;
212
+ }
213
+
214
+ async incrby(key: string, delta: number): Promise<number> {
215
+ return (await this.command("INCRBY", key, delta)) as number;
216
+ }
217
+
218
+ async expire(key: string, seconds: number): Promise<number> {
219
+ return (await this.command("EXPIRE", key, seconds)) as number;
220
+ }
221
+
222
+ async ttl(key: string): Promise<number> {
223
+ return (await this.command("TTL", key)) as number;
224
+ }
225
+
226
+ async keys(pattern: string): Promise<string[]> {
227
+ return (await this.command("KEYS", pattern)) as string[];
228
+ }
229
+
230
+ async mset(...pairs: string[]): Promise<string> {
231
+ return (await this.command("MSET", ...pairs)) as string;
232
+ }
233
+
234
+ async mget(...keys: string[]): Promise<(string | null)[]> {
235
+ return (await this.command("MGET", ...keys)) as (string | null)[];
236
+ }
237
+
238
+ async lpush(key: string, ...values: string[]): Promise<number> {
239
+ return (await this.command("LPUSH", key, ...values)) as number;
240
+ }
241
+
242
+ async rpush(key: string, ...values: string[]): Promise<number> {
243
+ return (await this.command("RPUSH", key, ...values)) as number;
244
+ }
245
+
246
+ async lpop(key: string): Promise<string | null> {
247
+ return (await this.command("LPOP", key)) as string | null;
248
+ }
249
+
250
+ async rpop(key: string): Promise<string | null> {
251
+ return (await this.command("RPOP", key)) as string | null;
252
+ }
253
+
254
+ async llen(key: string): Promise<number> {
255
+ return (await this.command("LLEN", key)) as number;
256
+ }
257
+
258
+ async lrange(key: string, start: number, stop: number): Promise<string[]> {
259
+ return (await this.command("LRANGE", key, start, stop)) as string[];
260
+ }
261
+
262
+ async hset(key: string, ...fieldValues: string[]): Promise<number> {
263
+ return (await this.command("HSET", key, ...fieldValues)) as number;
264
+ }
265
+
266
+ async hget(key: string, field: string): Promise<string | null> {
267
+ return (await this.command("HGET", key, field)) as string | null;
268
+ }
269
+
270
+ async hdel(key: string, ...fields: string[]): Promise<number> {
271
+ return (await this.command("HDEL", key, ...fields)) as number;
272
+ }
273
+
274
+ async hgetall(key: string): Promise<Record<string, string>> {
275
+ const arr = (await this.command("HGETALL", key)) as string[];
276
+ const result: Record<string, string> = {};
277
+ for (let i = 0; i < arr.length; i += 2) {
278
+ result[arr[i]] = arr[i + 1];
279
+ }
280
+ return result;
281
+ }
282
+
283
+ async sadd(key: string, ...members: string[]): Promise<number> {
284
+ return (await this.command("SADD", key, ...members)) as number;
285
+ }
286
+
287
+ async srem(key: string, ...members: string[]): Promise<number> {
288
+ return (await this.command("SREM", key, ...members)) as number;
289
+ }
290
+
291
+ async smembers(key: string): Promise<string[]> {
292
+ return (await this.command("SMEMBERS", key)) as string[];
293
+ }
294
+
295
+ async sismember(key: string, member: string): Promise<number> {
296
+ return (await this.command("SISMEMBER", key, member)) as number;
297
+ }
298
+
299
+ async publish(channel: string, message: string): Promise<number> {
300
+ return (await this.command("PUBLISH", channel, message)) as number;
301
+ }
302
+
303
+ async dbsize(): Promise<number> {
304
+ return (await this.command("DBSIZE")) as number;
305
+ }
306
+
307
+ async flushdb(): Promise<string> {
308
+ return (await this.command("FLUSHDB")) as string;
309
+ }
310
+
311
+ async ping(): Promise<string> {
312
+ return (await this.command("PING")) as string;
313
+ }
314
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,13 @@
1
+ export class LuxError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "LuxError";
5
+ }
6
+ }
7
+
8
+ export class LuxConnectionError extends LuxError {
9
+ constructor(message: string) {
10
+ super(message);
11
+ this.name = "LuxConnectionError";
12
+ }
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { Lux } from "./client";
2
+ export { LuxSubscriber, LuxSubscriber as Subscriber } from "./subscriber";
3
+ export { LuxError, LuxConnectionError } from "./errors";
4
+ export type { LuxConfig, RespValue } from "./types";
@@ -0,0 +1,156 @@
1
+ import { Socket } from "net";
2
+ import type { LuxConfig } from "./types";
3
+ import { LuxConnectionError } from "./errors";
4
+
5
+ export class LuxSubscriber {
6
+ private socket: Socket | null = null;
7
+ private host: string;
8
+ private port: number;
9
+ private connected = false;
10
+ private buffer = "";
11
+ private listeners = new Map<string, Set<(channel: string, message: string) => void>>();
12
+
13
+ constructor(config: LuxConfig) {
14
+ this.host = config.host;
15
+ this.port = config.port || 6379;
16
+ }
17
+
18
+ async connect(): Promise<void> {
19
+ if (this.connected) return;
20
+
21
+ return new Promise((resolve, reject) => {
22
+ this.socket = new Socket();
23
+
24
+ this.socket.on("connect", () => {
25
+ this.connected = true;
26
+ resolve();
27
+ });
28
+
29
+ this.socket.on("data", (data) => {
30
+ this.buffer += data.toString();
31
+ this.processBuffer();
32
+ });
33
+
34
+ this.socket.on("error", (err) => {
35
+ if (!this.connected) {
36
+ reject(new LuxConnectionError(err.message));
37
+ }
38
+ });
39
+
40
+ this.socket.on("close", () => {
41
+ this.connected = false;
42
+ });
43
+
44
+ this.socket.setNoDelay(true);
45
+ this.socket.connect(this.port, this.host);
46
+ });
47
+ }
48
+
49
+ disconnect(): void {
50
+ if (this.socket) {
51
+ this.socket.destroy();
52
+ this.socket = null;
53
+ this.connected = false;
54
+ }
55
+ }
56
+
57
+ async subscribe(channel: string, callback: (channel: string, message: string) => void): Promise<void> {
58
+ if (!this.socket || !this.connected) {
59
+ throw new LuxConnectionError("not connected");
60
+ }
61
+
62
+ if (!this.listeners.has(channel)) {
63
+ this.listeners.set(channel, new Set());
64
+ const cmd = `*2\r\n$9\r\nSUBSCRIBE\r\n$${Buffer.byteLength(channel)}\r\n${channel}\r\n`;
65
+ this.socket.write(cmd);
66
+ }
67
+
68
+ this.listeners.get(channel)!.add(callback);
69
+ }
70
+
71
+ async unsubscribe(channel: string, callback?: (channel: string, message: string) => void): Promise<void> {
72
+ if (!this.listeners.has(channel)) return;
73
+
74
+ if (callback) {
75
+ this.listeners.get(channel)!.delete(callback);
76
+ if (this.listeners.get(channel)!.size === 0) {
77
+ this.listeners.delete(channel);
78
+ }
79
+ } else {
80
+ this.listeners.delete(channel);
81
+ }
82
+
83
+ if (!this.listeners.has(channel) && this.socket && this.connected) {
84
+ const cmd = `*2\r\n$11\r\nUNSUBSCRIBE\r\n$${Buffer.byteLength(channel)}\r\n${channel}\r\n`;
85
+ this.socket.write(cmd);
86
+ }
87
+ }
88
+
89
+ private processBuffer(): void {
90
+ while (this.buffer.length > 0) {
91
+ const result = this.parseArray(0);
92
+ if (result === null) break;
93
+
94
+ const [arr, consumed] = result;
95
+ this.buffer = this.buffer.slice(consumed);
96
+
97
+ if (Array.isArray(arr) && arr.length >= 3 && arr[0] === "message") {
98
+ const channel = arr[1] as string;
99
+ const message = arr[2] as string;
100
+ const callbacks = this.listeners.get(channel);
101
+ if (callbacks) {
102
+ for (const cb of callbacks) {
103
+ cb(channel, message);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ private parseArray(pos: number): [unknown[], number] | null {
111
+ if (pos >= this.buffer.length || this.buffer[pos] !== "*") return null;
112
+
113
+ const nl = this.buffer.indexOf("\r\n", pos);
114
+ if (nl === -1) return null;
115
+
116
+ const count = parseInt(this.buffer.slice(pos + 1, nl), 10);
117
+ if (count <= 0) return [[], nl + 2];
118
+
119
+ const arr: unknown[] = [];
120
+ let cursor = nl + 2;
121
+
122
+ for (let i = 0; i < count; i++) {
123
+ if (cursor >= this.buffer.length) return null;
124
+ const type = this.buffer[cursor];
125
+
126
+ if (type === "$") {
127
+ const bnl = this.buffer.indexOf("\r\n", cursor);
128
+ if (bnl === -1) return null;
129
+ const len = parseInt(this.buffer.slice(cursor + 1, bnl), 10);
130
+ if (len === -1) {
131
+ arr.push(null);
132
+ cursor = bnl + 2;
133
+ } else {
134
+ const end = bnl + 2 + len + 2;
135
+ if (end > this.buffer.length) return null;
136
+ arr.push(this.buffer.slice(bnl + 2, bnl + 2 + len));
137
+ cursor = end;
138
+ }
139
+ } else if (type === ":") {
140
+ const inl = this.buffer.indexOf("\r\n", cursor);
141
+ if (inl === -1) return null;
142
+ arr.push(parseInt(this.buffer.slice(cursor + 1, inl), 10));
143
+ cursor = inl + 2;
144
+ } else if (type === "+") {
145
+ const snl = this.buffer.indexOf("\r\n", cursor);
146
+ if (snl === -1) return null;
147
+ arr.push(this.buffer.slice(cursor + 1, snl));
148
+ cursor = snl + 2;
149
+ } else {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ return [arr, cursor];
155
+ }
156
+ }
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface LuxConfig {
2
+ host: string;
3
+ port?: number;
4
+ maxRetries?: number;
5
+ retryDelay?: number;
6
+ }
7
+
8
+ export type RespValue = string | number | null | RespValue[];
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src"]
16
+ }