@plexspaces/sdk 0.1.7 → 0.1.9
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 +115 -0
- package/dist/actor.js +1 -4
- package/dist/host.d.ts +103 -16
- package/dist/host.js +316 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/wire/ws-frame-wire.d.ts +75 -0
- package/dist/wire/ws-frame-wire.js +412 -0
- package/dist/ws_thin_client.d.ts +79 -0
- package/dist/ws_thin_client.js +302 -0
- package/package.json +1 -1
- package/dist/actor.test.d.ts +0 -1
- package/dist/actor.test.js +0 -171
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// Protobuf wire encoding/decoding for plexspaces.transport.ws.v1.WsFrame.
|
|
5
|
+
// Covers the subset used by WsThinClient (tell, ask, register, heartbeat, ping).
|
|
6
|
+
// Byte-compatible with the prost-encoded WsFrame in crates/node/src/http_routes/ws_routes.rs.
|
|
7
|
+
//
|
|
8
|
+
// Field numbers from:
|
|
9
|
+
// proto/plexspaces/v1/transport/websocket.proto — WsFrame, WsNodeRegisterAck, WsError
|
|
10
|
+
// proto/plexspaces/v1/actors/actor_runtime.proto — SendMessageRequest, AskReplyRequest, AskReplyResponse
|
|
11
|
+
// proto/plexspaces/v1/node/node.proto — NodeRegistration, PingRequest, PingResponse,
|
|
12
|
+
// SendHeartbeatRequest
|
|
13
|
+
import { appendLengthDelimited, appendVarint, readLengthDelimited, readVarint, skipField, } from './proto-wire-common.js';
|
|
14
|
+
import { ActorID } from '../actor_id.js';
|
|
15
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
16
|
+
const textEnc = new TextEncoder();
|
|
17
|
+
const textDec = new TextDecoder('utf-8', { fatal: false });
|
|
18
|
+
function str(buf, fieldNum, s) {
|
|
19
|
+
if (!s)
|
|
20
|
+
return buf;
|
|
21
|
+
return appendLengthDelimited(buf, fieldNum, new Uint8Array(textEnc.encode(s)));
|
|
22
|
+
}
|
|
23
|
+
function bytes(buf, fieldNum, data) {
|
|
24
|
+
if (data.length === 0)
|
|
25
|
+
return buf;
|
|
26
|
+
return appendLengthDelimited(buf, fieldNum, data);
|
|
27
|
+
}
|
|
28
|
+
function uint32(buf, fieldNum, v) {
|
|
29
|
+
if (v === 0)
|
|
30
|
+
return buf;
|
|
31
|
+
let b = appendVarint(buf, (fieldNum << 3) | 0); // wire type 0 = varint
|
|
32
|
+
return appendVarint(b, v >>> 0);
|
|
33
|
+
}
|
|
34
|
+
function uint64(buf, fieldNum, v) {
|
|
35
|
+
if (v === 0)
|
|
36
|
+
return buf;
|
|
37
|
+
let b = appendVarint(buf, (fieldNum << 3) | 0);
|
|
38
|
+
return appendVarint(b, v);
|
|
39
|
+
}
|
|
40
|
+
// proto float (wire type 5, fixed32 little-endian)
|
|
41
|
+
function float32(buf, fieldNum, v) {
|
|
42
|
+
if (v === 0)
|
|
43
|
+
return buf;
|
|
44
|
+
const tag = appendVarint(buf, (fieldNum << 3) | 5);
|
|
45
|
+
const out = new Uint8Array(tag.length + 4);
|
|
46
|
+
out.set(tag, 0);
|
|
47
|
+
new DataView(out.buffer, tag.length, 4).setFloat32(0, v, true);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Encode a map<string,string> field (proto: repeated embedded message{key=1,value=2}). */
|
|
51
|
+
function mapStringString(buf, fieldNum, m) {
|
|
52
|
+
let out = buf;
|
|
53
|
+
for (const [k, v] of Object.entries(m)) {
|
|
54
|
+
let entry = new Uint8Array(0);
|
|
55
|
+
entry = str(entry, 1, k);
|
|
56
|
+
entry = str(entry, 2, v);
|
|
57
|
+
out = appendLengthDelimited(out, fieldNum, entry);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function readStr(data, pos) {
|
|
62
|
+
const { slice, nextPos } = readLengthDelimited(data, pos);
|
|
63
|
+
return { value: textDec.decode(slice), nextPos };
|
|
64
|
+
}
|
|
65
|
+
function readBool(data, pos) {
|
|
66
|
+
const { value, n } = readVarint(data, pos);
|
|
67
|
+
return { value: value !== 0n, nextPos: pos + n };
|
|
68
|
+
}
|
|
69
|
+
function readFloat32(data, pos) {
|
|
70
|
+
if (pos + 4 > data.length)
|
|
71
|
+
throw new Error('float32 underflow');
|
|
72
|
+
const value = new DataView(data.buffer, data.byteOffset + pos, 4).getFloat32(0, true);
|
|
73
|
+
return { value, nextPos: pos + 4 };
|
|
74
|
+
}
|
|
75
|
+
function readUint64(data, pos) {
|
|
76
|
+
const { value, n } = readVarint(data, pos);
|
|
77
|
+
return { value: Number(value), nextPos: pos + n };
|
|
78
|
+
}
|
|
79
|
+
// ─── WsFrame field numbers (from websocket.proto) ───────────────────────────
|
|
80
|
+
// WsFrame: request_id=1, tell=10, tell_response=11, ask=12, ask_response=13
|
|
81
|
+
// node_register=20, node_register_ack=21, node_ping=22, node_ping_response=23
|
|
82
|
+
// heartbeat=24, heartbeat_ack=25, metrics_request=26, metrics_response=27, error=30
|
|
83
|
+
// SendMessageRequest field numbers (actor_runtime.proto):
|
|
84
|
+
// request_id=1, namespace=2, actor_type=3, payload=5, sender_id=10, message_type=11, actor_name=21
|
|
85
|
+
// AskReplyRequest field numbers (actor_runtime.proto):
|
|
86
|
+
// request_id=1, namespace=2, actor_type=3, payload=5, sender_id=10, message_type=11, timeout=15, actor_name=21
|
|
87
|
+
// AskReplyResponse field numbers (actor_runtime.proto):
|
|
88
|
+
// request_id=1, success=2, payload=3, actor_id=5, error_message=6
|
|
89
|
+
// NodeRegistration (node.proto):
|
|
90
|
+
// node_id=1, node_address=2, capabilities=3(map), node_role=10
|
|
91
|
+
// WsNodeRegisterAck (websocket.proto):
|
|
92
|
+
// success=1, assigned_node_id=2, error_message=3
|
|
93
|
+
// PingRequest (node.proto):
|
|
94
|
+
// request_id=1, source_node_id=2, sequence_number=3
|
|
95
|
+
// PingResponse (node.proto):
|
|
96
|
+
// request_id=1, node_id=2, sequence_number=3, incarnation=4, cluster_name=6,
|
|
97
|
+
// node_address=7, resources=9 (NodeResourceHints)
|
|
98
|
+
//
|
|
99
|
+
// NodeResourceHints (node.proto):
|
|
100
|
+
// cpu_percent=1 (float), memory_available_mb=2 (uint64), available_cores=3 (uint32)
|
|
101
|
+
// SendHeartbeatRequest (node.proto):
|
|
102
|
+
// request_id=1, node_id=2
|
|
103
|
+
// WsError (websocket.proto):
|
|
104
|
+
// request_id=1, code=2, message=3
|
|
105
|
+
// SendMessageResponse (actor_runtime.proto):
|
|
106
|
+
// request_id=1, success=2, error_message=5
|
|
107
|
+
// ─── Encode (client → server) ───────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Encode a tell (fire-and-forget) frame.
|
|
110
|
+
* actor_type carries the full canonical actor ID when it contains '@'.
|
|
111
|
+
*/
|
|
112
|
+
export function encodeWsFrameTell(requestId, actorId, msgType, payloadBytes) {
|
|
113
|
+
const parsed = ActorID.parse(actorId);
|
|
114
|
+
// Build SendMessageRequest (field 10 inside WsFrame)
|
|
115
|
+
let inner = new Uint8Array(0);
|
|
116
|
+
inner = str(inner, 2, parsed.namespace); // namespace (required by server)
|
|
117
|
+
inner = str(inner, 3, actorId); // actor_type carries full canonical ID
|
|
118
|
+
if (msgType)
|
|
119
|
+
inner = str(inner, 11, msgType);
|
|
120
|
+
inner = bytes(inner, 5, payloadBytes);
|
|
121
|
+
// Build WsFrame
|
|
122
|
+
let frame = new Uint8Array(0);
|
|
123
|
+
frame = str(frame, 1, requestId);
|
|
124
|
+
frame = appendLengthDelimited(frame, 10, inner);
|
|
125
|
+
return frame;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Encode an ask (request-reply) frame.
|
|
129
|
+
* timeout field 15 in AskReplyRequest is a google.protobuf.Duration: message{seconds=1, nanos=2}.
|
|
130
|
+
*/
|
|
131
|
+
export function encodeWsFrameAsk(requestId, actorId, msgType, payloadBytes, timeoutMs) {
|
|
132
|
+
const parsed = ActorID.parse(actorId);
|
|
133
|
+
// Build AskReplyRequest (field 12 inside WsFrame)
|
|
134
|
+
let inner = new Uint8Array(0);
|
|
135
|
+
inner = str(inner, 1, requestId); // request_id inside AskReplyRequest
|
|
136
|
+
inner = str(inner, 2, parsed.namespace); // namespace (required by server)
|
|
137
|
+
inner = str(inner, 3, actorId); // actor_type carries full canonical ID
|
|
138
|
+
inner = str(inner, 4, 'POST'); // http_method — must be POST so server passes payload bytes through unchanged
|
|
139
|
+
if (msgType)
|
|
140
|
+
inner = str(inner, 11, msgType);
|
|
141
|
+
inner = bytes(inner, 5, payloadBytes);
|
|
142
|
+
// Encode timeout as Duration message {seconds: field 1, nanos: field 2}
|
|
143
|
+
if (timeoutMs > 0) {
|
|
144
|
+
const secs = Math.floor(timeoutMs / 1000);
|
|
145
|
+
const nanos = (timeoutMs % 1000) * 1000000;
|
|
146
|
+
let dur = new Uint8Array(0);
|
|
147
|
+
if (secs > 0)
|
|
148
|
+
dur = uint64(dur, 1, secs);
|
|
149
|
+
if (nanos > 0)
|
|
150
|
+
dur = uint32(dur, 2, nanos);
|
|
151
|
+
inner = appendLengthDelimited(inner, 15, dur);
|
|
152
|
+
}
|
|
153
|
+
// Build WsFrame
|
|
154
|
+
let frame = new Uint8Array(0);
|
|
155
|
+
frame = str(frame, 1, requestId);
|
|
156
|
+
frame = appendLengthDelimited(frame, 12, inner);
|
|
157
|
+
return frame;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Encode a node registration handshake frame (must be first frame after WS upgrade).
|
|
161
|
+
* resourceHints advertises browser capabilities as NodeResourceHints (field 11).
|
|
162
|
+
*/
|
|
163
|
+
export function encodeWsFrameNodeRegister(requestId, nodeId, nodeAddress, capabilities, resourceHints) {
|
|
164
|
+
// Build NodeRegistration (field 20 inside WsFrame)
|
|
165
|
+
// node_role = NODE_ROLE_THIN = 2
|
|
166
|
+
let inner = new Uint8Array(0);
|
|
167
|
+
inner = str(inner, 1, nodeId);
|
|
168
|
+
inner = str(inner, 2, nodeAddress);
|
|
169
|
+
inner = mapStringString(inner, 3, capabilities);
|
|
170
|
+
inner = uint32(inner, 10, 2); // node_role = NODE_ROLE_THIN
|
|
171
|
+
// Encode NodeResourceHints sub-message as field 11 if present
|
|
172
|
+
if (resourceHints) {
|
|
173
|
+
let hints = new Uint8Array(0);
|
|
174
|
+
if (resourceHints.cpuPercent)
|
|
175
|
+
hints = float32(hints, 1, resourceHints.cpuPercent);
|
|
176
|
+
if (resourceHints.memoryAvailableMb)
|
|
177
|
+
hints = uint64(hints, 2, resourceHints.memoryAvailableMb);
|
|
178
|
+
if (resourceHints.availableCores)
|
|
179
|
+
hints = uint32(hints, 3, resourceHints.availableCores);
|
|
180
|
+
if (hints.length > 0)
|
|
181
|
+
inner = appendLengthDelimited(inner, 11, hints);
|
|
182
|
+
}
|
|
183
|
+
// Build WsFrame
|
|
184
|
+
let frame = new Uint8Array(0);
|
|
185
|
+
frame = str(frame, 1, requestId);
|
|
186
|
+
frame = appendLengthDelimited(frame, 20, inner);
|
|
187
|
+
return frame;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Encode a heartbeat frame to keep the WS session alive.
|
|
191
|
+
*/
|
|
192
|
+
export function encodeWsFrameHeartbeat(requestId, nodeId) {
|
|
193
|
+
// Build SendHeartbeatRequest (field 24 inside WsFrame)
|
|
194
|
+
let inner = new Uint8Array(0);
|
|
195
|
+
inner = str(inner, 1, requestId);
|
|
196
|
+
inner = str(inner, 2, nodeId);
|
|
197
|
+
let frame = new Uint8Array(0);
|
|
198
|
+
frame = str(frame, 1, requestId);
|
|
199
|
+
frame = appendLengthDelimited(frame, 24, inner);
|
|
200
|
+
return frame;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Encode a node ping frame (SWIM-compatible, carries resource hints in the response).
|
|
204
|
+
*/
|
|
205
|
+
export function encodeWsFrameNodePing(requestId, sourceNodeId, sequenceNumber) {
|
|
206
|
+
// Build PingRequest (field 22 inside WsFrame)
|
|
207
|
+
let inner = new Uint8Array(0);
|
|
208
|
+
inner = str(inner, 1, requestId);
|
|
209
|
+
inner = str(inner, 2, sourceNodeId);
|
|
210
|
+
if (sequenceNumber)
|
|
211
|
+
inner = uint64(inner, 3, sequenceNumber);
|
|
212
|
+
let frame = new Uint8Array(0);
|
|
213
|
+
frame = str(frame, 1, requestId);
|
|
214
|
+
frame = appendLengthDelimited(frame, 22, inner);
|
|
215
|
+
return frame;
|
|
216
|
+
}
|
|
217
|
+
// ─── Decode helpers ──────────────────────────────────────────────────────────
|
|
218
|
+
function parseMessage(data) {
|
|
219
|
+
const fields = new Map();
|
|
220
|
+
const push = (fn, v) => {
|
|
221
|
+
const arr = fields.get(fn) ?? [];
|
|
222
|
+
arr.push(v);
|
|
223
|
+
fields.set(fn, arr);
|
|
224
|
+
};
|
|
225
|
+
let pos = 0;
|
|
226
|
+
while (pos < data.length) {
|
|
227
|
+
const { value: tag, n: tn } = readVarint(data, pos);
|
|
228
|
+
pos += tn;
|
|
229
|
+
const fn = Number(tag >> 3n);
|
|
230
|
+
const wt = Number(tag & 7n);
|
|
231
|
+
if (wt === 0) {
|
|
232
|
+
const { value, n } = readVarint(data, pos);
|
|
233
|
+
pos += n;
|
|
234
|
+
push(fn, value);
|
|
235
|
+
}
|
|
236
|
+
else if (wt === 2) {
|
|
237
|
+
const { slice, nextPos } = readLengthDelimited(data, pos);
|
|
238
|
+
pos = nextPos;
|
|
239
|
+
push(fn, slice);
|
|
240
|
+
}
|
|
241
|
+
else if (wt === 5) {
|
|
242
|
+
// fixed32 (float)
|
|
243
|
+
if (pos + 4 > data.length)
|
|
244
|
+
throw new Error('fixed32 underflow');
|
|
245
|
+
const v = new DataView(data.buffer, data.byteOffset + pos, 4).getFloat32(0, true);
|
|
246
|
+
pos += 4;
|
|
247
|
+
push(fn, v);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
pos = skipField(data, pos, wt);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return fields;
|
|
254
|
+
}
|
|
255
|
+
function getStr(fields, fn) {
|
|
256
|
+
const arr = fields.get(fn);
|
|
257
|
+
if (!arr || arr.length === 0)
|
|
258
|
+
return '';
|
|
259
|
+
const v = arr[0];
|
|
260
|
+
if (v instanceof Uint8Array)
|
|
261
|
+
return textDec.decode(v);
|
|
262
|
+
return '';
|
|
263
|
+
}
|
|
264
|
+
function getU64(fields, fn) {
|
|
265
|
+
const arr = fields.get(fn);
|
|
266
|
+
if (!arr || arr.length === 0)
|
|
267
|
+
return 0;
|
|
268
|
+
const v = arr[0];
|
|
269
|
+
if (typeof v === 'bigint')
|
|
270
|
+
return Number(v);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
function getBool(fields, fn) {
|
|
274
|
+
const arr = fields.get(fn);
|
|
275
|
+
if (!arr || arr.length === 0)
|
|
276
|
+
return false;
|
|
277
|
+
const v = arr[0];
|
|
278
|
+
if (typeof v === 'bigint')
|
|
279
|
+
return v !== 0n;
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
function getFloat(fields, fn) {
|
|
283
|
+
const arr = fields.get(fn);
|
|
284
|
+
if (!arr || arr.length === 0)
|
|
285
|
+
return 0;
|
|
286
|
+
const v = arr[0];
|
|
287
|
+
if (typeof v === 'number')
|
|
288
|
+
return v;
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
function getBytes(fields, fn) {
|
|
292
|
+
const arr = fields.get(fn);
|
|
293
|
+
if (!arr || arr.length === 0)
|
|
294
|
+
return new Uint8Array(0);
|
|
295
|
+
const v = arr[0];
|
|
296
|
+
if (v instanceof Uint8Array)
|
|
297
|
+
return v;
|
|
298
|
+
return new Uint8Array(0);
|
|
299
|
+
}
|
|
300
|
+
function parseJson(raw) {
|
|
301
|
+
if (raw.length === 0)
|
|
302
|
+
return null;
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(textDec.decode(raw));
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return textDec.decode(raw);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function parseAskResponse(data) {
|
|
311
|
+
const f = parseMessage(data);
|
|
312
|
+
return {
|
|
313
|
+
requestId: getStr(f, 1),
|
|
314
|
+
success: getBool(f, 2),
|
|
315
|
+
payloadJson: parseJson(getBytes(f, 3)),
|
|
316
|
+
errorMessage: getStr(f, 6),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function parseTellResponse(data) {
|
|
320
|
+
const f = parseMessage(data);
|
|
321
|
+
return { requestId: getStr(f, 1), success: getBool(f, 2), errorMessage: getStr(f, 5) };
|
|
322
|
+
}
|
|
323
|
+
function parseRegisterAck(data) {
|
|
324
|
+
const f = parseMessage(data);
|
|
325
|
+
return { success: getBool(f, 1), assignedNodeId: getStr(f, 2), errorMessage: getStr(f, 3) };
|
|
326
|
+
}
|
|
327
|
+
function parseNodeResourceHints(data) {
|
|
328
|
+
const f = parseMessage(data);
|
|
329
|
+
return {
|
|
330
|
+
cpuPercent: getFloat(f, 1),
|
|
331
|
+
memoryAvailableMb: getU64(f, 2),
|
|
332
|
+
availableCores: getU64(f, 3),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function parsePingResponse(data) {
|
|
336
|
+
const f = parseMessage(data);
|
|
337
|
+
// resources is field 9 (length-delimited NodeResourceHints sub-message)
|
|
338
|
+
const resourcesBytes = getBytes(f, 9);
|
|
339
|
+
const hints = resourcesBytes.length > 0
|
|
340
|
+
? parseNodeResourceHints(resourcesBytes)
|
|
341
|
+
: { cpuPercent: 0, memoryAvailableMb: 0, availableCores: 0 };
|
|
342
|
+
return {
|
|
343
|
+
requestId: getStr(f, 1),
|
|
344
|
+
nodeId: getStr(f, 2),
|
|
345
|
+
cpuPercent: hints.cpuPercent,
|
|
346
|
+
memoryAvailableMb: hints.memoryAvailableMb,
|
|
347
|
+
availableCores: hints.availableCores,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function parseIncomingTell(requestId, data) {
|
|
351
|
+
// SendMessageRequest: actor_type=3, message_type=11, payload=5
|
|
352
|
+
const f = parseMessage(data);
|
|
353
|
+
return {
|
|
354
|
+
actorId: getStr(f, 3),
|
|
355
|
+
msgType: getStr(f, 11),
|
|
356
|
+
payloadJson: parseJson(getBytes(f, 5)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function parseError(data) {
|
|
360
|
+
const f = parseMessage(data);
|
|
361
|
+
return { requestId: getStr(f, 1), code: getU64(f, 2), message: getStr(f, 3) };
|
|
362
|
+
}
|
|
363
|
+
// ─── Top-level decode ────────────────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Decode a binary WebSocket frame into a typed discriminated union.
|
|
366
|
+
* Unknown fields are skipped (forward-compatible).
|
|
367
|
+
*/
|
|
368
|
+
export function decodeWsFrame(bytes) {
|
|
369
|
+
try {
|
|
370
|
+
const top = parseMessage(bytes);
|
|
371
|
+
// Extract top-level request_id (field 1)
|
|
372
|
+
const requestId = getStr(top, 1);
|
|
373
|
+
// Check which oneof payload field is set (fields 10–30)
|
|
374
|
+
for (const [fn, arr] of top.entries()) {
|
|
375
|
+
if (!(arr[0] instanceof Uint8Array))
|
|
376
|
+
continue;
|
|
377
|
+
const payload = arr[0];
|
|
378
|
+
switch (fn) {
|
|
379
|
+
case 10: { // tell (incoming from server — server routing a tell back to thin node)
|
|
380
|
+
const t = parseIncomingTell(requestId, payload);
|
|
381
|
+
return { type: 'incoming_tell', requestId, actorId: t.actorId, msgType: t.msgType, payloadJson: t.payloadJson };
|
|
382
|
+
}
|
|
383
|
+
case 11: { // tell_response
|
|
384
|
+
const t = parseTellResponse(payload);
|
|
385
|
+
return { type: 'tell_response', requestId: t.requestId || requestId, success: t.success, errorMessage: t.errorMessage };
|
|
386
|
+
}
|
|
387
|
+
case 13: { // ask_response
|
|
388
|
+
const a = parseAskResponse(payload);
|
|
389
|
+
return { type: 'ask_response', requestId: a.requestId || requestId, success: a.success, payloadJson: a.payloadJson, errorMessage: a.errorMessage };
|
|
390
|
+
}
|
|
391
|
+
case 21: { // node_register_ack
|
|
392
|
+
const r = parseRegisterAck(payload);
|
|
393
|
+
return { type: 'node_register_ack', requestId, success: r.success, assignedNodeId: r.assignedNodeId, errorMessage: r.errorMessage };
|
|
394
|
+
}
|
|
395
|
+
case 23: { // node_ping_response
|
|
396
|
+
const p = parsePingResponse(payload);
|
|
397
|
+
return { type: 'node_ping_response', requestId: p.requestId || requestId, nodeId: p.nodeId, cpuPercent: p.cpuPercent, memoryAvailableMb: p.memoryAvailableMb, availableCores: p.availableCores };
|
|
398
|
+
}
|
|
399
|
+
case 25: // heartbeat_ack
|
|
400
|
+
return { type: 'heartbeat_ack', requestId };
|
|
401
|
+
case 30: { // error
|
|
402
|
+
const e = parseError(payload);
|
|
403
|
+
return { type: 'error', requestId: e.requestId || requestId, code: e.code, message: e.message };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { type: 'unknown' };
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return { type: 'unknown' };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface ThinClientOptions {
|
|
2
|
+
/** WebSocket URL, e.g. "ws://localhost:8091/ws" */
|
|
3
|
+
wsUrl: string;
|
|
4
|
+
/** JWT Bearer token. Appended as ?token=<jwt> on the WS URL if present. */
|
|
5
|
+
jwtToken?: string;
|
|
6
|
+
/** ULID preferred. Server assigns a new one if omitted or collision detected. */
|
|
7
|
+
nodeId?: string;
|
|
8
|
+
/** Placed in capabilities["tenant"] during registration. */
|
|
9
|
+
tenant?: string;
|
|
10
|
+
/** Placed in capabilities["namespace"] during registration. */
|
|
11
|
+
namespace?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resource snapshot from a PingResponse (proto fields 9–11 of PingResponse).
|
|
15
|
+
* Not a proto message — TypeScript interface over decoded fields.
|
|
16
|
+
*/
|
|
17
|
+
export interface ThinNodePingResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
nodeId: string;
|
|
20
|
+
cpuPercent: number;
|
|
21
|
+
memoryAvailableMb: number;
|
|
22
|
+
availableCores: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class WsThinClient {
|
|
25
|
+
private readonly opts;
|
|
26
|
+
private ws;
|
|
27
|
+
private assignedNodeId;
|
|
28
|
+
private readonly pendingAsks;
|
|
29
|
+
private readonly pendingPings;
|
|
30
|
+
private pendingReg;
|
|
31
|
+
private messageHandler;
|
|
32
|
+
private heartbeatTimer;
|
|
33
|
+
private readonly HEARTBEAT_INTERVAL_MS;
|
|
34
|
+
private readonly DEFAULT_ASK_TIMEOUT_MS;
|
|
35
|
+
constructor(opts: ThinClientOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Open the WebSocket, complete the NodeRegistration handshake, and start the
|
|
38
|
+
* heartbeat loop. Returns the server-assigned node_id.
|
|
39
|
+
*/
|
|
40
|
+
connect(): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Fire-and-forget. actorId must be the canonical form:
|
|
43
|
+
* {name}//{type}::{namespace}@{nodeId}
|
|
44
|
+
*/
|
|
45
|
+
tell(actorId: string, msgType: string, payload: unknown): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Request-reply. Returns the response payload (parsed JSON).
|
|
48
|
+
* Rejects with a timeout error if no response arrives within timeoutMs.
|
|
49
|
+
*/
|
|
50
|
+
ask(actorId: string, msgType: string, payload: unknown, timeoutMs?: number): Promise<unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* Register a handler for incoming tell frames addressed to this thin node.
|
|
53
|
+
* Called whenever the server routes a tell to one of this node's actor IDs.
|
|
54
|
+
*/
|
|
55
|
+
onMessage(handler: (actorId: string, msgType: string, payload: unknown) => void): void;
|
|
56
|
+
/**
|
|
57
|
+
* Send a SWIM-compatible ping to a target node and return its resource hints.
|
|
58
|
+
* The target nodeId must be known to the server (registered in SWIM membership).
|
|
59
|
+
*/
|
|
60
|
+
pingNode(targetNodeId: string, timeoutMs?: number): Promise<ThinNodePingResult>;
|
|
61
|
+
/** Send a heartbeat frame to keep the WS session alive. */
|
|
62
|
+
heartbeat(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Build a canonical actor ID on this thin node.
|
|
65
|
+
* Format: {name}//{type}::{namespace}@{assignedNodeId}
|
|
66
|
+
*/
|
|
67
|
+
localActorId(name: string, type: string, namespace?: string): string;
|
|
68
|
+
/** The server-assigned node_id (available after connect() resolves). */
|
|
69
|
+
get nodeId(): string;
|
|
70
|
+
/** Generate a new ULID. Exposed so examples can use it without a dep. */
|
|
71
|
+
static newUlid(): string;
|
|
72
|
+
/** Disconnect and clean up. */
|
|
73
|
+
disconnect(): Promise<void>;
|
|
74
|
+
private send;
|
|
75
|
+
private handleFrame;
|
|
76
|
+
private startHeartbeat;
|
|
77
|
+
private stopHeartbeat;
|
|
78
|
+
private rejectAllPending;
|
|
79
|
+
}
|