@hzhangxyz/ddss 0.0.1-alpha1

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.
Binary file
package/main.ts ADDED
@@ -0,0 +1,472 @@
1
+ // 导入工具模块
2
+ import {
3
+ promisify, // 将回调函数转换为 Promise 的工具
4
+ } from "util";
5
+ import {
6
+ randomUUID, // 生成随机 UUID
7
+ } from "crypto";
8
+ import {
9
+ createInterface, // 创建readline接口用于读取stdin
10
+ } from "readline";
11
+ // 导入 gRPC 相关模块
12
+ import * as grpc from "@grpc/grpc-js"; // gRPC JavaScript 实现
13
+ // 导入 ATSDS(自适应树搜索数据结构)相关模块
14
+ import {
15
+ Search as Search_, // ATSDS 搜索引擎基类
16
+ Rule,
17
+ } from "atsds";
18
+ import {
19
+ parse, // 解析规则字符串为内部表示
20
+ unparse, // 将内部表示转换为规则字符串
21
+ } from "atsds-bnf";
22
+ // 导入生成的 Protocol Buffers 类型和服务
23
+ import {
24
+ Node,
25
+ JoinRequest,
26
+ JoinResponse,
27
+ LeaveRequest,
28
+ LeaveResponse,
29
+ MetaDataRequest,
30
+ MetaDataResponse,
31
+ PushDataRequest,
32
+ PushDataResponse,
33
+ PullDataRequest,
34
+ PullDataResponse,
35
+ EngineKind,
36
+ ClusterClient,
37
+ ClusterServer,
38
+ ClusterService,
39
+ EngineClient,
40
+ EngineServer,
41
+ EngineService,
42
+ } from "./ddss.js";
43
+
44
+ interface NodeClient {
45
+ cluster: ClusterClient;
46
+ engine: EngineClient;
47
+ }
48
+
49
+ interface NodeInfoWithClient {
50
+ id: string;
51
+ addr: string;
52
+ client: NodeClient;
53
+ }
54
+
55
+ /**
56
+ * 搜索引擎类
57
+ * 扩展 ATSDS 的 Search 类,提供字符串形式的规则处理和数据管理
58
+ */
59
+ class Search extends Search_ {
60
+ private data: Set<string>;
61
+
62
+ /**
63
+ * 构造函数
64
+ * @param {number} limit_size - Size of the buffer for storing the final objects (rules/facts) in the knowledge base (default: 1000)
65
+ * @param {number} buffer_size - Size of the buffer for internal operations like conversions and transformations (default: 10000)
66
+ */
67
+ constructor(limit_size: number = 1000, buffer_size: number = 10000) {
68
+ super(limit_size, buffer_size);
69
+ this.data = new Set(); // 存储已添加的数据(使用 Set 避免重复)
70
+ }
71
+
72
+ /**
73
+ * 输入规则到搜索引擎
74
+ * @param {string} rule - 规则字符串
75
+ */
76
+ input(rule: string): string | null {
77
+ const parsedRule = parse(rule); // 解析规则字符串
78
+ if (super.add(parsedRule)) {
79
+ const unparsedRule = unparse(parsedRule);
80
+ this.data.add(unparsedRule); // 添加成功则保存到数据集合
81
+ return unparsedRule;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * 执行搜索并输出结果
88
+ * @param {function} callback - 处理搜索结果的回调函数
89
+ * @returns {*} - 搜索执行结果
90
+ */
91
+ output(callback: (result: string) => boolean): number {
92
+ return super.execute((candidate: Rule) => {
93
+ const result = unparse(candidate.toString());
94
+ this.data.add(result); // 保存搜索结果到数据集合
95
+ return callback(result);
96
+ });
97
+ }
98
+
99
+ /**
100
+ * 获取所有已存储的数据
101
+ * @returns {Array<string>} - 数据数组
102
+ */
103
+ getData(): string[] {
104
+ return [...this.data];
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 集群节点类
110
+ * 管理分布式搜索引擎集群中的单个节点
111
+ */
112
+ class ClusterNode {
113
+ id: string;
114
+ addr: string;
115
+ engine: Search;
116
+ server: grpc.Server;
117
+ nodes: Map<string, NodeInfoWithClient>;
118
+
119
+ /**
120
+ * 构造函数
121
+ * @param {string} addr - 节点绑定地址
122
+ * @param {string} id - 节点唯一标识符,默认随机生成
123
+ * @param {number} limit_size - 搜索引擎的限制大小参数,默认 1000
124
+ * @param {number} buffer_size - 搜索引擎的缓冲区大小参数,默认 10000
125
+ */
126
+ constructor(
127
+ addr: string,
128
+ id: string = randomUUID(),
129
+ limit_size: number = 1000,
130
+ buffer_size: number = 10000,
131
+ ) {
132
+ this.id = id; // 节点 ID
133
+ this.addr = addr; // 节点地址
134
+ this.engine = new Search(limit_size, buffer_size); // 创建搜索引擎实例
135
+ this.server = new grpc.Server();
136
+ this.nodes = new Map(); // 存储集群中所有节点信息
137
+ // 将自己加入节点列表
138
+ this.nodes.set(id, {
139
+ id,
140
+ addr,
141
+ client: {
142
+ cluster: null as any,
143
+ engine: null as any,
144
+ },
145
+ });
146
+ }
147
+ /**
148
+ * 设置定时循环执行搜索任务
149
+ * 每秒执行一次搜索,并将新发现的数据推送到其他节点
150
+ */
151
+ private setupSearchLoop(): void {
152
+ const loop = async (): Promise<void> => {
153
+ const begin = Date.now();
154
+ const data: string[] = []; // 存储本轮搜索发现的新数据
155
+ // 执行搜索引擎,处理搜索结果
156
+ this.engine.output((result: string) => {
157
+ data.push(result); // 添加到待推送列表(结果已由 engine.output 自动保存)
158
+ console.log(`Found data: ${result}`);
159
+ return false; // 继续搜索
160
+ });
161
+ // 如果发现新数据,推送到所有其他节点
162
+ if (data.length > 0) {
163
+ for (const id of this.nodes.keys()) {
164
+ if (id !== this.id) {
165
+ // 排除自己
166
+ const node = this.nodes.get(id)!;
167
+ const pushDataAsync = promisify<PushDataRequest, PushDataResponse>(
168
+ node.client.engine.pushData,
169
+ ).bind(node.client.engine);
170
+ await pushDataAsync({
171
+ data,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ const end = Date.now();
177
+ const cost = end - begin; // 计算本轮执行耗时
178
+ const waiting = Math.max(1000 - cost, 0); // 计算等待时间,确保每秒执行一次
179
+ setTimeout(loop, waiting); // 安排下次执行
180
+ };
181
+ setTimeout(loop, 0);
182
+ }
183
+ /**
184
+ * 设置stdin读取循环
185
+ * 持续读取标准输入,每收到一行时将其输入到搜索引擎并推送到所有其他节点
186
+ */
187
+ private setupStdinLoop(): ReturnType<typeof createInterface> {
188
+ const rl = createInterface({
189
+ input: process.stdin,
190
+ output: process.stdout,
191
+ terminal: false,
192
+ });
193
+ rl.on("line", async (line: string) => {
194
+ // 去除空白字符
195
+ const trimmedLine = line.trim();
196
+ if (trimmedLine.length === 0) {
197
+ return; // 跳过空行
198
+ }
199
+ // 将输入喂给搜索引擎
200
+ const formattedLine = this.engine.input(trimmedLine);
201
+ if (formattedLine === null) {
202
+ return; // 已存在则跳过
203
+ }
204
+ console.log(`Received input from stdin: ${formattedLine}`);
205
+ // 推送到所有其他节点(排除自己)
206
+ for (const id of this.nodes.keys()) {
207
+ if (id !== this.id) {
208
+ // 排除自己
209
+ const node = this.nodes.get(id)!;
210
+ const pushDataAsync = promisify<PushDataRequest, PushDataResponse>(
211
+ node.client.engine.pushData,
212
+ ).bind(node.client.engine);
213
+ await pushDataAsync({
214
+ data: [formattedLine],
215
+ });
216
+ }
217
+ }
218
+ });
219
+ return rl;
220
+ }
221
+ /**
222
+ * 创建节点信息对象
223
+ * @param {string} id - 节点 ID
224
+ * @param {string} addr - 节点地址
225
+ * @returns {Object} 包含节点信息和 gRPC 客户端的对象
226
+ */
227
+ private nodeInfo(id: string, addr: string): NodeInfoWithClient {
228
+ return {
229
+ id: id,
230
+ addr: addr,
231
+ client: {
232
+ // 创建集群管理服务客户端
233
+ cluster: new ClusterClient(addr, grpc.credentials.createInsecure()),
234
+ // 创建引擎服务客户端
235
+ engine: new EngineClient(addr, grpc.credentials.createInsecure()),
236
+ },
237
+ };
238
+ }
239
+ /**
240
+ * 启动节点监听服务
241
+ * 创建 gRPC 服务器并注册集群管理和引擎服务
242
+ * @returns {ClusterNode} 返回当前节点实例
243
+ */
244
+ async listen(): Promise<ClusterNode> {
245
+ // 注册集群管理服务
246
+ this.server.addService(ClusterService, {
247
+ /**
248
+ * 处理节点加入请求
249
+ * 将请求节点加入本地节点列表,并返回当前所有节点信息
250
+ */
251
+ join: async (
252
+ call: grpc.ServerUnaryCall<JoinRequest, JoinResponse>,
253
+ callback: grpc.sendUnaryData<JoinResponse>,
254
+ ) => {
255
+ const node = call.request.node!;
256
+ const { id, addr } = node;
257
+ if (!this.nodes.has(id)) {
258
+ this.nodes.set(id, this.nodeInfo(id, addr));
259
+ console.log(`Joined with node ${id} at ${addr}`);
260
+ }
261
+ const nodes = [...this.nodes.values()].map((n) => ({
262
+ id: n.id,
263
+ addr: n.addr,
264
+ }));
265
+ callback(null, {
266
+ nodes,
267
+ });
268
+ },
269
+ /**
270
+ * 处理节点离开请求
271
+ * 将请求节点从本地节点列表中移除
272
+ */
273
+ leave: async (
274
+ call: grpc.ServerUnaryCall<LeaveRequest, LeaveResponse>,
275
+ callback: grpc.sendUnaryData<LeaveResponse>,
276
+ ) => {
277
+ const node = call.request.node!;
278
+ const { id, addr } = node;
279
+ if (this.nodes.has(id)) {
280
+ this.nodes.delete(id);
281
+ console.log(`Left node ${id} at ${addr}`);
282
+ }
283
+ callback(null, {});
284
+ },
285
+ });
286
+ // 注册引擎服务
287
+ this.server.addService(EngineService, {
288
+ /**
289
+ * 处理元数据查询请求
290
+ * 返回引擎的元数据信息
291
+ */
292
+ metaData: async (
293
+ call: grpc.ServerUnaryCall<MetaDataRequest, MetaDataResponse>,
294
+ callback: grpc.sendUnaryData<MetaDataResponse>,
295
+ ) => {
296
+ callback(null, {
297
+ metadata: {
298
+ id: this.id,
299
+ kind: EngineKind.EAGER, // 积极模式
300
+ input: ["`x"], // 输入模式
301
+ output: ["`x"], // 输出模式
302
+ },
303
+ });
304
+ },
305
+ /**
306
+ * 处理数据推送请求
307
+ * 接收其他节点推送的数据并添加到本地搜索引擎
308
+ */
309
+ pushData: async (
310
+ call: grpc.ServerUnaryCall<PushDataRequest, PushDataResponse>,
311
+ callback: grpc.sendUnaryData<PushDataResponse>,
312
+ ) => {
313
+ const data = call.request.data!;
314
+ for (const item of data) {
315
+ console.log(`Received data: ${item}`);
316
+ this.engine.input(item);
317
+ }
318
+ callback(null, {});
319
+ },
320
+ /**
321
+ * 处理数据拉取请求
322
+ * 返回本地存储的所有数据
323
+ */
324
+ pullData: async (
325
+ call: grpc.ServerUnaryCall<PullDataRequest, PullDataResponse>,
326
+ callback: grpc.sendUnaryData<PullDataResponse>,
327
+ ) => {
328
+ callback(null, {
329
+ data: this.engine.getData(),
330
+ });
331
+ },
332
+ });
333
+ // 绑定服务器到指定地址
334
+ const bindAsync = promisify<string, grpc.ServerCredentials, number>(
335
+ this.server.bindAsync,
336
+ ).bind(this.server);
337
+ await bindAsync(this.addr, grpc.ServerCredentials.createInsecure());
338
+ this.setupSearchLoop();
339
+ const rl = this.setupStdinLoop();
340
+ // 注册进程终止信号处理器,确保优雅退出
341
+ process.on("SIGINT", async () => {
342
+ rl.close(); // 关闭 readline 接口
343
+ await this.leave(); // 通知其他节点本节点离开
344
+ process.exit(0);
345
+ });
346
+ // 注册 SIGUSR1 信号处理器,打印所有节点信息
347
+ process.on("SIGUSR1", () => {
348
+ console.log("=== All Nodes Information ===");
349
+ for (const [id, nodeInfo] of this.nodes.entries()) {
350
+ console.log(`Node ID: ${id}`);
351
+ console.log(` Address: ${nodeInfo.addr}`);
352
+ }
353
+ console.log(`Total nodes: ${this.nodes.size}`);
354
+ console.log("=============================");
355
+ });
356
+ // 注册 SIGUSR2 信号处理器,打印所有数据
357
+ process.on("SIGUSR2", () => {
358
+ console.log("=== All Data Managed by Search ===");
359
+ const data = this.engine.getData();
360
+ data.forEach((item, index) => {
361
+ console.log(`[${index + 1}] ${item}`);
362
+ });
363
+ console.log(`Total data items: ${data.length}`);
364
+ console.log("==================================");
365
+ });
366
+ console.log(`Node ${this.id} listening on ${this.addr}`);
367
+ return this;
368
+ }
369
+ /**
370
+ * 加入现有集群
371
+ * @param {string} addr - 要加入的集群中某个节点的地址
372
+ */
373
+ async join(addr: string): Promise<void> {
374
+ // 创建目标节点的集群服务客户端
375
+ const client = new ClusterClient(addr, grpc.credentials.createInsecure());
376
+ const joinAsync = promisify<JoinRequest, JoinResponse>(client.join).bind(
377
+ client,
378
+ );
379
+ // 向目标节点发送加入请求
380
+ const response = await joinAsync({
381
+ node: this,
382
+ });
383
+ // 遍历集群中的所有节点
384
+ const localData = this.engine.getData();
385
+ for (const node of response.nodes) {
386
+ if (!this.nodes.has(node.id)) {
387
+ // 创建节点信息并保存
388
+ const nodeInfo = this.nodeInfo(node.id, node.addr);
389
+ this.nodes.set(node.id, nodeInfo);
390
+ // 向该节点发送加入请求
391
+ const nodeJoinAsync = promisify<JoinRequest, JoinResponse>(
392
+ nodeInfo.client.cluster.join,
393
+ ).bind(nodeInfo.client.cluster);
394
+ await nodeJoinAsync({
395
+ node: this,
396
+ });
397
+ // 如果本地有数据,推送给新节点
398
+ if (localData.length > 0) {
399
+ const nodePushAsync = promisify<PushDataRequest, PushDataResponse>(
400
+ nodeInfo.client.engine.pushData,
401
+ ).bind(nodeInfo.client.engine);
402
+ await nodePushAsync({
403
+ data: localData,
404
+ });
405
+ }
406
+ // 从该节点拉取历史数据
407
+ const nodePullAsync = promisify<PullDataRequest, PullDataResponse>(
408
+ nodeInfo.client.engine.pullData,
409
+ ).bind(nodeInfo.client.engine);
410
+ const dataResponse = await nodePullAsync({});
411
+ if (dataResponse.data) {
412
+ for (const item of dataResponse.data) {
413
+ console.log(`Receiving data: ${item}`);
414
+ this.engine.input(item); // 添加到搜索引擎(数据自动保存)
415
+ }
416
+ }
417
+ console.log(`Joining node ${node.id} at ${node.addr}`);
418
+ }
419
+ }
420
+ }
421
+ /**
422
+ * 离开集群
423
+ * 向所有其他节点发送离开通知
424
+ */
425
+ async leave(): Promise<void> {
426
+ for (const id of this.nodes.keys()) {
427
+ if (id !== this.id) {
428
+ // 排除自己
429
+ const node = this.nodes.get(id)!;
430
+ const leaveAsync = promisify<LeaveRequest, LeaveResponse>(
431
+ node.client.cluster.leave,
432
+ ).bind(node.client.cluster);
433
+ await leaveAsync({
434
+ node: this,
435
+ });
436
+ console.log(`Leaving node ${node.id} at ${node.addr}`);
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ // ============================================================
443
+ // 主程序入口
444
+ // ============================================================
445
+
446
+ // 命令行参数检查
447
+ // 用法: node dist/main.mjs <bind_addr> [<join_addr>]
448
+ // bind_addr: 本节点绑定的端口号
449
+ // join_addr: (可选) 要加入的集群中某个节点的端口号
450
+ if (process.argv.length < 3 || process.argv.length > 4) {
451
+ console.error("Usage: main <bind_addr> [<join_addr>]");
452
+ process.exit(1);
453
+ }
454
+
455
+ // 场景一: 加入现有集群
456
+ // 启动节点并加入指定地址的集群
457
+ if (process.argv.length === 4) {
458
+ console.log(
459
+ `Starting node at ${process.argv[2]} and joining ${process.argv[3]}`,
460
+ );
461
+ const node = new ClusterNode(`0.0.0.0:${process.argv[2]}`);
462
+ await node.listen(); // 启动服务监听
463
+ await node.join(`0.0.0.0:${process.argv[3]}`); // 加入集群
464
+ }
465
+
466
+ // 场景二: 创建新集群
467
+ // 启动第一个节点,作为集群的初始节点
468
+ if (process.argv.length === 3) {
469
+ console.log(`Starting node at ${process.argv[2]}`);
470
+ const node = new ClusterNode(`0.0.0.0:${process.argv[2]}`);
471
+ await node.listen(); // 启动服务监听
472
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@hzhangxyz/ddss",
3
+ "version": "v0.0.1-alpha1",
4
+ "type": "module",
5
+ "bin": "dist/main.mjs",
6
+ "main": "dist/main.mjs",
7
+ "module": "dist/main.mjs",
8
+ "scripts": {
9
+ "proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. --ts_proto_opt=outputServices=grpc-js,env=node ./ddss.proto",
10
+ "rollup": "rollup -c rollup.config.js",
11
+ "build": "run-s proto rollup"
12
+ },
13
+ "devDependencies": {
14
+ "@grpc/grpc-js": "^1.14.3",
15
+ "@rollup/plugin-commonjs": "^29.0.0",
16
+ "@rollup/plugin-json": "^6.1.0",
17
+ "@rollup/plugin-node-resolve": "^16.0.3",
18
+ "@rollup/plugin-terser": "^0.4.4",
19
+ "@rollup/plugin-typescript": "^12.3.0",
20
+ "@types/node": "^25.0.1",
21
+ "atsds": "^0.0.6",
22
+ "atsds-bnf": "^0.0.6",
23
+ "npm-run-all": "^4.1.5",
24
+ "rollup": "^4.53.3",
25
+ "ts-proto": "^2.8.3",
26
+ "tslib": "^2.8.1",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ import commonjs from "@rollup/plugin-commonjs";
2
+ import resolve from "@rollup/plugin-node-resolve";
3
+ import typescript from "@rollup/plugin-typescript";
4
+ import terser from "@rollup/plugin-terser";
5
+ import json from "@rollup/plugin-json";
6
+
7
+ export default {
8
+ input: "main.ts",
9
+ output: {
10
+ file: "dist/main.js",
11
+ format: "es",
12
+ },
13
+ plugins: [
14
+ resolve(),
15
+ commonjs(),
16
+ json(),
17
+ typescript(),
18
+ terser()
19
+ ]
20
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "isolatedModules": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["main.ts", "ddss.ts"]
12
+ }