@cnstra/devtools-server-repository-redis 0.1.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,49 @@
1
+ # @cnstra/devtools-server-repository-redis
2
+
3
+ > **⚠️ Experimental**: API may change.
4
+
5
+ Redis persistence for the [CNStra](https://cnstra.org) DevTools server. A drop-in, durable
6
+ replacement for `@cnstra/devtools-server-repository-in-memory`: same
7
+ `ICNSDevToolsServerRepository` contract, backed by Redis so apps, topology, stimulations and
8
+ per-hop history outlive a restart.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @cnstra/devtools-server-repository-redis ioredis
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import Redis from 'ioredis';
20
+ import { CNSDevToolsServer } from '@cnstra/devtools-server';
21
+ import { CNSDevToolsServerRepositoryRedis } from '@cnstra/devtools-server-repository-redis';
22
+
23
+ const redis = new Redis(process.env.REDIS_URL);
24
+ const repo = new CNSDevToolsServerRepositoryRedis(redis);
25
+
26
+ const server = new CNSDevToolsServer(repo);
27
+ ```
28
+
29
+ The constructor accepts any [ioredis](https://github.com/redis/ioredis)-style client. `ioredis`
30
+ is an optional peer dependency (the client is typed structurally). `node-redis` v4 uses
31
+ different method names (`hSet`, `hGetAll`, …) and is **not** directly compatible.
32
+
33
+ ## Options
34
+
35
+ ```ts
36
+ new CNSDevToolsServerRepositoryRedis(redis, { keyPrefix: 'cns:' });
37
+ ```
38
+
39
+ ## Data layout
40
+
41
+ With the default `cns:` prefix: `cns:apps` / `cns:topology` / `cns:cnsToApp` (hashes),
42
+ `cns:stim:{id}` (stimulation JSON), `cns:stims:{appId}` (zset scored by `startedAt` for
43
+ ordering/paging and time-range filters), `cns:hops:{stimId}` (list), `cns:appToCns:{appId}`
44
+ (set). `hasError` / `collateralId` / `neuronId` filters are applied in memory after the
45
+ time-range zset scan — fine at DevTools scale.
46
+
47
+ ## License
48
+
49
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var parse = (s) => s == null ? null : JSON.parse(s);
5
+ var CNSDevToolsServerRepositoryRedis = class {
6
+ constructor(redis, options = {}) {
7
+ this.redis = redis;
8
+ this.p = options.keyPrefix ?? "cns:";
9
+ }
10
+ // ─── Apps ───────────────────────────────────────────────────────────────
11
+ async upsertApp(app) {
12
+ await this.redis.hset(`${this.p}apps`, app.id, JSON.stringify(app));
13
+ }
14
+ async listApps() {
15
+ const all = await this.redis.hgetall(`${this.p}apps`);
16
+ return Object.values(all ?? {}).map((v) => JSON.parse(v));
17
+ }
18
+ // ─── Topology ───────────────────────────────────────────────────────────
19
+ async saveTopology(snapshot) {
20
+ await this.redis.hset(
21
+ `${this.p}topology`,
22
+ snapshot.cnsId,
23
+ JSON.stringify(snapshot)
24
+ );
25
+ }
26
+ async getTopology(cnsId) {
27
+ if (cnsId) {
28
+ const raw = await this.redis.hget(`${this.p}topology`, cnsId);
29
+ const snap = parse(raw);
30
+ return snap ? [snap] : [];
31
+ }
32
+ const all = await this.redis.hgetall(`${this.p}topology`);
33
+ return Object.values(all ?? {}).map(
34
+ (v) => JSON.parse(v)
35
+ );
36
+ }
37
+ // ─── Stimulations ───────────────────────────────────────────────────────
38
+ async saveStimulation(s) {
39
+ await this.redis.set(`${this.p}stim:${s.id}`, JSON.stringify(s));
40
+ await this.redis.zadd(`${this.p}stims:${s.appId}`, s.startedAt, s.id);
41
+ }
42
+ async completeStimulation(stimulationId, completedAt, hopCount, hasError) {
43
+ const raw = await this.redis.get(`${this.p}stim:${stimulationId}`);
44
+ const s = parse(raw);
45
+ if (!s) return;
46
+ const updated = {
47
+ ...s,
48
+ completedAt,
49
+ hopCount,
50
+ hasError
51
+ };
52
+ await this.redis.set(
53
+ `${this.p}stim:${stimulationId}`,
54
+ JSON.stringify(updated)
55
+ );
56
+ }
57
+ async getStimulations(appId, filter) {
58
+ const max = filter.toTimestamp ?? "+inf";
59
+ const min = filter.fromTimestamp ?? "-inf";
60
+ const ids = await this.redis.zrevrangebyscore(
61
+ `${this.p}stims:${appId}`,
62
+ max,
63
+ min
64
+ );
65
+ if (ids.length === 0) return { items: [], total: 0 };
66
+ const raws = await Promise.all(
67
+ ids.map((id) => this.redis.get(`${this.p}stim:${id}`))
68
+ );
69
+ let items = raws.map((r) => parse(r)).filter((s) => s !== null);
70
+ if (filter.hasError !== void 0)
71
+ items = items.filter((s) => s.hasError === filter.hasError);
72
+ if (filter.collateralId !== void 0)
73
+ items = items.filter((s) => s.collateralId === filter.collateralId);
74
+ if (filter.neuronId !== void 0)
75
+ items = items.filter(
76
+ (s) => s.collateralId.startsWith(filter.neuronId)
77
+ );
78
+ items.sort((a, b) => b.startedAt - a.startedAt);
79
+ const total = items.length;
80
+ const limit = filter.limit ?? 100;
81
+ const offset = filter.offset ?? 0;
82
+ return { items: items.slice(offset, offset + limit), total };
83
+ }
84
+ // ─── Hops ───────────────────────────────────────────────────────────────
85
+ async saveHop(hop) {
86
+ await this.redis.rpush(
87
+ `${this.p}hops:${hop.stimulationId}`,
88
+ JSON.stringify(hop)
89
+ );
90
+ }
91
+ async getHops(stimulationId) {
92
+ const raws = await this.redis.lrange(
93
+ `${this.p}hops:${stimulationId}`,
94
+ 0,
95
+ -1
96
+ );
97
+ return raws.map((r) => JSON.parse(r)).sort((a, b) => a.index - b.index);
98
+ }
99
+ // ─── CNS ↔ App mapping ──────────────────────────────────────────────────
100
+ async addCnsToApp(appId, cnsId) {
101
+ await this.redis.hset(`${this.p}cnsToApp`, cnsId, appId);
102
+ await this.redis.sadd(`${this.p}appToCns:${appId}`, cnsId);
103
+ }
104
+ async getCnsByApp(appId) {
105
+ return this.redis.smembers(`${this.p}appToCns:${appId}`);
106
+ }
107
+ async findAppByCns(cnsId) {
108
+ const appId = await this.redis.hget(`${this.p}cnsToApp`, cnsId);
109
+ return appId ?? void 0;
110
+ }
111
+ };
112
+
113
+ exports.CNSDevToolsServerRepositoryRedis = CNSDevToolsServerRepositoryRedis;
114
+ //# sourceMappingURL=index.cjs.map
115
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n ICNSDevToolsServerRepository,\n CNSDTOTopologySnapshot,\n} from '@cnstra/devtools-server';\nimport type {\n CNSDTOApp,\n CNSDTOStimulation,\n CNSDTOStimulationFilter,\n CNSDTOHop,\n} from '@cnstra/devtools-dto';\n\n/**\n * Minimal structural view of an `ioredis` client — only the commands this\n * repository uses. A real `Redis` instance from `ioredis` satisfies it, so we\n * avoid a hard dependency on `ioredis` and its types.\n *\n * Note: this targets ioredis-style lowercase command methods. `node-redis` v4\n * uses different names (hSet, hGetAll, …) and is not directly compatible.\n */\nexport interface IRedisLike {\n hset(key: string, field: string, value: string): Promise<unknown>;\n hget(key: string, field: string): Promise<string | null>;\n hgetall(key: string): Promise<Record<string, string>>;\n set(key: string, value: string): Promise<unknown>;\n get(key: string): Promise<string | null>;\n zadd(key: string, score: number, member: string): Promise<unknown>;\n zrevrangebyscore(\n key: string,\n max: number | string,\n min: number | string\n ): Promise<string[]>;\n rpush(key: string, value: string): Promise<unknown>;\n lrange(key: string, start: number, stop: number): Promise<string[]>;\n sadd(key: string, member: string): Promise<unknown>;\n smembers(key: string): Promise<string[]>;\n}\n\nexport interface CNSDevToolsServerRepositoryRedisOptions {\n /** Key namespace prefix (default \"cns:\"). */\n keyPrefix?: string;\n}\n\nconst parse = <T>(s: string | null): T | null =>\n s == null ? null : (JSON.parse(s) as T);\n\n/**\n * Redis-backed repository for the CNStra DevTools server. Durable counterpart of\n * `@cnstra/devtools-server-repository-in-memory` with the same\n * `ICNSDevToolsServerRepository` contract.\n *\n * Data layout (with the default `cns:` prefix):\n * - `cns:apps` hash appId → app JSON\n * - `cns:topology` hash cnsId → snapshot JSON\n * - `cns:stim:{id}` string stimulation JSON\n * - `cns:stims:{appId}` zset score=startedAt, member=stimId (ordering/paging)\n * - `cns:hops:{stimId}` list hop JSON (append order)\n * - `cns:cnsToApp` hash cnsId → appId\n * - `cns:appToCns:{appId}` set cnsId\n */\nexport class CNSDevToolsServerRepositoryRedis\n implements ICNSDevToolsServerRepository\n{\n private readonly p: string;\n\n constructor(\n private readonly redis: IRedisLike,\n options: CNSDevToolsServerRepositoryRedisOptions = {}\n ) {\n this.p = options.keyPrefix ?? 'cns:';\n }\n\n // ─── Apps ───────────────────────────────────────────────────────────────\n\n async upsertApp(app: CNSDTOApp): Promise<void> {\n await this.redis.hset(`${this.p}apps`, app.id, JSON.stringify(app));\n }\n\n async listApps(): Promise<CNSDTOApp[]> {\n const all = await this.redis.hgetall(`${this.p}apps`);\n return Object.values(all ?? {}).map(v => JSON.parse(v) as CNSDTOApp);\n }\n\n // ─── Topology ───────────────────────────────────────────────────────────\n\n async saveTopology(snapshot: CNSDTOTopologySnapshot): Promise<void> {\n await this.redis.hset(\n `${this.p}topology`,\n snapshot.cnsId,\n JSON.stringify(snapshot)\n );\n }\n\n async getTopology(cnsId?: string): Promise<CNSDTOTopologySnapshot[]> {\n if (cnsId) {\n const raw = await this.redis.hget(`${this.p}topology`, cnsId);\n const snap = parse<CNSDTOTopologySnapshot>(raw);\n return snap ? [snap] : [];\n }\n const all = await this.redis.hgetall(`${this.p}topology`);\n return Object.values(all ?? {}).map(\n v => JSON.parse(v) as CNSDTOTopologySnapshot\n );\n }\n\n // ─── Stimulations ───────────────────────────────────────────────────────\n\n async saveStimulation(s: CNSDTOStimulation): Promise<void> {\n await this.redis.set(`${this.p}stim:${s.id}`, JSON.stringify(s));\n await this.redis.zadd(`${this.p}stims:${s.appId}`, s.startedAt, s.id);\n }\n\n async completeStimulation(\n stimulationId: string,\n completedAt: number,\n hopCount: number,\n hasError: boolean\n ): Promise<void> {\n const raw = await this.redis.get(`${this.p}stim:${stimulationId}`);\n const s = parse<CNSDTOStimulation>(raw);\n if (!s) return;\n const updated: CNSDTOStimulation = {\n ...s,\n completedAt,\n hopCount,\n hasError,\n };\n await this.redis.set(\n `${this.p}stim:${stimulationId}`,\n JSON.stringify(updated)\n );\n }\n\n async getStimulations(\n appId: string,\n filter: CNSDTOStimulationFilter\n ): Promise<{ items: CNSDTOStimulation[]; total: number }> {\n const max = filter.toTimestamp ?? '+inf';\n const min = filter.fromTimestamp ?? '-inf';\n\n // Ids ordered by startedAt desc, already time-range filtered by score.\n const ids = await this.redis.zrevrangebyscore(\n `${this.p}stims:${appId}`,\n max,\n min\n );\n if (ids.length === 0) return { items: [], total: 0 };\n\n const raws = await Promise.all(\n ids.map(id => this.redis.get(`${this.p}stim:${id}`))\n );\n let items = raws\n .map(r => parse<CNSDTOStimulation>(r))\n .filter((s): s is CNSDTOStimulation => s !== null);\n\n if (filter.hasError !== undefined)\n items = items.filter(s => s.hasError === filter.hasError);\n if (filter.collateralId !== undefined)\n items = items.filter(s => s.collateralId === filter.collateralId);\n if (filter.neuronId !== undefined)\n items = items.filter(s =>\n s.collateralId.startsWith(filter.neuronId!)\n );\n\n // zrevrangebyscore already returns desc; re-sort defensively.\n items.sort((a, b) => b.startedAt - a.startedAt);\n\n const total = items.length;\n const limit = filter.limit ?? 100;\n const offset = filter.offset ?? 0;\n return { items: items.slice(offset, offset + limit), total };\n }\n\n // ─── Hops ───────────────────────────────────────────────────────────────\n\n async saveHop(hop: CNSDTOHop): Promise<void> {\n await this.redis.rpush(\n `${this.p}hops:${hop.stimulationId}`,\n JSON.stringify(hop)\n );\n }\n\n async getHops(stimulationId: string): Promise<CNSDTOHop[]> {\n const raws = await this.redis.lrange(\n `${this.p}hops:${stimulationId}`,\n 0,\n -1\n );\n return raws\n .map(r => JSON.parse(r) as CNSDTOHop)\n .sort((a, b) => a.index - b.index);\n }\n\n // ─── CNS ↔ App mapping ──────────────────────────────────────────────────\n\n async addCnsToApp(appId: string, cnsId: string): Promise<void> {\n await this.redis.hset(`${this.p}cnsToApp`, cnsId, appId);\n await this.redis.sadd(`${this.p}appToCns:${appId}`, cnsId);\n }\n\n async getCnsByApp(appId: string): Promise<string[]> {\n return this.redis.smembers(`${this.p}appToCns:${appId}`);\n }\n\n async findAppByCns(cnsId: string): Promise<string | undefined> {\n const appId = await this.redis.hget(`${this.p}cnsToApp`, cnsId);\n return appId ?? undefined;\n }\n}\n"]}
@@ -0,0 +1,62 @@
1
+ import type { ICNSDevToolsServerRepository, CNSDTOTopologySnapshot } from '@cnstra/devtools-server';
2
+ import type { CNSDTOApp, CNSDTOStimulation, CNSDTOStimulationFilter, CNSDTOHop } from '@cnstra/devtools-dto';
3
+ /**
4
+ * Minimal structural view of an `ioredis` client — only the commands this
5
+ * repository uses. A real `Redis` instance from `ioredis` satisfies it, so we
6
+ * avoid a hard dependency on `ioredis` and its types.
7
+ *
8
+ * Note: this targets ioredis-style lowercase command methods. `node-redis` v4
9
+ * uses different names (hSet, hGetAll, …) and is not directly compatible.
10
+ */
11
+ export interface IRedisLike {
12
+ hset(key: string, field: string, value: string): Promise<unknown>;
13
+ hget(key: string, field: string): Promise<string | null>;
14
+ hgetall(key: string): Promise<Record<string, string>>;
15
+ set(key: string, value: string): Promise<unknown>;
16
+ get(key: string): Promise<string | null>;
17
+ zadd(key: string, score: number, member: string): Promise<unknown>;
18
+ zrevrangebyscore(key: string, max: number | string, min: number | string): Promise<string[]>;
19
+ rpush(key: string, value: string): Promise<unknown>;
20
+ lrange(key: string, start: number, stop: number): Promise<string[]>;
21
+ sadd(key: string, member: string): Promise<unknown>;
22
+ smembers(key: string): Promise<string[]>;
23
+ }
24
+ export interface CNSDevToolsServerRepositoryRedisOptions {
25
+ /** Key namespace prefix (default "cns:"). */
26
+ keyPrefix?: string;
27
+ }
28
+ /**
29
+ * Redis-backed repository for the CNStra DevTools server. Durable counterpart of
30
+ * `@cnstra/devtools-server-repository-in-memory` with the same
31
+ * `ICNSDevToolsServerRepository` contract.
32
+ *
33
+ * Data layout (with the default `cns:` prefix):
34
+ * - `cns:apps` hash appId → app JSON
35
+ * - `cns:topology` hash cnsId → snapshot JSON
36
+ * - `cns:stim:{id}` string stimulation JSON
37
+ * - `cns:stims:{appId}` zset score=startedAt, member=stimId (ordering/paging)
38
+ * - `cns:hops:{stimId}` list hop JSON (append order)
39
+ * - `cns:cnsToApp` hash cnsId → appId
40
+ * - `cns:appToCns:{appId}` set cnsId
41
+ */
42
+ export declare class CNSDevToolsServerRepositoryRedis implements ICNSDevToolsServerRepository {
43
+ private readonly redis;
44
+ private readonly p;
45
+ constructor(redis: IRedisLike, options?: CNSDevToolsServerRepositoryRedisOptions);
46
+ upsertApp(app: CNSDTOApp): Promise<void>;
47
+ listApps(): Promise<CNSDTOApp[]>;
48
+ saveTopology(snapshot: CNSDTOTopologySnapshot): Promise<void>;
49
+ getTopology(cnsId?: string): Promise<CNSDTOTopologySnapshot[]>;
50
+ saveStimulation(s: CNSDTOStimulation): Promise<void>;
51
+ completeStimulation(stimulationId: string, completedAt: number, hopCount: number, hasError: boolean): Promise<void>;
52
+ getStimulations(appId: string, filter: CNSDTOStimulationFilter): Promise<{
53
+ items: CNSDTOStimulation[];
54
+ total: number;
55
+ }>;
56
+ saveHop(hop: CNSDTOHop): Promise<void>;
57
+ getHops(stimulationId: string): Promise<CNSDTOHop[]>;
58
+ addCnsToApp(appId: string, cnsId: string): Promise<void>;
59
+ getCnsByApp(appId: string): Promise<string[]>;
60
+ findAppByCns(cnsId: string): Promise<string | undefined>;
61
+ }
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // src/index.ts
2
+ var parse = (s) => s == null ? null : JSON.parse(s);
3
+ var CNSDevToolsServerRepositoryRedis = class {
4
+ constructor(redis, options = {}) {
5
+ this.redis = redis;
6
+ this.p = options.keyPrefix ?? "cns:";
7
+ }
8
+ // ─── Apps ───────────────────────────────────────────────────────────────
9
+ async upsertApp(app) {
10
+ await this.redis.hset(`${this.p}apps`, app.id, JSON.stringify(app));
11
+ }
12
+ async listApps() {
13
+ const all = await this.redis.hgetall(`${this.p}apps`);
14
+ return Object.values(all ?? {}).map((v) => JSON.parse(v));
15
+ }
16
+ // ─── Topology ───────────────────────────────────────────────────────────
17
+ async saveTopology(snapshot) {
18
+ await this.redis.hset(
19
+ `${this.p}topology`,
20
+ snapshot.cnsId,
21
+ JSON.stringify(snapshot)
22
+ );
23
+ }
24
+ async getTopology(cnsId) {
25
+ if (cnsId) {
26
+ const raw = await this.redis.hget(`${this.p}topology`, cnsId);
27
+ const snap = parse(raw);
28
+ return snap ? [snap] : [];
29
+ }
30
+ const all = await this.redis.hgetall(`${this.p}topology`);
31
+ return Object.values(all ?? {}).map(
32
+ (v) => JSON.parse(v)
33
+ );
34
+ }
35
+ // ─── Stimulations ───────────────────────────────────────────────────────
36
+ async saveStimulation(s) {
37
+ await this.redis.set(`${this.p}stim:${s.id}`, JSON.stringify(s));
38
+ await this.redis.zadd(`${this.p}stims:${s.appId}`, s.startedAt, s.id);
39
+ }
40
+ async completeStimulation(stimulationId, completedAt, hopCount, hasError) {
41
+ const raw = await this.redis.get(`${this.p}stim:${stimulationId}`);
42
+ const s = parse(raw);
43
+ if (!s) return;
44
+ const updated = {
45
+ ...s,
46
+ completedAt,
47
+ hopCount,
48
+ hasError
49
+ };
50
+ await this.redis.set(
51
+ `${this.p}stim:${stimulationId}`,
52
+ JSON.stringify(updated)
53
+ );
54
+ }
55
+ async getStimulations(appId, filter) {
56
+ const max = filter.toTimestamp ?? "+inf";
57
+ const min = filter.fromTimestamp ?? "-inf";
58
+ const ids = await this.redis.zrevrangebyscore(
59
+ `${this.p}stims:${appId}`,
60
+ max,
61
+ min
62
+ );
63
+ if (ids.length === 0) return { items: [], total: 0 };
64
+ const raws = await Promise.all(
65
+ ids.map((id) => this.redis.get(`${this.p}stim:${id}`))
66
+ );
67
+ let items = raws.map((r) => parse(r)).filter((s) => s !== null);
68
+ if (filter.hasError !== void 0)
69
+ items = items.filter((s) => s.hasError === filter.hasError);
70
+ if (filter.collateralId !== void 0)
71
+ items = items.filter((s) => s.collateralId === filter.collateralId);
72
+ if (filter.neuronId !== void 0)
73
+ items = items.filter(
74
+ (s) => s.collateralId.startsWith(filter.neuronId)
75
+ );
76
+ items.sort((a, b) => b.startedAt - a.startedAt);
77
+ const total = items.length;
78
+ const limit = filter.limit ?? 100;
79
+ const offset = filter.offset ?? 0;
80
+ return { items: items.slice(offset, offset + limit), total };
81
+ }
82
+ // ─── Hops ───────────────────────────────────────────────────────────────
83
+ async saveHop(hop) {
84
+ await this.redis.rpush(
85
+ `${this.p}hops:${hop.stimulationId}`,
86
+ JSON.stringify(hop)
87
+ );
88
+ }
89
+ async getHops(stimulationId) {
90
+ const raws = await this.redis.lrange(
91
+ `${this.p}hops:${stimulationId}`,
92
+ 0,
93
+ -1
94
+ );
95
+ return raws.map((r) => JSON.parse(r)).sort((a, b) => a.index - b.index);
96
+ }
97
+ // ─── CNS ↔ App mapping ──────────────────────────────────────────────────
98
+ async addCnsToApp(appId, cnsId) {
99
+ await this.redis.hset(`${this.p}cnsToApp`, cnsId, appId);
100
+ await this.redis.sadd(`${this.p}appToCns:${appId}`, cnsId);
101
+ }
102
+ async getCnsByApp(appId) {
103
+ return this.redis.smembers(`${this.p}appToCns:${appId}`);
104
+ }
105
+ async findAppByCns(cnsId) {
106
+ const appId = await this.redis.hget(`${this.p}cnsToApp`, cnsId);
107
+ return appId ?? void 0;
108
+ }
109
+ };
110
+
111
+ export { CNSDevToolsServerRepositoryRedis };
112
+ //# sourceMappingURL=index.js.map
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n ICNSDevToolsServerRepository,\n CNSDTOTopologySnapshot,\n} from '@cnstra/devtools-server';\nimport type {\n CNSDTOApp,\n CNSDTOStimulation,\n CNSDTOStimulationFilter,\n CNSDTOHop,\n} from '@cnstra/devtools-dto';\n\n/**\n * Minimal structural view of an `ioredis` client — only the commands this\n * repository uses. A real `Redis` instance from `ioredis` satisfies it, so we\n * avoid a hard dependency on `ioredis` and its types.\n *\n * Note: this targets ioredis-style lowercase command methods. `node-redis` v4\n * uses different names (hSet, hGetAll, …) and is not directly compatible.\n */\nexport interface IRedisLike {\n hset(key: string, field: string, value: string): Promise<unknown>;\n hget(key: string, field: string): Promise<string | null>;\n hgetall(key: string): Promise<Record<string, string>>;\n set(key: string, value: string): Promise<unknown>;\n get(key: string): Promise<string | null>;\n zadd(key: string, score: number, member: string): Promise<unknown>;\n zrevrangebyscore(\n key: string,\n max: number | string,\n min: number | string\n ): Promise<string[]>;\n rpush(key: string, value: string): Promise<unknown>;\n lrange(key: string, start: number, stop: number): Promise<string[]>;\n sadd(key: string, member: string): Promise<unknown>;\n smembers(key: string): Promise<string[]>;\n}\n\nexport interface CNSDevToolsServerRepositoryRedisOptions {\n /** Key namespace prefix (default \"cns:\"). */\n keyPrefix?: string;\n}\n\nconst parse = <T>(s: string | null): T | null =>\n s == null ? null : (JSON.parse(s) as T);\n\n/**\n * Redis-backed repository for the CNStra DevTools server. Durable counterpart of\n * `@cnstra/devtools-server-repository-in-memory` with the same\n * `ICNSDevToolsServerRepository` contract.\n *\n * Data layout (with the default `cns:` prefix):\n * - `cns:apps` hash appId → app JSON\n * - `cns:topology` hash cnsId → snapshot JSON\n * - `cns:stim:{id}` string stimulation JSON\n * - `cns:stims:{appId}` zset score=startedAt, member=stimId (ordering/paging)\n * - `cns:hops:{stimId}` list hop JSON (append order)\n * - `cns:cnsToApp` hash cnsId → appId\n * - `cns:appToCns:{appId}` set cnsId\n */\nexport class CNSDevToolsServerRepositoryRedis\n implements ICNSDevToolsServerRepository\n{\n private readonly p: string;\n\n constructor(\n private readonly redis: IRedisLike,\n options: CNSDevToolsServerRepositoryRedisOptions = {}\n ) {\n this.p = options.keyPrefix ?? 'cns:';\n }\n\n // ─── Apps ───────────────────────────────────────────────────────────────\n\n async upsertApp(app: CNSDTOApp): Promise<void> {\n await this.redis.hset(`${this.p}apps`, app.id, JSON.stringify(app));\n }\n\n async listApps(): Promise<CNSDTOApp[]> {\n const all = await this.redis.hgetall(`${this.p}apps`);\n return Object.values(all ?? {}).map(v => JSON.parse(v) as CNSDTOApp);\n }\n\n // ─── Topology ───────────────────────────────────────────────────────────\n\n async saveTopology(snapshot: CNSDTOTopologySnapshot): Promise<void> {\n await this.redis.hset(\n `${this.p}topology`,\n snapshot.cnsId,\n JSON.stringify(snapshot)\n );\n }\n\n async getTopology(cnsId?: string): Promise<CNSDTOTopologySnapshot[]> {\n if (cnsId) {\n const raw = await this.redis.hget(`${this.p}topology`, cnsId);\n const snap = parse<CNSDTOTopologySnapshot>(raw);\n return snap ? [snap] : [];\n }\n const all = await this.redis.hgetall(`${this.p}topology`);\n return Object.values(all ?? {}).map(\n v => JSON.parse(v) as CNSDTOTopologySnapshot\n );\n }\n\n // ─── Stimulations ───────────────────────────────────────────────────────\n\n async saveStimulation(s: CNSDTOStimulation): Promise<void> {\n await this.redis.set(`${this.p}stim:${s.id}`, JSON.stringify(s));\n await this.redis.zadd(`${this.p}stims:${s.appId}`, s.startedAt, s.id);\n }\n\n async completeStimulation(\n stimulationId: string,\n completedAt: number,\n hopCount: number,\n hasError: boolean\n ): Promise<void> {\n const raw = await this.redis.get(`${this.p}stim:${stimulationId}`);\n const s = parse<CNSDTOStimulation>(raw);\n if (!s) return;\n const updated: CNSDTOStimulation = {\n ...s,\n completedAt,\n hopCount,\n hasError,\n };\n await this.redis.set(\n `${this.p}stim:${stimulationId}`,\n JSON.stringify(updated)\n );\n }\n\n async getStimulations(\n appId: string,\n filter: CNSDTOStimulationFilter\n ): Promise<{ items: CNSDTOStimulation[]; total: number }> {\n const max = filter.toTimestamp ?? '+inf';\n const min = filter.fromTimestamp ?? '-inf';\n\n // Ids ordered by startedAt desc, already time-range filtered by score.\n const ids = await this.redis.zrevrangebyscore(\n `${this.p}stims:${appId}`,\n max,\n min\n );\n if (ids.length === 0) return { items: [], total: 0 };\n\n const raws = await Promise.all(\n ids.map(id => this.redis.get(`${this.p}stim:${id}`))\n );\n let items = raws\n .map(r => parse<CNSDTOStimulation>(r))\n .filter((s): s is CNSDTOStimulation => s !== null);\n\n if (filter.hasError !== undefined)\n items = items.filter(s => s.hasError === filter.hasError);\n if (filter.collateralId !== undefined)\n items = items.filter(s => s.collateralId === filter.collateralId);\n if (filter.neuronId !== undefined)\n items = items.filter(s =>\n s.collateralId.startsWith(filter.neuronId!)\n );\n\n // zrevrangebyscore already returns desc; re-sort defensively.\n items.sort((a, b) => b.startedAt - a.startedAt);\n\n const total = items.length;\n const limit = filter.limit ?? 100;\n const offset = filter.offset ?? 0;\n return { items: items.slice(offset, offset + limit), total };\n }\n\n // ─── Hops ───────────────────────────────────────────────────────────────\n\n async saveHop(hop: CNSDTOHop): Promise<void> {\n await this.redis.rpush(\n `${this.p}hops:${hop.stimulationId}`,\n JSON.stringify(hop)\n );\n }\n\n async getHops(stimulationId: string): Promise<CNSDTOHop[]> {\n const raws = await this.redis.lrange(\n `${this.p}hops:${stimulationId}`,\n 0,\n -1\n );\n return raws\n .map(r => JSON.parse(r) as CNSDTOHop)\n .sort((a, b) => a.index - b.index);\n }\n\n // ─── CNS ↔ App mapping ──────────────────────────────────────────────────\n\n async addCnsToApp(appId: string, cnsId: string): Promise<void> {\n await this.redis.hset(`${this.p}cnsToApp`, cnsId, appId);\n await this.redis.sadd(`${this.p}appToCns:${appId}`, cnsId);\n }\n\n async getCnsByApp(appId: string): Promise<string[]> {\n return this.redis.smembers(`${this.p}appToCns:${appId}`);\n }\n\n async findAppByCns(cnsId: string): Promise<string | undefined> {\n const appId = await this.redis.hget(`${this.p}cnsToApp`, cnsId);\n return appId ?? undefined;\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@cnstra/devtools-server-repository-redis",
3
+ "version": "0.1.0",
4
+ "description": "Redis repository for CNStra DevTools server (durable stimulations & hops)",
5
+ "keywords": ["cnstra", "devtools", "repository", "redis", "ioredis"],
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsup && tsc --emitDeclarationOnly",
20
+ "build:types": "tsc --emitDeclarationOnly",
21
+ "test": "jest",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "files": ["dist"],
25
+ "dependencies": {
26
+ "@cnstra/devtools-dto": "^1.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@cnstra/devtools-server": "^1.0.1",
30
+ "ioredis": ">=5"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "ioredis": { "optional": true }
34
+ },
35
+ "devDependencies": {
36
+ "@types/jest": "^29.5.5",
37
+ "ioredis-mock": "^8.9.0",
38
+ "jest": "^29.7.0",
39
+ "ts-jest": "^29.1.1",
40
+ "tsup": "^8.5.0",
41
+ "typescript": "^5.8.3"
42
+ },
43
+ "author": "Andrei Baikov <abaikov23@gmail.com>",
44
+ "license": "MIT",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }