kuberpc-node 2.0.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,185 @@
1
+ # kubeRPC - Node.js SDK
2
+
3
+ Node.js SDK for [kubeRPC](https://github.com/darksuei/kubeRPC). Register callable methods on your service and invoke methods on other services over a persistent TCP connection with MessagePack framing.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install kuberpc-node
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Kubernetes usage (zero-config)
16
+
17
+ When kubeRPC core is deployed with the admission webhook enabled, annotate your pod and all required environment variables are injected automatically at pod creation time:
18
+
19
+ ```yaml
20
+ annotations:
21
+ kuberpc.suei.io/enabled: "true" # inject KUBERPC_CORE_URL
22
+ kuberpc.suei.io/service: "my-service" # inject KUBERPC_SERVICE_NAME, KUBERPC_HOST, KUBERPC_PORT
23
+ kuberpc.suei.io/port: "7749" # optional - defaults to 7749
24
+ ```
25
+
26
+ With those env vars present, the constructor takes no arguments:
27
+
28
+ ```js
29
+ import { KubeRPC } from "kuberpc-node";
30
+
31
+ const rpc = new KubeRPC();
32
+ ```
33
+
34
+ > The Kubernetes Service fronting your pod **must be named to match** `kuberpc.suei.io/service`. That name is what kubeRPC core stores as the reachable host for your service.
35
+
36
+ ---
37
+
38
+ ## Non-Kubernetes usage
39
+
40
+ Pass configuration explicitly. These values override any environment variables.
41
+
42
+ ```js
43
+ import { KubeRPC } from "kuberpc-node";
44
+
45
+ const rpc = new KubeRPC({
46
+ coreURL: "http://localhost:8080", // kubeRPC core endpoint
47
+ serviceName: "my-service", // unique name for this service
48
+ host: "localhost", // address other services use to reach this one
49
+ port: 7749, // TCP port this service listens on for inbound RPC calls
50
+ });
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Register a method
56
+
57
+ Expose a callable method. The TCP listener starts on the first `register()` call, and the service is registered with kubeRPC core.
58
+
59
+ ```js
60
+ await rpc.register("getUser", async ({ id }) => {
61
+ return db.users.findById(id);
62
+ });
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Call a method
68
+
69
+ Get a proxy for a target service, then call a method on it. The first call resolves the endpoint from kubeRPC core and opens a persistent TCP connection. Subsequent calls reuse the connection.
70
+
71
+ ```js
72
+ const userService = rpc.service("user-service");
73
+
74
+ const user = await userService.call("getUser", { id: "123" });
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Full example
80
+
81
+ **service-a** - registers a method:
82
+
83
+ ```js
84
+ import { KubeRPC } from "kuberpc-node";
85
+
86
+ // In Kubernetes: no args needed, env vars are injected by the webhook.
87
+ // Outside Kubernetes: pass config explicitly.
88
+ const rpc = new KubeRPC({
89
+ coreURL: "http://localhost:8080",
90
+ serviceName: "service-a",
91
+ host: "localhost",
92
+ port: 7749,
93
+ });
94
+
95
+ await rpc.register("greet", async ({ name }) => {
96
+ return `Hello, ${name}!`;
97
+ });
98
+ ```
99
+
100
+ **service-b** - calls the method:
101
+
102
+ ```js
103
+ import { KubeRPC } from "kuberpc-node";
104
+
105
+ const rpc = new KubeRPC({
106
+ coreURL: "http://localhost:8080",
107
+ serviceName: "service-b",
108
+ host: "localhost",
109
+ port: 7750,
110
+ });
111
+
112
+ const serviceA = rpc.service("service-a");
113
+ const message = await serviceA.call("greet", { name: "world" });
114
+
115
+ console.log(message); // Hello, world!
116
+
117
+ rpc.close();
118
+ ```
119
+
120
+ ---
121
+
122
+ ## API reference
123
+
124
+ ### `new KubeRPC(config?)`
125
+
126
+ All fields are optional. Environment variables are the fallback when a field is omitted.
127
+
128
+ | Field | Env var | Default | Description |
129
+ |---|---|---|---|
130
+ | `coreURL` | `KUBERPC_CORE_URL` | - | kubeRPC core base URL. **Required** (via config or env). |
131
+ | `serviceName` | `KUBERPC_SERVICE_NAME` | `""` | Name this service registers under. Required when calling `register()`. |
132
+ | `host` | `KUBERPC_HOST` | `"localhost"` | Address other services use to reach this one. |
133
+ | `port` | `KUBERPC_PORT` | `7749` | TCP port for inbound RPC calls. |
134
+
135
+ ---
136
+
137
+ ### `rpc.register(name, handler)`
138
+
139
+ Registers a method and starts the TCP listener if not already running. Returns a `Promise<void>` that resolves once the method is registered with kubeRPC core.
140
+
141
+ ```ts
142
+ await rpc.register("methodName", async (args) => {
143
+ return result;
144
+ });
145
+ ```
146
+
147
+ ---
148
+
149
+ ### `rpc.service(name): ServiceProxy`
150
+
151
+ Returns a lightweight proxy for a named service. Does not make any network calls.
152
+
153
+ ```ts
154
+ const svc = rpc.service("other-service");
155
+ ```
156
+
157
+ ---
158
+
159
+ ### `ServiceProxy.call(method, args?): Promise<any>`
160
+
161
+ Invokes a method on the target service. Resolves the endpoint from kubeRPC core on the first call (cached for subsequent calls), then sends the request over a persistent TCP connection.
162
+
163
+ ```ts
164
+ const result = await svc.call("methodName", { key: "value" });
165
+ ```
166
+
167
+ ---
168
+
169
+ ### `rpc.close()`
170
+
171
+ Closes all outbound TCP connections, destroys the inbound TCP listener, and clears internal state. Call this on graceful shutdown.
172
+
173
+ ```ts
174
+ rpc.close();
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Transport behaviour
180
+
181
+ - **Wire format**: length-prefixed MessagePack frames. Each frame is a 4-byte big-endian length followed by a MessagePack-encoded positional array.
182
+ - **Connection pooling**: one persistent TCP socket per target service, opened on first call and reused for all subsequent calls.
183
+ - **Endpoint cache**: service host/port is resolved from kubeRPC core once and cached. The cache is invalidated if the connection drops.
184
+ - **Concurrency**: concurrent calls to the same service are serialised through an internal queue. If you need parallel throughput across services, each target service gets its own independent queue and connection.
185
+ - **Reconnect**: if the socket drops, the next call attempts a single reconnect. There are no automatic retries beyond that - error handling is left to the caller.
@@ -0,0 +1,7 @@
1
+ export declare class KubeRpcError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string, message: string);
4
+ static methodNotFound(method: string, service: string): KubeRpcError;
5
+ static connectionFailed(host: string, port: number): KubeRpcError;
6
+ static coreUnreachable(url: string): KubeRpcError;
7
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KubeRpcError = void 0;
4
+ class KubeRpcError extends Error {
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "KubeRpcError";
9
+ Object.setPrototypeOf(this, KubeRpcError.prototype);
10
+ }
11
+ static methodNotFound(method, service) {
12
+ return new KubeRpcError("METHOD_NOT_FOUND", `Method "${method}" not found in service "${service}"`);
13
+ }
14
+ static connectionFailed(host, port) {
15
+ return new KubeRpcError("CONNECTION_FAILED", `Failed to connect to ${host}:${port}`);
16
+ }
17
+ static coreUnreachable(url) {
18
+ return new KubeRpcError("CORE_UNREACHABLE", `kubeRPC core unreachable at ${url}`);
19
+ }
20
+ }
21
+ exports.KubeRpcError = KubeRpcError;
@@ -0,0 +1,3 @@
1
+ export { KubeRPC, ServiceProxy } from "./kuberpc";
2
+ export { KubeRpcError } from "./errors";
3
+ export type { KubeRPCConfig, Handler } from "./@types";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KubeRpcError = exports.ServiceProxy = exports.KubeRPC = void 0;
4
+ var kuberpc_1 = require("./kuberpc");
5
+ Object.defineProperty(exports, "KubeRPC", { enumerable: true, get: function () { return kuberpc_1.KubeRPC; } });
6
+ Object.defineProperty(exports, "ServiceProxy", { enumerable: true, get: function () { return kuberpc_1.ServiceProxy; } });
7
+ var errors_1 = require("./errors");
8
+ Object.defineProperty(exports, "KubeRpcError", { enumerable: true, get: function () { return errors_1.KubeRpcError; } });
@@ -0,0 +1,29 @@
1
+ import { Handler, KubeRPCConfig } from "./@types";
2
+ export declare class ServiceProxy {
3
+ private readonly name;
4
+ private readonly rpc;
5
+ constructor(name: string, rpc: KubeRPC);
6
+ call(method: string, args?: Record<string, any>): Promise<any>;
7
+ }
8
+ export declare class KubeRPC {
9
+ private http;
10
+ private config;
11
+ private handlers;
12
+ private server;
13
+ private pool;
14
+ private endpointCache;
15
+ private locks;
16
+ constructor(config?: KubeRPCConfig);
17
+ service(name: string): ServiceProxy;
18
+ register(name: string, handler: Handler): Promise<void>;
19
+ call(service: string, method: string, args?: Record<string, any>): Promise<any>;
20
+ close(): void;
21
+ private startServer;
22
+ private drainInbound;
23
+ private handleInboundFrame;
24
+ private writeFrame;
25
+ private resolve;
26
+ private getConnection;
27
+ private enqueue;
28
+ private sendFrame;
29
+ }
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.KubeRPC = exports.ServiceProxy = void 0;
16
+ const net_1 = __importDefault(require("net"));
17
+ const axios_1 = __importDefault(require("axios"));
18
+ const msgpackr_1 = require("msgpackr");
19
+ const errors_1 = require("./errors");
20
+ // Wire format (positional arrays - avoids encoding key strings on every call):
21
+ // request → [method: string, args: object]
22
+ // response → [null, result] on success
23
+ // response → [errorMsg: string] on error
24
+ const DEFAULT_RPC_PORT = 7749;
25
+ class ServiceProxy {
26
+ constructor(name, rpc) {
27
+ this.name = name;
28
+ this.rpc = rpc;
29
+ }
30
+ call(method, args = {}) {
31
+ return this.rpc.call(this.name, method, args);
32
+ }
33
+ }
34
+ exports.ServiceProxy = ServiceProxy;
35
+ class KubeRPC {
36
+ constructor(config = {}) {
37
+ var _a, _b, _c, _d, _e, _f, _g, _h;
38
+ this.handlers = new Map();
39
+ this.server = null;
40
+ this.pool = new Map();
41
+ this.endpointCache = new Map();
42
+ this.locks = new Map();
43
+ const coreURL = (_b = (_a = config.coreURL) !== null && _a !== void 0 ? _a : process.env.KUBERPC_CORE_URL) !== null && _b !== void 0 ? _b : "";
44
+ const serviceName = (_d = (_c = config.serviceName) !== null && _c !== void 0 ? _c : process.env.KUBERPC_SERVICE_NAME) !== null && _d !== void 0 ? _d : "";
45
+ const port = (_e = config.port) !== null && _e !== void 0 ? _e : Number((_f = process.env.KUBERPC_PORT) !== null && _f !== void 0 ? _f : DEFAULT_RPC_PORT);
46
+ const host = (_h = (_g = config.host) !== null && _g !== void 0 ? _g : process.env.KUBERPC_HOST) !== null && _h !== void 0 ? _h : "localhost";
47
+ if (!coreURL) {
48
+ throw new Error("kubeRPC: coreURL is required. Pass it via config or set KUBERPC_CORE_URL.");
49
+ }
50
+ this.config = { coreURL, serviceName, port, host };
51
+ this.http = axios_1.default.create({ baseURL: coreURL });
52
+ }
53
+ service(name) {
54
+ return new ServiceProxy(name, this);
55
+ }
56
+ register(name, handler) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ this.handlers.set(name, handler);
59
+ if (!this.server) {
60
+ yield this.startServer();
61
+ }
62
+ yield this.http.post("/register-methods", {
63
+ service_name: this.config.serviceName,
64
+ methods: [{ name, params: [], description: "" }],
65
+ });
66
+ });
67
+ }
68
+ call(service_1, method_1) {
69
+ return __awaiter(this, arguments, void 0, function* (service, method, args = {}) {
70
+ const endpoint = yield this.resolve(service, method);
71
+ const socket = yield this.getConnection(service, endpoint);
72
+ return this.enqueue(service, socket, method, args);
73
+ });
74
+ }
75
+ close() {
76
+ for (const socket of this.pool.values())
77
+ socket.destroy();
78
+ this.pool.clear();
79
+ this.locks.clear();
80
+ this.endpointCache.clear();
81
+ if (this.server)
82
+ this.server.close();
83
+ }
84
+ startServer() {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ yield this.http.put(`/update-service?name=${this.config.serviceName}`, {
87
+ host: this.config.host,
88
+ port: this.config.port,
89
+ });
90
+ return new Promise((resolve, reject) => {
91
+ this.server = net_1.default.createServer((socket) => {
92
+ socket.setNoDelay(true);
93
+ socket.setKeepAlive(true, 0);
94
+ let buf = Buffer.alloc(0);
95
+ socket.on("data", (chunk) => {
96
+ buf = Buffer.concat([buf, chunk]);
97
+ this.drainInbound(socket, buf).then((remaining) => {
98
+ buf = remaining;
99
+ });
100
+ });
101
+ socket.on("error", () => socket.destroy());
102
+ });
103
+ this.server.listen(this.config.port, () => resolve());
104
+ this.server.once("error", reject);
105
+ });
106
+ });
107
+ }
108
+ drainInbound(socket, buf) {
109
+ return __awaiter(this, void 0, void 0, function* () {
110
+ while (buf.length >= 4) {
111
+ const len = buf.readUInt32BE(0);
112
+ if (buf.length < 4 + len)
113
+ break;
114
+ const frame = buf.slice(4, 4 + len);
115
+ buf = buf.slice(4 + len);
116
+ yield this.handleInboundFrame(socket, frame);
117
+ }
118
+ return buf;
119
+ });
120
+ }
121
+ handleInboundFrame(socket, frame) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ var _a;
124
+ try {
125
+ const [method, args] = (0, msgpackr_1.unpack)(frame);
126
+ const handler = this.handlers.get(method);
127
+ if (!handler) {
128
+ this.writeFrame(socket, [`Method "${method}" not found`]);
129
+ return;
130
+ }
131
+ const result = yield handler(args);
132
+ this.writeFrame(socket, [null, result]);
133
+ }
134
+ catch (err) {
135
+ this.writeFrame(socket, [(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : "Internal error"]);
136
+ }
137
+ });
138
+ }
139
+ writeFrame(socket, payload) {
140
+ const body = (0, msgpackr_1.pack)(payload);
141
+ const frame = Buffer.allocUnsafe(4 + body.byteLength);
142
+ frame.writeUInt32BE(body.byteLength, 0);
143
+ frame.set(body, 4);
144
+ socket.write(frame);
145
+ }
146
+ resolve(service, method) {
147
+ return __awaiter(this, void 0, void 0, function* () {
148
+ const cached = this.endpointCache.get(service);
149
+ if (cached)
150
+ return cached;
151
+ const { data } = yield this.http
152
+ .get(`/get-method?name=${service}&method=${method}`)
153
+ .catch(() => {
154
+ throw errors_1.KubeRpcError.methodNotFound(method, service);
155
+ });
156
+ if (!data.host || !data.port) {
157
+ throw errors_1.KubeRpcError.methodNotFound(method, service);
158
+ }
159
+ const endpoint = { host: data.host, port: Number(data.port) };
160
+ this.endpointCache.set(service, endpoint);
161
+ return endpoint;
162
+ });
163
+ }
164
+ getConnection(service, endpoint) {
165
+ return __awaiter(this, void 0, void 0, function* () {
166
+ const existing = this.pool.get(service);
167
+ if (existing && !existing.destroyed)
168
+ return existing;
169
+ const socket = yield new Promise((resolve, reject) => {
170
+ const s = new net_1.default.Socket();
171
+ s.setNoDelay(true);
172
+ s.setKeepAlive(true, 0);
173
+ s.connect(endpoint.port, endpoint.host, () => resolve(s));
174
+ s.once("error", () => reject(errors_1.KubeRpcError.connectionFailed(endpoint.host, endpoint.port)));
175
+ });
176
+ this.pool.set(service, socket);
177
+ socket.once("close", () => {
178
+ this.pool.delete(service);
179
+ this.locks.delete(service);
180
+ this.endpointCache.delete(service);
181
+ });
182
+ socket.once("error", () => {
183
+ this.pool.delete(service);
184
+ this.locks.delete(service);
185
+ });
186
+ return socket;
187
+ });
188
+ }
189
+ enqueue(service, socket, method, args) {
190
+ var _a;
191
+ const tail = ((_a = this.locks.get(service)) !== null && _a !== void 0 ? _a : Promise.resolve()).then(() => this.sendFrame(socket, method, args));
192
+ this.locks.set(service, tail.catch(() => { }));
193
+ return tail;
194
+ }
195
+ sendFrame(socket, method, args) {
196
+ return new Promise((resolve, reject) => {
197
+ const body = (0, msgpackr_1.pack)([method, args]);
198
+ const frame = Buffer.allocUnsafe(4 + body.byteLength);
199
+ frame.writeUInt32BE(body.byteLength, 0);
200
+ frame.set(body, 4);
201
+ let buf = Buffer.alloc(0);
202
+ const onData = (chunk) => {
203
+ buf = Buffer.concat([buf, chunk]);
204
+ if (buf.length < 4)
205
+ return;
206
+ const len = buf.readUInt32BE(0);
207
+ if (buf.length < 4 + len)
208
+ return;
209
+ socket.removeListener("data", onData);
210
+ const r = decodeResponse(buf.slice(4, 4 + len));
211
+ if (r.ok)
212
+ resolve(r.value);
213
+ else
214
+ reject(new Error(r.error));
215
+ };
216
+ socket.on("data", onData);
217
+ socket.once("error", reject);
218
+ socket.write(frame);
219
+ });
220
+ }
221
+ }
222
+ exports.KubeRPC = KubeRPC;
223
+ function decodeResponse(buf) {
224
+ const raw = (0, msgpackr_1.unpack)(buf);
225
+ if (!Array.isArray(raw)) {
226
+ return { ok: false, error: `Protocol mismatch: expected array, got ${typeof raw}. Restart the server.` };
227
+ }
228
+ if (raw.length === 1)
229
+ return { ok: false, error: raw[0] };
230
+ return { ok: true, value: raw[1] };
231
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "kuberpc-node",
3
+ "version": "2.0.0",
4
+ "description": "Node.js SDK for kubeRPC. Register and invoke service methods over raw TCP.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "start": "node dist/index.js",
13
+ "build": "rimraf dist && tsc",
14
+ "build:all": "rimraf dist && tsc && yarn --cwd samples/server install --force && yarn --cwd samples/client install --force && yarn --cwd benchmark/server install --force && yarn --cwd benchmark/client install --force"
15
+ },
16
+ "keywords": [
17
+ "RPC",
18
+ "KubeRPC",
19
+ "SDK"
20
+ ],
21
+ "author": "Folarin Raphael",
22
+ "license": "ISC",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/darksuei/kuberpc-sdk.git"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.8.5",
29
+ "rimraf": "^6.0.1",
30
+ "typescript": "^5.6.3"
31
+ },
32
+ "dependencies": {
33
+ "axios": "^1.7.7",
34
+ "msgpackr": "^2.0.4"
35
+ }
36
+ }