@luxdb/sdk 1.0.0 → 1.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/dist/index.d.ts CHANGED
@@ -1,19 +1,93 @@
1
- import Redis, { RedisOptions } from 'ioredis';
1
+ import Redis, { type RedisOptions } from 'ioredis';
2
2
  export interface VSearchResult {
3
3
  key: string;
4
4
  similarity: number;
5
5
  metadata?: Record<string, unknown>;
6
6
  }
7
- export interface VSearchOptions {
8
- k: number;
9
- filter?: {
10
- key: string;
11
- value: string;
7
+ export interface TSSample {
8
+ timestamp: number;
9
+ value: number;
10
+ }
11
+ export interface TSAddOptions {
12
+ retention?: number;
13
+ labels?: Record<string, string>;
14
+ }
15
+ export interface TSRangeOptions {
16
+ aggregation?: {
17
+ type: 'avg' | 'sum' | 'min' | 'max' | 'count' | 'first' | 'last' | 'range' | 'std.p' | 'std.s' | 'var.p' | 'var.s';
18
+ bucketSize: number;
12
19
  };
13
- meta?: boolean;
20
+ }
21
+ export interface TSMRangeResult {
22
+ key: string;
23
+ labels: Record<string, string>;
24
+ samples: TSSample[];
25
+ }
26
+ export interface KSubEvent {
27
+ pattern: string;
28
+ key: string;
29
+ operation: string;
30
+ }
31
+ export interface TableRow {
32
+ id: number;
33
+ [field: string]: unknown;
34
+ }
35
+ declare class TableQueryBuilder {
36
+ private client;
37
+ private name;
38
+ private conditions;
39
+ private orderField?;
40
+ private orderDir?;
41
+ private limitCount?;
42
+ private joinTable?;
43
+ constructor(client: Lux, name: string);
44
+ where(field: string, op: '=' | '!=' | '>' | '<' | '>=' | '<=', value: string | number | boolean): this;
45
+ orderBy(field: string, dir?: 'asc' | 'desc'): this;
46
+ limit(n: number): this;
47
+ join(table: string): this;
48
+ run(): Promise<TableRow[]>;
49
+ insert(data: Record<string, unknown>): Promise<number>;
50
+ update(id: number, data: Record<string, unknown>): Promise<string>;
51
+ delete(...ids: number[]): Promise<number>;
52
+ }
53
+ declare class VectorNamespace {
54
+ private client;
55
+ constructor(client: Lux);
56
+ set(key: string, vector: number[], metadata?: Record<string, unknown>): Promise<string>;
57
+ get(key: string): Promise<{
58
+ dims: number;
59
+ vector: number[];
60
+ metadata?: Record<string, unknown>;
61
+ } | null>;
62
+ search(query: number[], options: {
63
+ topK: number;
64
+ filter?: {
65
+ key: string;
66
+ value: string;
67
+ };
68
+ meta?: boolean;
69
+ }): Promise<VSearchResult[]>;
70
+ count(): Promise<number>;
71
+ }
72
+ declare class TimeSeriesNamespace {
73
+ private client;
74
+ constructor(client: Lux);
75
+ add(key: string, value: number, options?: {
76
+ timestamp?: number | '*';
77
+ retention?: number;
78
+ labels?: Record<string, string>;
79
+ }): Promise<number>;
80
+ get(key: string): Promise<TSSample | null>;
81
+ range(key: string, from: number | '-', to: number | '+', options?: TSRangeOptions): Promise<TSSample[]>;
82
+ mrange(from: number | '-', to: number | '+', filter: string, options?: TSRangeOptions): Promise<TSMRangeResult[]>;
83
+ info(key: string): Promise<Record<string, unknown>>;
14
84
  }
15
85
  export declare class Lux extends Redis {
86
+ vectors: VectorNamespace;
87
+ timeseries: TimeSeriesNamespace;
16
88
  constructor(options?: RedisOptions | string);
89
+ table(name: string): TableQueryBuilder;
90
+ _tquery(args: string[]): Promise<TableRow[]>;
17
91
  vset(key: string, vector: number[], options?: {
18
92
  metadata?: Record<string, unknown>;
19
93
  ex?: number;
@@ -24,7 +98,24 @@ export declare class Lux extends Redis {
24
98
  vector: number[];
25
99
  metadata?: Record<string, unknown>;
26
100
  } | null>;
27
- vsearch(query: number[], options: VSearchOptions): Promise<VSearchResult[]>;
101
+ vsearch(query: number[], options: {
102
+ k: number;
103
+ filter?: {
104
+ key: string;
105
+ value: string;
106
+ };
107
+ meta?: boolean;
108
+ }): Promise<VSearchResult[]>;
28
109
  vcard(): Promise<number>;
110
+ tsadd(key: string, timestamp: number | '*', value: number, options?: TSAddOptions): Promise<number>;
111
+ tsmadd(...entries: [string, number | '*', number][]): Promise<string>;
112
+ tsget(key: string): Promise<TSSample | null>;
113
+ tsrange(key: string, from: number | '-', to: number | '+', options?: TSRangeOptions): Promise<TSSample[]>;
114
+ tsmrange(from: number | '-', to: number | '+', filter: string, options?: TSRangeOptions): Promise<TSMRangeResult[]>;
115
+ tsinfo(key: string): Promise<Record<string, unknown>>;
116
+ ksub(patterns: string[], handler: (event: KSubEvent) => void): {
117
+ unsubscribe: () => void;
118
+ connection: Redis;
119
+ };
29
120
  }
30
121
  export default Lux;
package/dist/index.js CHANGED
@@ -5,10 +5,139 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Lux = void 0;
7
7
  const ioredis_1 = __importDefault(require("ioredis"));
8
+ class TableQueryBuilder {
9
+ constructor(client, name) {
10
+ this.conditions = [];
11
+ this.client = client;
12
+ this.name = name;
13
+ }
14
+ where(field, op, value) {
15
+ const v = typeof value === 'string' ? `'${value}'` : String(value);
16
+ this.conditions.push(`${field} ${op} ${v}`);
17
+ return this;
18
+ }
19
+ orderBy(field, dir = 'asc') {
20
+ this.orderField = field;
21
+ this.orderDir = dir.toUpperCase();
22
+ return this;
23
+ }
24
+ limit(n) {
25
+ this.limitCount = n;
26
+ return this;
27
+ }
28
+ join(table) {
29
+ this.joinTable = table;
30
+ return this;
31
+ }
32
+ async run() {
33
+ const args = [this.name];
34
+ if (this.conditions.length) {
35
+ args.push('WHERE', this.conditions.join(' AND '));
36
+ }
37
+ if (this.orderField) {
38
+ args.push('ORDER', 'BY', this.orderField, this.orderDir || 'ASC');
39
+ }
40
+ if (this.limitCount != null) {
41
+ args.push('LIMIT', String(this.limitCount));
42
+ }
43
+ if (this.joinTable) {
44
+ args.push('JOIN', this.joinTable);
45
+ }
46
+ return this.client._tquery(args);
47
+ }
48
+ async insert(data) {
49
+ const args = [this.name];
50
+ for (const [k, v] of Object.entries(data)) {
51
+ args.push(k, String(v));
52
+ }
53
+ const result = await this.client.call('TINSERT', ...args);
54
+ return parseInt(result, 10) || 0;
55
+ }
56
+ async update(id, data) {
57
+ const args = [this.name, id];
58
+ for (const [k, v] of Object.entries(data)) {
59
+ args.push(k, String(v));
60
+ }
61
+ return this.client.call('TUPDATE', ...args);
62
+ }
63
+ async delete(...ids) {
64
+ return this.client.call('TDEL', this.name, ...ids);
65
+ }
66
+ }
67
+ class VectorNamespace {
68
+ constructor(client) {
69
+ this.client = client;
70
+ }
71
+ async set(key, vector, metadata) {
72
+ const args = [key, vector.length, ...vector];
73
+ if (metadata) {
74
+ args.push('META', JSON.stringify(metadata));
75
+ }
76
+ return this.client.call('VSET', ...args);
77
+ }
78
+ async get(key) {
79
+ return this.client.vget(key);
80
+ }
81
+ async search(query, options) {
82
+ return this.client.vsearch(query, { k: options.topK, filter: options.filter, meta: options.meta ?? true });
83
+ }
84
+ async count() {
85
+ return this.client.vcard();
86
+ }
87
+ }
88
+ class TimeSeriesNamespace {
89
+ constructor(client) {
90
+ this.client = client;
91
+ }
92
+ async add(key, value, options) {
93
+ return this.client.tsadd(key, options?.timestamp ?? '*', value, { retention: options?.retention, labels: options?.labels });
94
+ }
95
+ async get(key) {
96
+ return this.client.tsget(key);
97
+ }
98
+ async range(key, from, to, options) {
99
+ return this.client.tsrange(key, from, to, options);
100
+ }
101
+ async mrange(from, to, filter, options) {
102
+ return this.client.tsmrange(from, to, filter, options);
103
+ }
104
+ async info(key) {
105
+ return this.client.tsinfo(key);
106
+ }
107
+ }
8
108
  class Lux extends ioredis_1.default {
9
109
  constructor(options) {
10
110
  super(options);
111
+ this.vectors = new VectorNamespace(this);
112
+ this.timeseries = new TimeSeriesNamespace(this);
113
+ }
114
+ table(name) {
115
+ return new TableQueryBuilder(this, name);
11
116
  }
117
+ async _tquery(args) {
118
+ const result = await this.call('TQUERY', ...args);
119
+ if (!result || !Array.isArray(result))
120
+ return [];
121
+ const rows = [];
122
+ for (const item of result) {
123
+ if (Array.isArray(item) && item.length >= 2) {
124
+ const row = { id: 0 };
125
+ for (let i = 0; i < item.length - 1; i += 2) {
126
+ const key = String(item[i]);
127
+ const val = item[i + 1];
128
+ if (key === 'id') {
129
+ row.id = parseInt(val, 10);
130
+ }
131
+ else {
132
+ row[key] = val;
133
+ }
134
+ }
135
+ rows.push(row);
136
+ }
137
+ }
138
+ return rows;
139
+ }
140
+ // Vector methods (keep for backward compat)
12
141
  async vset(key, vector, options) {
13
142
  const args = [key, vector.length, ...vector];
14
143
  if (options?.metadata) {
@@ -23,7 +152,7 @@ class Lux extends ioredis_1.default {
23
152
  return this.call('VSET', ...args);
24
153
  }
25
154
  async vget(key) {
26
- const result = (await this.call('VGET', key));
155
+ const result = await this.call('VGET', key);
27
156
  if (!result || !Array.isArray(result))
28
157
  return null;
29
158
  const dims = parseInt(result[0], 10);
@@ -49,16 +178,13 @@ class Lux extends ioredis_1.default {
49
178
  if (options.meta) {
50
179
  args.push('META');
51
180
  }
52
- const result = (await this.call('VSEARCH', ...args));
181
+ const result = await this.call('VSEARCH', ...args);
53
182
  if (!result || !Array.isArray(result))
54
183
  return [];
55
184
  const results = [];
56
185
  for (const item of result) {
57
186
  if (Array.isArray(item)) {
58
- const entry = {
59
- key: item[0],
60
- similarity: parseFloat(item[1]),
61
- };
187
+ const entry = { key: item[0], similarity: parseFloat(item[1]) };
62
188
  if (options.meta && item[2]) {
63
189
  try {
64
190
  entry.metadata = JSON.parse(item[2]);
@@ -75,6 +201,122 @@ class Lux extends ioredis_1.default {
75
201
  async vcard() {
76
202
  return this.call('VCARD');
77
203
  }
204
+ // Time series methods (keep for backward compat)
205
+ async tsadd(key, timestamp, value, options) {
206
+ const args = [key, timestamp === '*' ? '*' : timestamp, value];
207
+ if (options?.retention != null) {
208
+ args.push('RETENTION', options.retention);
209
+ }
210
+ if (options?.labels) {
211
+ args.push('LABELS');
212
+ for (const [k, v] of Object.entries(options.labels)) {
213
+ args.push(k, v);
214
+ }
215
+ }
216
+ return this.call('TSADD', ...args);
217
+ }
218
+ async tsmadd(...entries) {
219
+ const args = [];
220
+ for (const [key, ts, val] of entries) {
221
+ args.push(key, ts === '*' ? '*' : ts, val);
222
+ }
223
+ return this.call('TSMADD', ...args);
224
+ }
225
+ async tsget(key) {
226
+ const result = await this.call('TSGET', key);
227
+ if (!result || !Array.isArray(result) || result.length < 2)
228
+ return null;
229
+ return { timestamp: parseInt(result[0], 10), value: parseFloat(result[1]) };
230
+ }
231
+ async tsrange(key, from, to, options) {
232
+ const args = [key, from === '-' ? '-' : from, to === '+' ? '+' : to];
233
+ if (options?.aggregation) {
234
+ args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
235
+ }
236
+ const result = await this.call('TSRANGE', ...args);
237
+ if (!result || !Array.isArray(result))
238
+ return [];
239
+ return result.map((pair) => ({ timestamp: parseInt(pair[0], 10), value: parseFloat(pair[1]) }));
240
+ }
241
+ async tsmrange(from, to, filter, options) {
242
+ const args = [from === '-' ? '-' : from, to === '+' ? '+' : to];
243
+ if (options?.aggregation) {
244
+ args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
245
+ }
246
+ args.push('FILTER', filter);
247
+ const result = await this.call('TSMRANGE', ...args);
248
+ if (!result || !Array.isArray(result))
249
+ return [];
250
+ return result.map((series) => {
251
+ const labels = {};
252
+ if (Array.isArray(series[1])) {
253
+ for (const pair of series[1]) {
254
+ if (Array.isArray(pair) && pair.length >= 2)
255
+ labels[pair[0]] = pair[1];
256
+ }
257
+ }
258
+ const samples = Array.isArray(series[2])
259
+ ? series[2].map((s) => ({ timestamp: parseInt(s[0], 10), value: parseFloat(s[1]) }))
260
+ : [];
261
+ return { key: series[0], labels, samples };
262
+ });
263
+ }
264
+ async tsinfo(key) {
265
+ const result = await this.call('TSINFO', key);
266
+ if (!result || !Array.isArray(result))
267
+ return {};
268
+ const info = {};
269
+ for (let i = 0; i < result.length - 1; i += 2) {
270
+ const k = result[i];
271
+ const v = result[i + 1];
272
+ if (k === 'labels' && Array.isArray(v)) {
273
+ const labels = {};
274
+ for (const pair of v) {
275
+ if (Array.isArray(pair) && pair.length >= 2)
276
+ labels[pair[0]] = pair[1];
277
+ }
278
+ info[k] = labels;
279
+ }
280
+ else {
281
+ info[k] = v;
282
+ }
283
+ }
284
+ return info;
285
+ }
286
+ // Realtime key subscriptions
287
+ ksub(patterns, handler) {
288
+ const sub = this.duplicate();
289
+ sub.on('error', () => { });
290
+ const dataHandler = sub._dataHandler || sub.dataHandler;
291
+ if (dataHandler && dataHandler.returnReply) {
292
+ const origReturn = dataHandler.returnReply.bind(dataHandler);
293
+ dataHandler.returnReply = (reply) => {
294
+ if (Array.isArray(reply) && reply.length === 4 && reply[0] === 'kmessage') {
295
+ handler({ pattern: reply[1], key: reply[2], operation: reply[3] });
296
+ return;
297
+ }
298
+ return origReturn(reply);
299
+ };
300
+ }
301
+ else {
302
+ const origEmit = sub.emit.bind(sub);
303
+ sub.emit = (event, ...args) => {
304
+ if (event === 'error' && args[0]?.message?.includes('Command queue state error')) {
305
+ const match = args[0].message.match(/Last reply: kmessage,([^,]+),([^,]+),(.+)/);
306
+ if (match) {
307
+ handler({ pattern: match[1], key: match[2], operation: match[3] });
308
+ return true;
309
+ }
310
+ }
311
+ return origEmit(event, ...args);
312
+ };
313
+ }
314
+ sub.call('KSUB', ...patterns);
315
+ return {
316
+ connection: sub,
317
+ unsubscribe() { sub.disconnect(); },
318
+ };
319
+ }
78
320
  }
79
321
  exports.Lux = Lux;
80
322
  exports.default = Lux;
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@luxdb/sdk",
3
- "version": "1.0.0",
4
- "description": "Lux SDK - ioredis extended with native vector search",
3
+ "version": "1.2.0",
4
+ "description": "Lux SDK - ioredis extended with vector search, time series, and realtime key subscriptions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": ["dist"],
8
8
  "scripts": {
9
9
  "build": "tsc",
10
- "prepublishOnly": "npm run build"
10
+ "prepublishOnly": "bun run build"
11
11
  },
12
12
  "dependencies": {
13
13
  "ioredis": "^5.0.0"
14
14
  },
15
15
  "devDependencies": {
16
+ "@types/node": "^25.5.0",
16
17
  "typescript": "^5.9.0"
17
18
  },
18
19
  "peerDependencies": {
@@ -24,5 +25,5 @@
24
25
  "url": "https://github.com/lux-db/lux",
25
26
  "directory": "sdk"
26
27
  },
27
- "keywords": ["lux", "redis", "vector", "search", "database", "ioredis", "similarity"]
28
+ "keywords": ["lux", "redis", "vector", "search", "database", "ioredis", "similarity", "timeseries", "realtime", "subscriptions"]
28
29
  }