@silicaclaw/cli 1.0.0-beta.20 → 1.0.0-beta.21

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.
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelayPreviewAdapter = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const messageEnvelope_1 = require("./abstractions/messageEnvelope");
6
+ const jsonMessageEnvelopeCodec_1 = require("./codec/jsonMessageEnvelopeCodec");
7
+ const jsonTopicCodec_1 = require("./codec/jsonTopicCodec");
8
+ function dedupe(values) {
9
+ return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
10
+ }
11
+ class RelayPreviewAdapter {
12
+ peerId;
13
+ namespace;
14
+ signalingEndpoints;
15
+ room;
16
+ seedPeers;
17
+ bootstrapHints;
18
+ bootstrapSources;
19
+ maxMessageBytes;
20
+ pollIntervalMs;
21
+ maxFutureDriftMs;
22
+ maxPastDriftMs;
23
+ envelopeCodec;
24
+ topicCodec;
25
+ started = false;
26
+ poller = null;
27
+ handlers = new Map();
28
+ peers = new Map();
29
+ seenMessageIds = new Set();
30
+ activeEndpoint = "";
31
+ discoveryEvents = [];
32
+ discoveryEventsTotal = 0;
33
+ lastDiscoveryEventAt = 0;
34
+ signalingMessagesSentTotal = 0;
35
+ signalingMessagesReceivedTotal = 0;
36
+ reconnectAttemptsTotal = 0;
37
+ stats = {
38
+ publish_attempted: 0,
39
+ publish_sent: 0,
40
+ received_total: 0,
41
+ delivered_total: 0,
42
+ dropped_malformed: 0,
43
+ dropped_oversized: 0,
44
+ dropped_namespace_mismatch: 0,
45
+ dropped_timestamp_future_drift: 0,
46
+ dropped_timestamp_past_drift: 0,
47
+ dropped_decode_failed: 0,
48
+ dropped_self: 0,
49
+ dropped_topic_decode_error: 0,
50
+ dropped_handler_error: 0,
51
+ signaling_errors: 0,
52
+ invalid_signaling_payload_total: 0,
53
+ duplicate_sdp_total: 0,
54
+ duplicate_ice_total: 0,
55
+ start_errors: 0,
56
+ stop_errors: 0,
57
+ received_validated: 0,
58
+ };
59
+ constructor(options = {}) {
60
+ this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
61
+ this.namespace = String(options.namespace || "silicaclaw.preview").trim() || "silicaclaw.preview";
62
+ this.signalingEndpoints = dedupe((options.signalingUrls && options.signalingUrls.length > 0
63
+ ? options.signalingUrls
64
+ : [options.signalingUrl || "http://localhost:4510"]));
65
+ this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
66
+ this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
67
+ this.seedPeers = dedupe(options.seedPeers || []);
68
+ this.bootstrapHints = dedupe(options.bootstrapHints || []);
69
+ this.bootstrapSources = dedupe(options.bootstrapSources || []);
70
+ this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
71
+ this.pollIntervalMs = options.pollIntervalMs ?? 2000;
72
+ this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
73
+ this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
74
+ this.envelopeCodec = new jsonMessageEnvelopeCodec_1.JsonMessageEnvelopeCodec();
75
+ this.topicCodec = new jsonTopicCodec_1.JsonTopicCodec();
76
+ }
77
+ async start() {
78
+ if (this.started)
79
+ return;
80
+ try {
81
+ await this.post("/join", { room: this.room, peer_id: this.peerId });
82
+ this.started = true;
83
+ await this.refreshPeers();
84
+ await this.pollOnce();
85
+ this.poller = setInterval(() => {
86
+ this.pollOnce().catch(() => { });
87
+ }, this.pollIntervalMs);
88
+ this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
89
+ }
90
+ catch (error) {
91
+ this.stats.start_errors += 1;
92
+ throw new Error(`Relay start failed: ${error instanceof Error ? error.message : String(error)}`);
93
+ }
94
+ }
95
+ async stop() {
96
+ if (!this.started)
97
+ return;
98
+ if (this.poller) {
99
+ clearInterval(this.poller);
100
+ this.poller = null;
101
+ }
102
+ try {
103
+ await this.post("/leave", { room: this.room, peer_id: this.peerId });
104
+ }
105
+ catch {
106
+ this.stats.stop_errors += 1;
107
+ }
108
+ this.started = false;
109
+ this.recordDiscovery("signaling_disconnected", { endpoint: this.activeEndpoint });
110
+ }
111
+ async publish(topic, data) {
112
+ if (!this.started)
113
+ return;
114
+ this.stats.publish_attempted += 1;
115
+ const envelope = {
116
+ version: 1,
117
+ message_id: (0, crypto_1.randomUUID)(),
118
+ topic: `${this.namespace}:${topic}`,
119
+ source_peer_id: this.peerId,
120
+ timestamp: Date.now(),
121
+ payload: this.topicCodec.encode(topic, data),
122
+ };
123
+ const raw = this.envelopeCodec.encode(envelope);
124
+ if (raw.length > this.maxMessageBytes) {
125
+ this.stats.dropped_oversized += 1;
126
+ return;
127
+ }
128
+ await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
129
+ this.stats.publish_sent += 1;
130
+ this.signalingMessagesSentTotal += 1;
131
+ }
132
+ subscribe(topic, handler) {
133
+ const key = `${this.namespace}:${topic}`;
134
+ if (!this.handlers.has(key)) {
135
+ this.handlers.set(key, new Set());
136
+ }
137
+ this.handlers.get(key)?.add(handler);
138
+ }
139
+ getDiagnostics() {
140
+ const peerItems = Array.from(this.peers.values()).sort((a, b) => b.last_seen_at - a.last_seen_at);
141
+ return {
142
+ adapter: "relay-preview",
143
+ peer_id: this.peerId,
144
+ namespace: this.namespace,
145
+ room: this.room,
146
+ signaling_url: this.activeEndpoint,
147
+ signaling_endpoints: this.signalingEndpoints,
148
+ bootstrap_sources: this.bootstrapSources,
149
+ seed_peers_count: this.seedPeers.length,
150
+ bootstrap_hints_count: this.bootstrapHints.length,
151
+ discovery_events_total: this.discoveryEventsTotal,
152
+ last_discovery_event_at: this.lastDiscoveryEventAt,
153
+ discovery_events: this.discoveryEvents,
154
+ signaling_messages_sent_total: this.signalingMessagesSentTotal,
155
+ signaling_messages_received_total: this.signalingMessagesReceivedTotal,
156
+ reconnect_attempts_total: this.reconnectAttemptsTotal,
157
+ active_webrtc_peers: peerItems.length,
158
+ components: {
159
+ transport: "HttpRelayTransport",
160
+ discovery: "RelayRoomPeerList",
161
+ envelope_codec: this.envelopeCodec.constructor.name,
162
+ topic_codec: this.topicCodec.constructor.name,
163
+ },
164
+ limits: {
165
+ max_message_bytes: this.maxMessageBytes,
166
+ max_future_drift_ms: this.maxFutureDriftMs,
167
+ max_past_drift_ms: this.maxPastDriftMs,
168
+ },
169
+ config: {
170
+ started: this.started,
171
+ topic_handler_count: this.handlers.size,
172
+ poll_interval_ms: this.pollIntervalMs,
173
+ },
174
+ peers: {
175
+ total: peerItems.length,
176
+ online: peerItems.length,
177
+ stale: 0,
178
+ items: peerItems,
179
+ },
180
+ stats: { ...this.stats },
181
+ };
182
+ }
183
+ async pollOnce() {
184
+ const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
185
+ const messages = Array.isArray(payload?.messages) ? payload.messages : [];
186
+ for (const message of messages) {
187
+ this.signalingMessagesReceivedTotal += 1;
188
+ this.onEnvelope(message?.envelope);
189
+ }
190
+ await this.refreshPeers();
191
+ }
192
+ async refreshPeers() {
193
+ const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
194
+ const peerIds = Array.isArray(payload?.peers) ? payload.peers.map((value) => String(value || "").trim()).filter(Boolean) : [];
195
+ const now = Date.now();
196
+ const next = new Map();
197
+ for (const peerId of peerIds) {
198
+ if (peerId === this.peerId)
199
+ continue;
200
+ const existing = this.peers.get(peerId);
201
+ if (!existing) {
202
+ this.recordDiscovery("peer_joined", { peer_id: peerId });
203
+ }
204
+ next.set(peerId, {
205
+ peer_id: peerId,
206
+ status: "online",
207
+ first_seen_at: existing?.first_seen_at ?? now,
208
+ last_seen_at: now,
209
+ messages_seen: existing?.messages_seen ?? 0,
210
+ reconnect_attempts: existing?.reconnect_attempts ?? 0,
211
+ });
212
+ }
213
+ for (const peerId of this.peers.keys()) {
214
+ if (!next.has(peerId)) {
215
+ this.recordDiscovery("peer_removed", { peer_id: peerId });
216
+ }
217
+ }
218
+ this.peers = next;
219
+ }
220
+ onEnvelope(envelope) {
221
+ this.stats.received_total += 1;
222
+ const validated = (0, messageEnvelope_1.validateNetworkMessageEnvelope)(envelope, {
223
+ max_future_drift_ms: this.maxFutureDriftMs,
224
+ max_past_drift_ms: this.maxPastDriftMs,
225
+ });
226
+ if (!validated.ok || !validated.envelope) {
227
+ if (validated.reason === "timestamp_future_drift") {
228
+ this.stats.dropped_timestamp_future_drift += 1;
229
+ }
230
+ else if (validated.reason === "timestamp_past_drift") {
231
+ this.stats.dropped_timestamp_past_drift += 1;
232
+ }
233
+ else {
234
+ this.stats.dropped_malformed += 1;
235
+ }
236
+ return;
237
+ }
238
+ const message = validated.envelope;
239
+ if (message.source_peer_id === this.peerId) {
240
+ this.stats.dropped_self += 1;
241
+ return;
242
+ }
243
+ if (!message.topic.startsWith(`${this.namespace}:`)) {
244
+ this.stats.dropped_namespace_mismatch += 1;
245
+ return;
246
+ }
247
+ if (this.seenMessageIds.has(message.message_id)) {
248
+ return;
249
+ }
250
+ this.seenMessageIds.add(message.message_id);
251
+ if (this.seenMessageIds.size > 10000) {
252
+ const first = this.seenMessageIds.values().next().value;
253
+ if (first)
254
+ this.seenMessageIds.delete(first);
255
+ }
256
+ this.stats.received_validated += 1;
257
+ const topicKey = message.topic;
258
+ const topic = topicKey.slice(this.namespace.length + 1);
259
+ const handlers = this.handlers.get(topicKey);
260
+ if (!handlers || handlers.size === 0)
261
+ return;
262
+ const peer = this.peers.get(message.source_peer_id);
263
+ if (peer) {
264
+ peer.last_seen_at = Date.now();
265
+ peer.messages_seen += 1;
266
+ }
267
+ let payload;
268
+ try {
269
+ payload = this.topicCodec.decode(topic, message.payload);
270
+ }
271
+ catch {
272
+ this.stats.dropped_topic_decode_error += 1;
273
+ return;
274
+ }
275
+ for (const handler of handlers) {
276
+ try {
277
+ handler(payload);
278
+ this.stats.delivered_total += 1;
279
+ }
280
+ catch {
281
+ this.stats.dropped_handler_error += 1;
282
+ }
283
+ }
284
+ }
285
+ recordDiscovery(type, extra = {}) {
286
+ const event = {
287
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
288
+ type,
289
+ at: Date.now(),
290
+ ...extra,
291
+ };
292
+ this.discoveryEvents.unshift(event);
293
+ this.discoveryEvents = this.discoveryEvents.slice(0, 200);
294
+ this.discoveryEventsTotal += 1;
295
+ this.lastDiscoveryEventAt = event.at;
296
+ }
297
+ async get(path) {
298
+ const endpoint = this.activeEndpoint.replace(/\/+$/, "");
299
+ const response = await fetch(`${endpoint}${path}`);
300
+ if (!response.ok) {
301
+ this.stats.signaling_errors += 1;
302
+ throw new Error(`Relay GET failed (${response.status})`);
303
+ }
304
+ return response.json();
305
+ }
306
+ async post(path, body) {
307
+ const endpoint = this.activeEndpoint.replace(/\/+$/, "");
308
+ const response = await fetch(`${endpoint}${path}`, {
309
+ method: "POST",
310
+ headers: { "content-type": "application/json" },
311
+ body: JSON.stringify(body),
312
+ });
313
+ if (!response.ok) {
314
+ this.stats.signaling_errors += 1;
315
+ throw new Error(`Relay POST failed (${response.status})`);
316
+ }
317
+ return response.json();
318
+ }
319
+ }
320
+ exports.RelayPreviewAdapter = RelayPreviewAdapter;
@@ -3,6 +3,7 @@ export * from "./mock";
3
3
  export * from "./localEventBus";
4
4
  export * from "./realPreview";
5
5
  export * from "./webrtcPreview";
6
+ export * from "./relayPreview";
6
7
 
7
8
  export * from "./abstractions/messageEnvelope";
8
9
  export * from "./abstractions/topicCodec";