Skip to content

Commit 9593aa3

Browse files
author
MesTTo
committed
feat(das-client): generate gRPC stubs from das-proto + real BusNode (ping/execute_message), in-process tested
1 parent 09ed115 commit 9593aa3

15 files changed

Lines changed: 2106 additions & 5 deletions

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
packages/core/src/prelude.ts
44
corpus/**
55
pnpm-lock.yaml
6+
7+
packages/das-client/src/gen/
8+
packages/das-client/proto/

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import tseslint from "typescript-eslint";
22
import prettier from "eslint-config-prettier";
33

44
export default tseslint.config(
5-
{ ignores: ["**/dist/**", "**/*.config.*", "packages/*/src/prelude.ts", "docs/**", "corpus/**"] },
5+
{ ignores: ["**/dist/**", "**/*.config.*", "packages/*/src/prelude.ts", "docs/**", "corpus/**", "**/src/gen/**"] },
66
...tseslint.configs.recommended,
77
{
88
files: ["packages/*/src/**/*.ts"],

packages/das-client/package.json

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,25 @@
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
7-
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
8-
"files": ["dist"],
9-
"scripts": { "build": "tsup src/index.ts --format esm --dts --clean" },
10-
"dependencies": { "@metta-ts/core": "workspace:*" }
7+
"exports": {
8+
".": {
9+
"types": "./dist/index.d.ts",
10+
"default": "./dist/index.js"
11+
}
12+
},
13+
"files": [
14+
"dist"
15+
],
16+
"scripts": {
17+
"build": "tsup src/index.ts --format esm --dts --clean",
18+
"gen": "protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/gen --ts_proto_opt=outputServices=grpc-js,esModuleInterop=true,forceLong=string -I proto proto/*.proto"
19+
},
20+
"dependencies": {
21+
"@metta-ts/core": "workspace:*",
22+
"@grpc/grpc-js": "^1.14.4",
23+
"@bufbuild/protobuf": "^2.2.3"
24+
},
25+
"devDependencies": {
26+
"ts-proto": "^2.11.8"
27+
}
1128
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
syntax = "proto3";
2+
import "common.proto";
3+
package dasproto;
4+
5+
message HandleList {
6+
repeated string list = 1;
7+
int32 request_type = 2;
8+
int64 hebbian_network = 3;
9+
string context = 4;
10+
}
11+
12+
message HandleListList {
13+
repeated HandleList list = 1;
14+
int32 request_type = 2;
15+
int64 hebbian_network = 3;
16+
string context = 4;
17+
}
18+
19+
message HandleCount {
20+
map<string, uint32> map = 1;
21+
int32 request_type = 2;
22+
int64 hebbian_network = 3;
23+
string context = 4;
24+
}
25+
26+
message ImportanceList {
27+
repeated float list = 1;
28+
}
29+
30+
message Parameters {
31+
float rent_rate = 1;
32+
float spreading_rate_lowerbound = 2;
33+
float spreading_rate_upperbound = 3;
34+
}
35+
36+
message ContextPersistence {
37+
string context = 1;
38+
string file_name = 2;
39+
}
40+
41+
service AttentionBroker {
42+
rpc ping (Empty) returns (Ack) {}
43+
rpc stimulate (HandleCount) returns (Ack) {}
44+
rpc correlate (HandleList) returns (Ack) {}
45+
rpc asymmetric_correlate (HandleList) returns (Ack) {}
46+
rpc get_importance (HandleList) returns (ImportanceList) {}
47+
rpc set_determiners (HandleListList) returns (Ack) {}
48+
rpc set_parameters (Parameters) returns (Ack) {}
49+
rpc save_context (ContextPersistence) returns (Ack) {}
50+
rpc drop_and_load_context (ContextPersistence) returns (Ack) {}
51+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
syntax = "proto3";
2+
package dasproto;
3+
4+
message Empty {}
5+
6+
message Ack {
7+
bool error = 1;
8+
string msg = 2;
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
syntax = "proto3";
2+
import "common.proto";
3+
package dasproto;
4+
5+
message MessageData {
6+
string command = 1;
7+
repeated string args = 2;
8+
string sender = 3;
9+
bool is_broadcast = 4;
10+
repeated string visited_recipients = 5;
11+
}
12+
13+
service DistributedAlgorithmNode {
14+
rpc ping (Empty) returns (Ack) {}
15+
rpc execute_message (MessageData) returns (Empty) {}
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
syntax = "proto3";
2+
3+
package echo;
4+
5+
service Echo {
6+
rpc echo (EchoRequest) returns (EchoResponse) {}
7+
}
8+
9+
message EchoRequest {
10+
string msg = 1;
11+
}
12+
13+
message EchoResponse {
14+
string msg = 1;
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect, afterEach } from "vitest";
2+
import { BusNode } from "./bus-node";
3+
import type { MessageData } from "./gen/distributed_algorithm_node";
4+
5+
// In-process gRPC: two bus nodes exchange a ping and an execute_message command over the real
6+
// das-proto wire (no live DAS needed — this verifies the gRPC bus layer end to end).
7+
let nodes: BusNode[] = [];
8+
afterEach(async () => {
9+
await Promise.all(nodes.map((n) => n.stop()));
10+
nodes = [];
11+
});
12+
13+
describe("BusNode (real gRPC over das-proto)", () => {
14+
it("ping returns the server's Ack", async () => {
15+
const a = new BusNode("127.0.0.1:0");
16+
nodes.push(a);
17+
// bind on an ephemeral port via the OS, then ping ourselves on a fixed test port
18+
const server = new BusNode("127.0.0.1:52771");
19+
nodes.push(server);
20+
await server.start();
21+
const ack = await a.ping("127.0.0.1:52771");
22+
expect(ack.error).toBe(false);
23+
expect(ack.msg).toBe("pong");
24+
});
25+
26+
it("execute_message delivers a command to the peer", async () => {
27+
let received: MessageData | undefined;
28+
const server = new BusNode("127.0.0.1:52772", (m) => (received = m));
29+
nodes.push(server);
30+
await server.start();
31+
const client = new BusNode("127.0.0.1:0");
32+
nodes.push(client);
33+
await client.send("127.0.0.1:52772", {
34+
command: "bus_command_proxy",
35+
args: ["pattern-matching-query", "(Similarity human $s)"],
36+
sender: "127.0.0.1:0",
37+
isBroadcast: false,
38+
visitedRecipients: [],
39+
});
40+
expect(received?.command).toBe("bus_command_proxy");
41+
expect(received?.args[1]).toBe("(Similarity human $s)");
42+
});
43+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// A DAS bus node over the generated gRPC stubs (das-proto `DistributedAlgorithmNode`). This is the
2+
// real wire layer the live transport is built on: a participant hosts an inbound node (a gRPC
3+
// server) and sends `execute_message` commands to peers. The full pattern-matching choreography
4+
// (the proxy that issues a `bus_command_proxy` and assembles the streamed `query_answer_tokens_flow`)
5+
// is ported on top of this from the Python `hyperon_das` service_bus/proxy; this layer is what makes
6+
// a node reachable and lets it ping/exchange messages. Node-only.
7+
import {
8+
Server,
9+
ServerCredentials,
10+
credentials,
11+
type ServerUnaryCall,
12+
type sendUnaryData,
13+
} from "@grpc/grpc-js";
14+
import {
15+
DistributedAlgorithmNodeClient,
16+
DistributedAlgorithmNodeService,
17+
type MessageData,
18+
} from "./gen/distributed_algorithm_node";
19+
import { type Empty, type Ack } from "./gen/common";
20+
21+
/** The bus command vocabulary (from the Python/Rust clients' `service_bus`). */
22+
export const BusCommand = {
23+
ping: "ping",
24+
ack: "ack",
25+
nodeJoinedNetwork: "node_joined_network",
26+
busCommandProxy: "bus_command_proxy",
27+
queryAnswerTokensFlow: "query_answer_tokens_flow",
28+
} as const;
29+
30+
export type MessageHandler = (msg: MessageData) => void;
31+
32+
/** An inbound bus node: a gRPC server implementing `DistributedAlgorithmNode`, plus the ability to
33+
* send `execute_message` to peers. */
34+
export class BusNode {
35+
private server?: Server;
36+
constructor(
37+
readonly address: string,
38+
private readonly onMessage: MessageHandler = () => {},
39+
) {}
40+
41+
/** Start the inbound gRPC server. Resolves once it is listening. */
42+
start(): Promise<void> {
43+
const server = new Server();
44+
server.addService(DistributedAlgorithmNodeService, {
45+
ping: (_call: ServerUnaryCall<Empty, Ack>, cb: sendUnaryData<Ack>) =>
46+
cb(null, { error: false, msg: "pong" }),
47+
executeMessage: (call: ServerUnaryCall<MessageData, Empty>, cb: sendUnaryData<Empty>) => {
48+
this.onMessage(call.request);
49+
cb(null, {});
50+
},
51+
});
52+
this.server = server;
53+
return new Promise((resolve, reject) => {
54+
server.bindAsync(this.address, ServerCredentials.createInsecure(), (err) =>
55+
err ? reject(err) : resolve(),
56+
);
57+
});
58+
}
59+
60+
/** Send a command message to a peer node and await its acknowledgement. */
61+
send(peer: string, message: MessageData): Promise<void> {
62+
const client = new DistributedAlgorithmNodeClient(peer, credentials.createInsecure());
63+
return new Promise((resolve, reject) => {
64+
client.executeMessage(message, (err) => {
65+
client.close();
66+
if (err) reject(err);
67+
else resolve();
68+
});
69+
});
70+
}
71+
72+
/** Ping a peer node; resolves with its `Ack`. */
73+
ping(peer: string): Promise<Ack> {
74+
const client = new DistributedAlgorithmNodeClient(peer, credentials.createInsecure());
75+
return new Promise((resolve, reject) => {
76+
client.ping({}, (err, ack) => {
77+
client.close();
78+
if (err) reject(err);
79+
else resolve(ack);
80+
});
81+
});
82+
}
83+
84+
stop(): Promise<void> {
85+
return new Promise((resolve) => {
86+
if (!this.server) return resolve();
87+
this.server.tryShutdown(() => resolve());
88+
});
89+
}
90+
}

0 commit comments

Comments
 (0)