symmetry-cli 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/.eslintrc.js ADDED
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ plugins: ['@typescript-eslint'],
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:@typescript-eslint/recommended',
7
+ ],
8
+ env: {
9
+ node: true,
10
+ jest: true,
11
+ },
12
+ rules: {
13
+ // You can add custom rules here
14
+ },
15
+ };
package/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 Richard Macarthy (rjmacarthy)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,61 @@
1
+ import { SymmetryClient } from "../src/client";
2
+ import yaml from "js-yaml";
3
+
4
+ jest.mock("hyperswarm", () => {
5
+ return jest.fn().mockImplementation(() => ({
6
+ join: jest
7
+ .fn()
8
+ .mockReturnValue({ flushed: jest.fn().mockResolvedValue(undefined) }),
9
+ on: jest.fn(),
10
+ destroy: jest.fn().mockResolvedValue(undefined),
11
+ flush: jest.fn().mockResolvedValue(undefined),
12
+ }));
13
+ });
14
+
15
+ jest.mock("hypercore-crypto", () => ({
16
+ discoveryKey: jest.fn().mockReturnValue("test"),
17
+ keyPair: jest.fn().mockReturnValue({
18
+ publicKey: "test-public-key",
19
+ secretKey: "test-secret-key",
20
+ }),
21
+ sign: jest.fn(),
22
+ }));
23
+
24
+ jest.mock("fs", () => ({
25
+ readFileSync: jest.fn(),
26
+ writeFileSync: jest.fn(),
27
+ existsSync: jest.fn(),
28
+ }));
29
+
30
+ jest.mock("js-yaml", () => ({
31
+ load: jest.fn(),
32
+ }));
33
+
34
+
35
+ describe("Symmetry", () => {
36
+ let writer: SymmetryClient;
37
+ const mockConfig = {
38
+ path: "/test/path",
39
+ temperature: 1,
40
+ apiHostname: "test.api.com",
41
+ apiPort: 443,
42
+ apiPath: "/v1/chat",
43
+ apiProtocol: "https",
44
+ apiKey: "test-api-key",
45
+ apiProvider: "test-provider",
46
+ modelName: "test-model",
47
+ name: "test",
48
+ public: true,
49
+ serverKey: "test-server-key",
50
+ };
51
+
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+ (yaml.load as jest.Mock).mockReturnValue(mockConfig);
55
+ writer = new SymmetryClient("mock-config.yaml");
56
+ });
57
+
58
+ test("init method sets up the writer correctly", async () => {
59
+ await writer.init();
60
+ });
61
+ });
package/dist/client.js ADDED
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SymmetryClient = void 0;
7
+ const node_stream_1 = require("node:stream");
8
+ const promises_1 = require("stream/promises");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const hyperswarm_1 = __importDefault(require("hyperswarm"));
11
+ const hypercore_crypto_1 = __importDefault(require("hypercore-crypto"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const config_1 = require("./config");
14
+ const utils_1 = require("./utils");
15
+ const logger_1 = require("./logger");
16
+ const constants_1 = require("./constants");
17
+ class SymmetryClient {
18
+ constructor(configPath) {
19
+ this._conversationIndex = 0;
20
+ this._discoveryKey = null;
21
+ this._isPublic = false;
22
+ this._providerSwarm = null;
23
+ this._serverPeer = null;
24
+ this._challenge = null;
25
+ logger_1.logger.info(`🔗 Initializing client using config file: ${configPath}`);
26
+ this._config = new config_1.ConfigManager(configPath);
27
+ this._isPublic = this._config.get("public");
28
+ }
29
+ async init() {
30
+ this._providerSwarm = new hyperswarm_1.default();
31
+ const keyPair = hypercore_crypto_1.default.keyPair();
32
+ this._discoveryKey = hypercore_crypto_1.default.discoveryKey(keyPair.publicKey);
33
+ const discovery = this._providerSwarm.join(this._discoveryKey, {
34
+ client: true,
35
+ server: false,
36
+ });
37
+ await discovery.flushed();
38
+ this._providerSwarm.on("error", (err) => {
39
+ logger_1.logger.error(chalk_1.default.red("🚨 Swarm Error:"), err);
40
+ });
41
+ this._providerSwarm.on("connection", (peer) => {
42
+ var _a, _b;
43
+ (_a = this._serverPeer) === null || _a === void 0 ? void 0 : _a.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.conectionSize, {
44
+ connections: (_b = this._providerSwarm) === null || _b === void 0 ? void 0 : _b.connections.size,
45
+ }));
46
+ logger_1.logger.info(`⚡️ New connection from peer: ${peer.rawStream.remoteHost}`);
47
+ this.listeners(peer);
48
+ });
49
+ logger_1.logger.info(`📁 Symmetry client initialized.`);
50
+ logger_1.logger.info(`🔑 Discovery key: ${this._discoveryKey.toString("hex")}`);
51
+ if (this._isPublic) {
52
+ logger_1.logger.info(chalk_1.default.white(`🔑 Server key: ${this._config.get("serverKey")}`));
53
+ logger_1.logger.info(chalk_1.default.white("🔗 Joining server, please wait."));
54
+ await this.joinServer();
55
+ }
56
+ }
57
+ async joinServer() {
58
+ const serverSwarm = new hyperswarm_1.default();
59
+ const serverKey = Buffer.from(this._config.get("serverKey"));
60
+ serverSwarm.join(hypercore_crypto_1.default.discoveryKey(serverKey), {
61
+ client: true,
62
+ server: false,
63
+ });
64
+ serverSwarm.flush();
65
+ serverSwarm.on("connection", (peer) => {
66
+ this._serverPeer = peer;
67
+ logger_1.logger.info(chalk_1.default.green("🔗 Connected to server."));
68
+ this._challenge = hypercore_crypto_1.default.randomBytes(32);
69
+ this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.challenge, {
70
+ challenge: this._challenge,
71
+ }));
72
+ this._serverPeer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.join, {
73
+ ...this._config.getAll(),
74
+ discoveryKey: this._discoveryKey,
75
+ }));
76
+ this._serverPeer.on("data", async (buffer) => {
77
+ if (!buffer)
78
+ return;
79
+ const data = (0, utils_1.safeParseJson)(buffer.toString());
80
+ if (data && data.key) {
81
+ switch (data.key) {
82
+ case constants_1.serverMessageKeys.challenge:
83
+ this.handleServerHandshake(data.data);
84
+ break;
85
+ }
86
+ }
87
+ });
88
+ });
89
+ }
90
+ getServerPublicKey(serverKeyHex) {
91
+ const publicKey = Buffer.from(serverKeyHex, "hex");
92
+ if (publicKey.length !== 32) {
93
+ throw new Error(`Expected a 32-byte public key, but got ${publicKey.length} bytes`);
94
+ }
95
+ return publicKey;
96
+ }
97
+ handleServerHandshake(data) {
98
+ if (!this._challenge) {
99
+ console.log("No challenge set. Cannot verify.");
100
+ return;
101
+ }
102
+ const serverKeyHex = this._config.get("serverKey");
103
+ try {
104
+ const publicKey = this.getServerPublicKey(serverKeyHex);
105
+ const signatureBuffer = Buffer.from(data.signature.data, "base64");
106
+ const verified = hypercore_crypto_1.default.verify(this._challenge, signatureBuffer, publicKey);
107
+ if (verified) {
108
+ logger_1.logger.info(chalk_1.default.greenBright(`✅ Verification handshake successful.`));
109
+ }
110
+ else {
111
+ logger_1.logger.error(`❌ Verification failed!`);
112
+ }
113
+ }
114
+ catch (error) {
115
+ console.error("Error during verification:", error);
116
+ }
117
+ }
118
+ heartbeat(peer) {
119
+ peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.heartbeat, {
120
+ discoveryKey: this._discoveryKey,
121
+ }));
122
+ }
123
+ listeners(peer) {
124
+ peer.on("close", () => {
125
+ logger_1.logger.info(`🔌 Peer ${peer.rawStream.remoteHost} disconnected.`);
126
+ });
127
+ peer.on("data", async (buffer) => {
128
+ if (!buffer)
129
+ return;
130
+ const data = (0, utils_1.safeParseJson)(buffer.toString());
131
+ if (data && data.key) {
132
+ switch (data.key) {
133
+ case constants_1.serverMessageKeys.newConversation:
134
+ this._conversationIndex = this._conversationIndex + 1;
135
+ break;
136
+ case constants_1.serverMessageKeys.inference:
137
+ logger_1.logger.info(`📦 Inference message received from ${peer.rawStream.remoteHost}`);
138
+ await this.handleInference(data, peer);
139
+ break;
140
+ }
141
+ }
142
+ });
143
+ }
144
+ async handleInference(data, peer) {
145
+ const messages = data === null || data === void 0 ? void 0 : data.data;
146
+ if (!messages || !messages.length)
147
+ return;
148
+ const req = this.buildStreamRequest(messages);
149
+ if (!req)
150
+ return;
151
+ const { requestOptions, requestBody } = req;
152
+ const { protocol, hostname, port, path, method, headers } = requestOptions;
153
+ const url = `${protocol}://${hostname}:${port}${path}`;
154
+ try {
155
+ const response = await fetch(url, {
156
+ method,
157
+ headers,
158
+ body: JSON.stringify(requestBody),
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`Server responded with status code: ${response.status}`);
162
+ }
163
+ if (!response.body) {
164
+ throw new Error("Failed to get a ReadableStream from the response");
165
+ }
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ const responseStream = node_stream_1.Readable.fromWeb(response.body);
168
+ const peerStream = new node_stream_1.PassThrough();
169
+ responseStream.pipe(peerStream);
170
+ let completion = "";
171
+ const provider = this._config.get("apiProvider");
172
+ const peerPipeline = (0, promises_1.pipeline)(peerStream, async function (source) {
173
+ for await (const chunk of source) {
174
+ if (peer.writable) {
175
+ completion += (0, utils_1.getChatDataFromProvider)(provider, (0, utils_1.safeParseStreamResponse)(chunk.toString()));
176
+ if (!peer.write(chunk)) {
177
+ await new Promise((resolve) => peer.once("drain", resolve));
178
+ }
179
+ }
180
+ else {
181
+ break;
182
+ }
183
+ }
184
+ });
185
+ await Promise.resolve(peerPipeline);
186
+ if (this._config.get("dataCollectionEnabled")) {
187
+ this.saveCompletion(completion, peer, messages);
188
+ }
189
+ }
190
+ catch (error) {
191
+ let errorMessage = "An error occurred during inference";
192
+ if (error instanceof Error)
193
+ errorMessage = error.message;
194
+ logger_1.logger.error(`🚨 ${errorMessage}`);
195
+ }
196
+ }
197
+ async saveCompletion(completion, peer, messages) {
198
+ node_fs_1.default.writeFile(`${this._config.get("path")}/${peer.publicKey.toString("hex")}-${this._conversationIndex}.json`, JSON.stringify([
199
+ ...messages,
200
+ {
201
+ role: "assistant",
202
+ content: completion,
203
+ },
204
+ ]), () => {
205
+ logger_1.logger.info(`📝 Completion saved to file`);
206
+ });
207
+ }
208
+ buildStreamRequest(messages) {
209
+ const requestOptions = {
210
+ hostname: this._config.get("apiHostname"),
211
+ port: Number(this._config.get("apiPort")),
212
+ path: this._config.get("apiPath"),
213
+ protocol: this._config.get("apiProtocol"),
214
+ method: "POST",
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ Authorization: `Bearer ${this._config.get("apiKey")}`,
218
+ },
219
+ };
220
+ const requestBody = {
221
+ model: this._config.get("modelName"),
222
+ messages: messages || undefined,
223
+ temperature: 1, // TODO: Make this configurable
224
+ stream: true,
225
+ };
226
+ return { requestOptions, requestBody };
227
+ }
228
+ }
229
+ exports.SymmetryClient = SymmetryClient;
230
+ exports.default = SymmetryClient;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigManager = void 0;
7
+ exports.createConfigManager = createConfigManager;
8
+ exports.createConfigManagerClient = createConfigManagerClient;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ class ConfigManager {
12
+ constructor(configPath) {
13
+ const configFile = fs_1.default.readFileSync(configPath, "utf8");
14
+ const loadedConfig = js_yaml_1.default.load(configFile);
15
+ this.config = {
16
+ ...loadedConfig,
17
+ type: "client"
18
+ };
19
+ this.validate();
20
+ }
21
+ validate() {
22
+ this.validateClientConfig(this.config);
23
+ }
24
+ validateClientConfig(config) {
25
+ const requiredFields = [
26
+ "apiHostname",
27
+ "apiPath",
28
+ "apiPort",
29
+ "apiProtocol",
30
+ "apiProvider",
31
+ "modelName",
32
+ "path",
33
+ "public",
34
+ "serverKey",
35
+ ];
36
+ for (const field of requiredFields) {
37
+ if (!(field in config)) {
38
+ throw new Error(`Missing required field in client configuration: ${field}`);
39
+ }
40
+ }
41
+ if (typeof config.public !== "boolean") {
42
+ throw new Error('The "public" field in client configuration must be a boolean');
43
+ }
44
+ }
45
+ get(key) {
46
+ return this.config[key];
47
+ }
48
+ isClientConfig() {
49
+ return this.config.type === "client";
50
+ }
51
+ }
52
+ exports.ConfigManager = ConfigManager;
53
+ function createConfigManager(configPath) {
54
+ return new ConfigManager(configPath);
55
+ }
56
+ function createConfigManagerClient(configPath) {
57
+ return new ConfigManager(configPath);
58
+ }
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConfigManager = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ class ConfigManager {
10
+ constructor(configPath) {
11
+ const configFile = fs_1.default.readFileSync(configPath, "utf8");
12
+ const config = js_yaml_1.default.load(configFile);
13
+ this.config = config;
14
+ this.validate();
15
+ }
16
+ getAll() {
17
+ return this.config;
18
+ }
19
+ validate() {
20
+ const requiredFields = [
21
+ "apiHostname",
22
+ "apiPath",
23
+ "apiPort",
24
+ "apiProtocol",
25
+ "apiProvider",
26
+ "modelName",
27
+ "path",
28
+ "public",
29
+ "serverKey",
30
+ ];
31
+ for (const field of requiredFields) {
32
+ if (!(field in this.config)) {
33
+ throw new Error(`Missing required field in client configuration: ${field}`);
34
+ }
35
+ }
36
+ if (typeof this.config.public !== "boolean") {
37
+ throw new Error('The "public" field in client configuration must be a boolean');
38
+ }
39
+ }
40
+ get(key) {
41
+ return this.config[key];
42
+ }
43
+ }
44
+ exports.ConfigManager = ConfigManager;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiProviders = exports.serverMessageKeys = exports.NORMALIZE_REGEX = void 0;
4
+ exports.NORMALIZE_REGEX = /\s*\r?\n|\r/g;
5
+ exports.serverMessageKeys = {
6
+ challenge: "challenge",
7
+ conectionSize: "conectionSize",
8
+ heartbeat: "heartbeat",
9
+ inference: "inference",
10
+ inferenceEnded: "inferenceEnded",
11
+ join: "join",
12
+ joinAck: "joinAck",
13
+ leave: "leave",
14
+ newConversation: "newConversation",
15
+ ping: "ping",
16
+ pong: "pong",
17
+ providerDetails: "providerDetails",
18
+ reportCompletion: "reportCompletion",
19
+ requestProvider: "requestProvider",
20
+ sessionValid: "sessionValid",
21
+ verifySession: "verifySession",
22
+ };
23
+ exports.apiProviders = {
24
+ LiteLLM: 'litellm',
25
+ LlamaCpp: 'llamacpp',
26
+ LMStudio: 'lmstudio',
27
+ Ollama: 'ollama',
28
+ Oobabooga: 'oobabooga',
29
+ OpenWebUI: 'openwebui',
30
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.database = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const sqlite3_1 = __importDefault(require("sqlite3"));
9
+ exports.database = new sqlite3_1.default.Database(path_1.default.join(__dirname, "../sqlite.db"));
package/dist/logger.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = exports.Logger = exports.LogLevel = void 0;
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ var LogLevel;
10
+ (function (LogLevel) {
11
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
12
+ LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
13
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
14
+ LogLevel[LogLevel["WARNING"] = 3] = "WARNING";
15
+ })(LogLevel || (exports.LogLevel = LogLevel = {}));
16
+ class Logger {
17
+ constructor() {
18
+ this.logLevel = LogLevel.INFO;
19
+ }
20
+ static getInstance() {
21
+ if (!Logger.instance) {
22
+ Logger.instance = new Logger();
23
+ }
24
+ return Logger.instance;
25
+ }
26
+ setLogLevel(level) {
27
+ this.logLevel = level;
28
+ }
29
+ info(message, ...args) {
30
+ if (this.logLevel <= LogLevel.INFO) {
31
+ console.log(chalk_1.default.blue("ℹ️ INFO:"), message, ...args);
32
+ }
33
+ }
34
+ warning(message, ...args) {
35
+ console.log(chalk_1.default.yellow("⚠️ WARNING:"), message, ...args);
36
+ }
37
+ error(message, ...args) {
38
+ console.error(chalk_1.default.red("❌ ERROR:"), message, ...args);
39
+ }
40
+ debug(message, ...args) {
41
+ console.log(chalk_1.default.gray("🐛 DEBUG:"), message, ...args);
42
+ }
43
+ }
44
+ exports.Logger = Logger;
45
+ exports.logger = Logger.getInstance();
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PeerRepository = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const database_1 = require("./database");
9
+ const logger_1 = require("./logger");
10
+ class PeerRepository {
11
+ constructor() {
12
+ this.db = database_1.database;
13
+ }
14
+ upsert(message) {
15
+ return new Promise((resolve, reject) => {
16
+ this.db.run(`
17
+ INSERT OR REPLACE INTO peers (
18
+ key, discovery_key, gpu_memory, model_name, public, server_key, last_seen, online
19
+ ) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, TRUE)
20
+ `, [
21
+ message.key,
22
+ message.discoveryKey,
23
+ message.config.gpuMemory,
24
+ message.config.modelName,
25
+ message.config.public,
26
+ message.config.serverKey,
27
+ ], function (err) {
28
+ if (err) {
29
+ reject(err);
30
+ }
31
+ else {
32
+ resolve(this.lastID);
33
+ }
34
+ });
35
+ });
36
+ }
37
+ getByDiscoveryKey(discoveryKey) {
38
+ return new Promise((resolve, reject) => {
39
+ this.db.get("SELECT * FROM peers WHERE discovery_key = ?", [discoveryKey], (err, row) => {
40
+ if (err) {
41
+ reject(err);
42
+ }
43
+ else {
44
+ resolve(row);
45
+ }
46
+ });
47
+ });
48
+ }
49
+ getPeer(randomPeerRequest) {
50
+ return new Promise((resolve, reject) => {
51
+ const { modelName } = randomPeerRequest;
52
+ this.db.get(`SELECT * FROM peers WHERE model_name = ? ORDER BY RANDOM() LIMIT 1`, [modelName], (err, row) => {
53
+ if (err) {
54
+ reject(err);
55
+ }
56
+ else {
57
+ resolve(row);
58
+ }
59
+ });
60
+ });
61
+ }
62
+ updateLastSeen(peerKey) {
63
+ return new Promise((resolve, reject) => {
64
+ this.db.run("UPDATE peers SET last_seen = ?, online = FALSE WHERE key = ?", [new Date().toISOString(), peerKey], function (err) {
65
+ if (err) {
66
+ console.error(chalk_1.default.red("❌ Error updating peer last seen in database:"), err);
67
+ reject(err);
68
+ }
69
+ else {
70
+ if (this.changes > 0) {
71
+ logger_1.logger.info(chalk_1.default.yellow("🕒 Peer disconnected"));
72
+ }
73
+ else {
74
+ logger_1.logger.info(chalk_1.default.yellow("⚠️ Peer not found in database"));
75
+ }
76
+ resolve(this.changes);
77
+ }
78
+ });
79
+ });
80
+ }
81
+ async getActivePeerCount() {
82
+ return new Promise((resolve, reject) => {
83
+ this.db.get("SELECT COUNT(*) as count FROM peers WHERE online = TRUE", (err, row) => {
84
+ if (err) {
85
+ reject(err);
86
+ }
87
+ else {
88
+ resolve(row.count);
89
+ }
90
+ });
91
+ });
92
+ }
93
+ async getActiveModelCount() {
94
+ return new Promise((resolve, reject) => {
95
+ this.db.get("SELECT COUNT(DISTINCT model_name) as count FROM peers WHERE online = TRUE", (err, row) => {
96
+ if (err) {
97
+ reject(err);
98
+ }
99
+ else {
100
+ resolve(row.count);
101
+ }
102
+ });
103
+ });
104
+ }
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ updateStats(peerKey, data) {
107
+ // TODO: Update stats in database
108
+ logger_1.logger.info(peerKey, data);
109
+ }
110
+ }
111
+ exports.PeerRepository = PeerRepository;
112
+ module.exports = {
113
+ PeerRepository,
114
+ };