@silicaclaw/cli 1.0.0-beta.3 → 1.0.0-beta.31
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/CHANGELOG.md +93 -0
- package/INSTALL.md +41 -4
- package/README.md +80 -6
- package/apps/local-console/public/index.html +1283 -251
- package/apps/local-console/src/server.ts +108 -31
- package/docs/CLOUDFLARE_RELAY.md +61 -0
- package/docs/NEW_USER_INSTALL.md +166 -0
- package/docs/NEW_USER_OPERATIONS.md +265 -0
- package/package.json +6 -1
- package/packages/core/dist/crypto.d.ts +6 -0
- package/packages/core/dist/crypto.js +50 -0
- package/packages/core/dist/directory.d.ts +17 -0
- package/packages/core/dist/directory.js +145 -0
- package/packages/core/dist/identity.d.ts +2 -0
- package/packages/core/dist/identity.js +18 -0
- package/packages/core/dist/index.d.ts +11 -0
- package/packages/core/dist/index.js +27 -0
- package/packages/core/dist/indexing.d.ts +6 -0
- package/packages/core/dist/indexing.js +43 -0
- package/packages/core/dist/presence.d.ts +4 -0
- package/packages/core/dist/presence.js +23 -0
- package/packages/core/dist/profile.d.ts +4 -0
- package/packages/core/dist/profile.js +39 -0
- package/packages/core/dist/publicProfileSummary.d.ts +70 -0
- package/packages/core/dist/publicProfileSummary.js +103 -0
- package/packages/core/dist/socialConfig.d.ts +99 -0
- package/packages/core/dist/socialConfig.js +288 -0
- package/packages/core/dist/socialResolver.d.ts +46 -0
- package/packages/core/dist/socialResolver.js +237 -0
- package/packages/core/dist/socialTemplate.d.ts +2 -0
- package/packages/core/dist/socialTemplate.js +88 -0
- package/packages/core/dist/types.d.ts +37 -0
- package/packages/core/dist/types.js +2 -0
- package/packages/core/src/socialConfig.ts +8 -7
- package/packages/core/src/socialResolver.ts +17 -5
- package/packages/network/dist/abstractions/messageEnvelope.d.ts +28 -0
- package/packages/network/dist/abstractions/messageEnvelope.js +36 -0
- package/packages/network/dist/abstractions/peerDiscovery.d.ts +43 -0
- package/packages/network/dist/abstractions/peerDiscovery.js +2 -0
- package/packages/network/dist/abstractions/topicCodec.d.ts +4 -0
- package/packages/network/dist/abstractions/topicCodec.js +2 -0
- package/packages/network/dist/abstractions/transport.d.ts +36 -0
- package/packages/network/dist/abstractions/transport.js +2 -0
- package/packages/network/dist/codec/jsonMessageEnvelopeCodec.d.ts +5 -0
- package/packages/network/dist/codec/jsonMessageEnvelopeCodec.js +24 -0
- package/packages/network/dist/codec/jsonTopicCodec.d.ts +5 -0
- package/packages/network/dist/codec/jsonTopicCodec.js +12 -0
- package/packages/network/dist/discovery/heartbeatPeerDiscovery.d.ts +28 -0
- package/packages/network/dist/discovery/heartbeatPeerDiscovery.js +144 -0
- package/packages/network/dist/index.d.ts +14 -0
- package/packages/network/dist/index.js +30 -0
- package/packages/network/dist/localEventBus.d.ts +9 -0
- package/packages/network/dist/localEventBus.js +47 -0
- package/packages/network/dist/mock.d.ts +8 -0
- package/packages/network/dist/mock.js +24 -0
- package/packages/network/dist/realPreview.d.ts +105 -0
- package/packages/network/dist/realPreview.js +327 -0
- package/packages/network/dist/relayPreview.d.ts +166 -0
- package/packages/network/dist/relayPreview.js +430 -0
- package/packages/network/dist/transport/udpLanBroadcastTransport.d.ts +23 -0
- package/packages/network/dist/transport/udpLanBroadcastTransport.js +153 -0
- package/packages/network/dist/types.d.ts +6 -0
- package/packages/network/dist/types.js +2 -0
- package/packages/network/dist/webrtcPreview.d.ts +163 -0
- package/packages/network/dist/webrtcPreview.js +844 -0
- package/packages/network/src/index.ts +1 -0
- package/packages/network/src/relayPreview.ts +552 -0
- package/packages/storage/dist/index.d.ts +3 -0
- package/packages/storage/dist/index.js +19 -0
- package/packages/storage/dist/jsonRepo.d.ts +7 -0
- package/packages/storage/dist/jsonRepo.js +29 -0
- package/packages/storage/dist/repos.d.ts +21 -0
- package/packages/storage/dist/repos.js +41 -0
- package/packages/storage/dist/socialRuntimeRepo.d.ts +5 -0
- package/packages/storage/dist/socialRuntimeRepo.js +52 -0
- package/packages/storage/src/socialRuntimeRepo.ts +4 -4
- package/packages/storage/tsconfig.json +6 -1
- package/scripts/quickstart.sh +314 -36
- package/scripts/silicaclaw-cli.mjs +458 -24
- package/scripts/silicaclaw-gateway.mjs +467 -0
- package/scripts/webrtc-signaling-server.mjs +89 -5
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { NetworkAdapter } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
MessageEnvelopeCodec,
|
|
5
|
+
NetworkMessageEnvelope,
|
|
6
|
+
validateNetworkMessageEnvelope,
|
|
7
|
+
} from "./abstractions/messageEnvelope";
|
|
8
|
+
import { TopicCodec } from "./abstractions/topicCodec";
|
|
9
|
+
import { JsonMessageEnvelopeCodec } from "./codec/jsonMessageEnvelopeCodec";
|
|
10
|
+
import { JsonTopicCodec } from "./codec/jsonTopicCodec";
|
|
11
|
+
|
|
12
|
+
type RelayPreviewOptions = {
|
|
13
|
+
peerId?: string;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
signalingUrl?: string;
|
|
16
|
+
signalingUrls?: string[];
|
|
17
|
+
room?: string;
|
|
18
|
+
seedPeers?: string[];
|
|
19
|
+
bootstrapHints?: string[];
|
|
20
|
+
bootstrapSources?: string[];
|
|
21
|
+
maxMessageBytes?: number;
|
|
22
|
+
pollIntervalMs?: number;
|
|
23
|
+
maxFutureDriftMs?: number;
|
|
24
|
+
maxPastDriftMs?: number;
|
|
25
|
+
requestTimeoutMs?: number;
|
|
26
|
+
peerRefreshIntervalMs?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type RelayPeer = {
|
|
30
|
+
peer_id: string;
|
|
31
|
+
status: "online";
|
|
32
|
+
first_seen_at: number;
|
|
33
|
+
last_seen_at: number;
|
|
34
|
+
messages_seen: number;
|
|
35
|
+
reconnect_attempts: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type RelayDiagnostics = {
|
|
39
|
+
adapter: "relay-preview";
|
|
40
|
+
peer_id: string;
|
|
41
|
+
namespace: string;
|
|
42
|
+
room: string;
|
|
43
|
+
signaling_url: string;
|
|
44
|
+
signaling_endpoints: string[];
|
|
45
|
+
active_endpoint_index: number;
|
|
46
|
+
bootstrap_sources: string[];
|
|
47
|
+
seed_peers_count: number;
|
|
48
|
+
bootstrap_hints_count: number;
|
|
49
|
+
discovery_events_total: number;
|
|
50
|
+
last_discovery_event_at: number;
|
|
51
|
+
last_join_at: number;
|
|
52
|
+
last_poll_at: number;
|
|
53
|
+
last_publish_at: number;
|
|
54
|
+
last_peer_refresh_at: number;
|
|
55
|
+
last_error_at: number;
|
|
56
|
+
last_error: string | null;
|
|
57
|
+
discovery_events: Array<{
|
|
58
|
+
id: string;
|
|
59
|
+
type: string;
|
|
60
|
+
at: number;
|
|
61
|
+
peer_id?: string;
|
|
62
|
+
endpoint?: string;
|
|
63
|
+
detail?: string;
|
|
64
|
+
}>;
|
|
65
|
+
signaling_messages_sent_total: number;
|
|
66
|
+
signaling_messages_received_total: number;
|
|
67
|
+
reconnect_attempts_total: number;
|
|
68
|
+
active_webrtc_peers: number;
|
|
69
|
+
components: {
|
|
70
|
+
transport: string;
|
|
71
|
+
discovery: string;
|
|
72
|
+
envelope_codec: string;
|
|
73
|
+
topic_codec: string;
|
|
74
|
+
};
|
|
75
|
+
limits: {
|
|
76
|
+
max_message_bytes: number;
|
|
77
|
+
max_future_drift_ms: number;
|
|
78
|
+
max_past_drift_ms: number;
|
|
79
|
+
};
|
|
80
|
+
config: {
|
|
81
|
+
started: boolean;
|
|
82
|
+
topic_handler_count: number;
|
|
83
|
+
poll_interval_ms: number;
|
|
84
|
+
};
|
|
85
|
+
peers: {
|
|
86
|
+
total: number;
|
|
87
|
+
online: number;
|
|
88
|
+
stale: number;
|
|
89
|
+
items: RelayPeer[];
|
|
90
|
+
};
|
|
91
|
+
stats: {
|
|
92
|
+
publish_attempted: number;
|
|
93
|
+
publish_sent: number;
|
|
94
|
+
received_total: number;
|
|
95
|
+
delivered_total: number;
|
|
96
|
+
dropped_malformed: number;
|
|
97
|
+
dropped_oversized: number;
|
|
98
|
+
dropped_namespace_mismatch: number;
|
|
99
|
+
dropped_timestamp_future_drift: number;
|
|
100
|
+
dropped_timestamp_past_drift: number;
|
|
101
|
+
dropped_decode_failed: number;
|
|
102
|
+
dropped_self: number;
|
|
103
|
+
dropped_topic_decode_error: number;
|
|
104
|
+
dropped_handler_error: number;
|
|
105
|
+
signaling_errors: number;
|
|
106
|
+
invalid_signaling_payload_total: number;
|
|
107
|
+
duplicate_sdp_total: number;
|
|
108
|
+
duplicate_ice_total: number;
|
|
109
|
+
start_errors: number;
|
|
110
|
+
stop_errors: number;
|
|
111
|
+
received_validated: number;
|
|
112
|
+
join_attempted: number;
|
|
113
|
+
join_succeeded: number;
|
|
114
|
+
poll_attempted: number;
|
|
115
|
+
poll_succeeded: number;
|
|
116
|
+
peers_refresh_attempted: number;
|
|
117
|
+
peers_refresh_succeeded: number;
|
|
118
|
+
publish_succeeded: number;
|
|
119
|
+
poll_skipped_inflight: number;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function dedupe(values: string[]): string[] {
|
|
124
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class RelayPreviewAdapter implements NetworkAdapter {
|
|
128
|
+
private readonly peerId: string;
|
|
129
|
+
private readonly namespace: string;
|
|
130
|
+
private readonly signalingEndpoints: string[];
|
|
131
|
+
private readonly room: string;
|
|
132
|
+
private readonly seedPeers: string[];
|
|
133
|
+
private readonly bootstrapHints: string[];
|
|
134
|
+
private readonly bootstrapSources: string[];
|
|
135
|
+
private readonly maxMessageBytes: number;
|
|
136
|
+
private readonly pollIntervalMs: number;
|
|
137
|
+
private readonly maxFutureDriftMs: number;
|
|
138
|
+
private readonly maxPastDriftMs: number;
|
|
139
|
+
private readonly requestTimeoutMs: number;
|
|
140
|
+
private readonly peerRefreshIntervalMs: number;
|
|
141
|
+
private readonly envelopeCodec: MessageEnvelopeCodec;
|
|
142
|
+
private readonly topicCodec: TopicCodec;
|
|
143
|
+
|
|
144
|
+
private started = false;
|
|
145
|
+
private poller: NodeJS.Timeout | null = null;
|
|
146
|
+
private handlers = new Map<string, Set<(data: any) => void>>();
|
|
147
|
+
private peers = new Map<string, RelayPeer>();
|
|
148
|
+
private seenMessageIds = new Set<string>();
|
|
149
|
+
private activeEndpoint = "";
|
|
150
|
+
private discoveryEvents: RelayDiagnostics["discovery_events"] = [];
|
|
151
|
+
private discoveryEventsTotal = 0;
|
|
152
|
+
private lastDiscoveryEventAt = 0;
|
|
153
|
+
private signalingMessagesSentTotal = 0;
|
|
154
|
+
private signalingMessagesReceivedTotal = 0;
|
|
155
|
+
private reconnectAttemptsTotal = 0;
|
|
156
|
+
private activeEndpointIndex = 0;
|
|
157
|
+
private lastJoinAt = 0;
|
|
158
|
+
private lastPollAt = 0;
|
|
159
|
+
private lastPublishAt = 0;
|
|
160
|
+
private lastPeerRefreshAt = 0;
|
|
161
|
+
private lastErrorAt = 0;
|
|
162
|
+
private lastError: string | null = null;
|
|
163
|
+
private pollInFlight = false;
|
|
164
|
+
private currentPollDelayMs = 0;
|
|
165
|
+
|
|
166
|
+
private stats: RelayDiagnostics["stats"] = {
|
|
167
|
+
publish_attempted: 0,
|
|
168
|
+
publish_sent: 0,
|
|
169
|
+
received_total: 0,
|
|
170
|
+
delivered_total: 0,
|
|
171
|
+
dropped_malformed: 0,
|
|
172
|
+
dropped_oversized: 0,
|
|
173
|
+
dropped_namespace_mismatch: 0,
|
|
174
|
+
dropped_timestamp_future_drift: 0,
|
|
175
|
+
dropped_timestamp_past_drift: 0,
|
|
176
|
+
dropped_decode_failed: 0,
|
|
177
|
+
dropped_self: 0,
|
|
178
|
+
dropped_topic_decode_error: 0,
|
|
179
|
+
dropped_handler_error: 0,
|
|
180
|
+
signaling_errors: 0,
|
|
181
|
+
invalid_signaling_payload_total: 0,
|
|
182
|
+
duplicate_sdp_total: 0,
|
|
183
|
+
duplicate_ice_total: 0,
|
|
184
|
+
start_errors: 0,
|
|
185
|
+
stop_errors: 0,
|
|
186
|
+
received_validated: 0,
|
|
187
|
+
join_attempted: 0,
|
|
188
|
+
join_succeeded: 0,
|
|
189
|
+
poll_attempted: 0,
|
|
190
|
+
poll_succeeded: 0,
|
|
191
|
+
peers_refresh_attempted: 0,
|
|
192
|
+
peers_refresh_succeeded: 0,
|
|
193
|
+
publish_succeeded: 0,
|
|
194
|
+
poll_skipped_inflight: 0,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
constructor(options: RelayPreviewOptions = {}) {
|
|
198
|
+
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
199
|
+
this.namespace = String(options.namespace || "silicaclaw.preview").trim() || "silicaclaw.preview";
|
|
200
|
+
this.signalingEndpoints = dedupe(
|
|
201
|
+
(options.signalingUrls && options.signalingUrls.length > 0
|
|
202
|
+
? options.signalingUrls
|
|
203
|
+
: [options.signalingUrl || "http://localhost:4510"])
|
|
204
|
+
);
|
|
205
|
+
this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
|
|
206
|
+
this.activeEndpointIndex = 0;
|
|
207
|
+
this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
|
|
208
|
+
this.seedPeers = dedupe(options.seedPeers || []);
|
|
209
|
+
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
210
|
+
this.bootstrapSources = dedupe(options.bootstrapSources || []);
|
|
211
|
+
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
212
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
213
|
+
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
214
|
+
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
215
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 4000;
|
|
216
|
+
this.peerRefreshIntervalMs = options.peerRefreshIntervalMs ?? 30_000;
|
|
217
|
+
this.envelopeCodec = new JsonMessageEnvelopeCodec();
|
|
218
|
+
this.topicCodec = new JsonTopicCodec();
|
|
219
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async start(): Promise<void> {
|
|
223
|
+
if (this.started) return;
|
|
224
|
+
try {
|
|
225
|
+
await this.joinRoom("start");
|
|
226
|
+
this.started = true;
|
|
227
|
+
await this.refreshPeers();
|
|
228
|
+
await this.pollOnce();
|
|
229
|
+
this.scheduleNextPoll(this.pollIntervalMs);
|
|
230
|
+
this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
this.stats.start_errors += 1;
|
|
233
|
+
throw new Error(`Relay start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async stop(): Promise<void> {
|
|
238
|
+
if (!this.started) return;
|
|
239
|
+
if (this.poller) {
|
|
240
|
+
clearTimeout(this.poller);
|
|
241
|
+
this.poller = null;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
await this.post("/leave", { room: this.room, peer_id: this.peerId });
|
|
245
|
+
} catch {
|
|
246
|
+
this.stats.stop_errors += 1;
|
|
247
|
+
}
|
|
248
|
+
this.started = false;
|
|
249
|
+
this.recordDiscovery("signaling_disconnected", { endpoint: this.activeEndpoint });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async publish(topic: string, data: any): Promise<void> {
|
|
253
|
+
if (!this.started) return;
|
|
254
|
+
this.stats.publish_attempted += 1;
|
|
255
|
+
await this.maybeRefreshJoin("publish");
|
|
256
|
+
const envelope: NetworkMessageEnvelope = {
|
|
257
|
+
version: 1,
|
|
258
|
+
message_id: randomUUID(),
|
|
259
|
+
topic: `${this.namespace}:${topic}`,
|
|
260
|
+
source_peer_id: this.peerId,
|
|
261
|
+
timestamp: Date.now(),
|
|
262
|
+
payload: this.topicCodec.encode(topic, data),
|
|
263
|
+
};
|
|
264
|
+
const raw = this.envelopeCodec.encode(envelope);
|
|
265
|
+
if (raw.length > this.maxMessageBytes) {
|
|
266
|
+
this.stats.dropped_oversized += 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
|
|
270
|
+
this.lastPublishAt = Date.now();
|
|
271
|
+
this.stats.publish_sent += 1;
|
|
272
|
+
this.stats.publish_succeeded += 1;
|
|
273
|
+
this.signalingMessagesSentTotal += 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
subscribe(topic: string, handler: (data: any) => void): void {
|
|
277
|
+
const key = `${this.namespace}:${topic}`;
|
|
278
|
+
if (!this.handlers.has(key)) {
|
|
279
|
+
this.handlers.set(key, new Set());
|
|
280
|
+
}
|
|
281
|
+
this.handlers.get(key)?.add(handler);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getDiagnostics(): RelayDiagnostics {
|
|
285
|
+
const peerItems = Array.from(this.peers.values()).sort((a, b) => b.last_seen_at - a.last_seen_at);
|
|
286
|
+
return {
|
|
287
|
+
adapter: "relay-preview",
|
|
288
|
+
peer_id: this.peerId,
|
|
289
|
+
namespace: this.namespace,
|
|
290
|
+
room: this.room,
|
|
291
|
+
signaling_url: this.activeEndpoint,
|
|
292
|
+
signaling_endpoints: this.signalingEndpoints,
|
|
293
|
+
active_endpoint_index: this.activeEndpointIndex,
|
|
294
|
+
bootstrap_sources: this.bootstrapSources,
|
|
295
|
+
seed_peers_count: this.seedPeers.length,
|
|
296
|
+
bootstrap_hints_count: this.bootstrapHints.length,
|
|
297
|
+
discovery_events_total: this.discoveryEventsTotal,
|
|
298
|
+
last_discovery_event_at: this.lastDiscoveryEventAt,
|
|
299
|
+
last_join_at: this.lastJoinAt,
|
|
300
|
+
last_poll_at: this.lastPollAt,
|
|
301
|
+
last_publish_at: this.lastPublishAt,
|
|
302
|
+
last_peer_refresh_at: this.lastPeerRefreshAt,
|
|
303
|
+
last_error_at: this.lastErrorAt,
|
|
304
|
+
last_error: this.lastError,
|
|
305
|
+
discovery_events: this.discoveryEvents,
|
|
306
|
+
signaling_messages_sent_total: this.signalingMessagesSentTotal,
|
|
307
|
+
signaling_messages_received_total: this.signalingMessagesReceivedTotal,
|
|
308
|
+
reconnect_attempts_total: this.reconnectAttemptsTotal,
|
|
309
|
+
active_webrtc_peers: peerItems.length,
|
|
310
|
+
components: {
|
|
311
|
+
transport: "HttpRelayTransport",
|
|
312
|
+
discovery: "RelayRoomPeerList",
|
|
313
|
+
envelope_codec: this.envelopeCodec.constructor.name,
|
|
314
|
+
topic_codec: this.topicCodec.constructor.name,
|
|
315
|
+
},
|
|
316
|
+
limits: {
|
|
317
|
+
max_message_bytes: this.maxMessageBytes,
|
|
318
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
319
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
320
|
+
},
|
|
321
|
+
config: {
|
|
322
|
+
started: this.started,
|
|
323
|
+
topic_handler_count: this.handlers.size,
|
|
324
|
+
poll_interval_ms: this.pollIntervalMs,
|
|
325
|
+
},
|
|
326
|
+
peers: {
|
|
327
|
+
total: peerItems.length,
|
|
328
|
+
online: peerItems.length,
|
|
329
|
+
stale: 0,
|
|
330
|
+
items: peerItems,
|
|
331
|
+
},
|
|
332
|
+
stats: { ...this.stats },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async pollOnce(): Promise<void> {
|
|
337
|
+
if (this.pollInFlight) {
|
|
338
|
+
this.stats.poll_skipped_inflight += 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.pollInFlight = true;
|
|
342
|
+
await this.maybeRefreshJoin("poll");
|
|
343
|
+
this.stats.poll_attempted += 1;
|
|
344
|
+
try {
|
|
345
|
+
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
346
|
+
this.lastPollAt = Date.now();
|
|
347
|
+
this.stats.poll_succeeded += 1;
|
|
348
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
349
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
350
|
+
for (const message of messages) {
|
|
351
|
+
this.signalingMessagesReceivedTotal += 1;
|
|
352
|
+
this.onEnvelope(message?.envelope);
|
|
353
|
+
}
|
|
354
|
+
if (Array.isArray(payload?.peers)) {
|
|
355
|
+
this.updatePeersFromList(payload.peers);
|
|
356
|
+
} else if (!this.lastPeerRefreshAt || Date.now() - this.lastPeerRefreshAt >= this.peerRefreshIntervalMs) {
|
|
357
|
+
await this.refreshPeers();
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.currentPollDelayMs = Math.min(15_000, Math.max(this.pollIntervalMs, this.currentPollDelayMs * 2));
|
|
361
|
+
throw error;
|
|
362
|
+
} finally {
|
|
363
|
+
this.pollInFlight = false;
|
|
364
|
+
if (this.started) {
|
|
365
|
+
this.scheduleNextPoll(this.currentPollDelayMs);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async refreshPeers(): Promise<void> {
|
|
371
|
+
this.stats.peers_refresh_attempted += 1;
|
|
372
|
+
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
373
|
+
this.lastPeerRefreshAt = Date.now();
|
|
374
|
+
this.stats.peers_refresh_succeeded += 1;
|
|
375
|
+
const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
|
|
376
|
+
this.updatePeersFromList(peerIds);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private onEnvelope(envelope: unknown): void {
|
|
380
|
+
this.stats.received_total += 1;
|
|
381
|
+
const validated = validateNetworkMessageEnvelope(envelope, {
|
|
382
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
383
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
384
|
+
});
|
|
385
|
+
if (!validated.ok || !validated.envelope) {
|
|
386
|
+
if (validated.reason === "timestamp_future_drift") {
|
|
387
|
+
this.stats.dropped_timestamp_future_drift += 1;
|
|
388
|
+
} else if (validated.reason === "timestamp_past_drift") {
|
|
389
|
+
this.stats.dropped_timestamp_past_drift += 1;
|
|
390
|
+
} else {
|
|
391
|
+
this.stats.dropped_malformed += 1;
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const message = validated.envelope;
|
|
396
|
+
if (message.source_peer_id === this.peerId) {
|
|
397
|
+
this.stats.dropped_self += 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!message.topic.startsWith(`${this.namespace}:`)) {
|
|
401
|
+
this.stats.dropped_namespace_mismatch += 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this.seenMessageIds.has(message.message_id)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.seenMessageIds.add(message.message_id);
|
|
408
|
+
if (this.seenMessageIds.size > 10000) {
|
|
409
|
+
const first = this.seenMessageIds.values().next().value;
|
|
410
|
+
if (first) this.seenMessageIds.delete(first);
|
|
411
|
+
}
|
|
412
|
+
this.stats.received_validated += 1;
|
|
413
|
+
|
|
414
|
+
const topicKey = message.topic;
|
|
415
|
+
const topic = topicKey.slice(this.namespace.length + 1);
|
|
416
|
+
const handlers = this.handlers.get(topicKey);
|
|
417
|
+
if (!handlers || handlers.size === 0) return;
|
|
418
|
+
|
|
419
|
+
const peer = this.peers.get(message.source_peer_id);
|
|
420
|
+
if (peer) {
|
|
421
|
+
peer.last_seen_at = Date.now();
|
|
422
|
+
peer.messages_seen += 1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let payload: unknown;
|
|
426
|
+
try {
|
|
427
|
+
payload = this.topicCodec.decode(topic, message.payload);
|
|
428
|
+
} catch {
|
|
429
|
+
this.stats.dropped_topic_decode_error += 1;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const handler of handlers) {
|
|
433
|
+
try {
|
|
434
|
+
handler(payload);
|
|
435
|
+
this.stats.delivered_total += 1;
|
|
436
|
+
} catch {
|
|
437
|
+
this.stats.dropped_handler_error += 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private recordDiscovery(type: string, extra: { peer_id?: string; endpoint?: string; detail?: string } = {}): void {
|
|
443
|
+
const event = {
|
|
444
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
445
|
+
type,
|
|
446
|
+
at: Date.now(),
|
|
447
|
+
...extra,
|
|
448
|
+
};
|
|
449
|
+
this.discoveryEvents.unshift(event);
|
|
450
|
+
this.discoveryEvents = this.discoveryEvents.slice(0, 200);
|
|
451
|
+
this.discoveryEventsTotal += 1;
|
|
452
|
+
this.lastDiscoveryEventAt = event.at;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private async joinRoom(reason: string): Promise<void> {
|
|
456
|
+
this.stats.join_attempted += 1;
|
|
457
|
+
await this.post("/join", { room: this.room, peer_id: this.peerId });
|
|
458
|
+
this.lastJoinAt = Date.now();
|
|
459
|
+
this.stats.join_succeeded += 1;
|
|
460
|
+
this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private async maybeRefreshJoin(reason: string): Promise<void> {
|
|
464
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(45_000, this.pollIntervalMs * 6)) {
|
|
465
|
+
await this.joinRoom(reason);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async get(path: string): Promise<any> {
|
|
470
|
+
return this.requestJson("GET", path);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async post(path: string, body: any): Promise<any> {
|
|
474
|
+
return this.requestJson("POST", path, body);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async requestJson(method: "GET" | "POST", path: string, body?: any): Promise<any> {
|
|
478
|
+
const errors: string[] = [];
|
|
479
|
+
for (let offset = 0; offset < this.signalingEndpoints.length; offset += 1) {
|
|
480
|
+
const index = (this.activeEndpointIndex + offset) % this.signalingEndpoints.length;
|
|
481
|
+
const endpoint = this.signalingEndpoints[index]?.replace(/\/+$/, "");
|
|
482
|
+
if (!endpoint) continue;
|
|
483
|
+
try {
|
|
484
|
+
const controller = new AbortController();
|
|
485
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
486
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
487
|
+
method,
|
|
488
|
+
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
489
|
+
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
490
|
+
signal: controller.signal,
|
|
491
|
+
});
|
|
492
|
+
clearTimeout(timeout);
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
495
|
+
}
|
|
496
|
+
this.activeEndpointIndex = index;
|
|
497
|
+
this.activeEndpoint = endpoint;
|
|
498
|
+
this.lastError = null;
|
|
499
|
+
return response.json();
|
|
500
|
+
} catch (error) {
|
|
501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
+
errors.push(`${endpoint}: ${message}`);
|
|
503
|
+
this.stats.signaling_errors += 1;
|
|
504
|
+
this.lastError = message;
|
|
505
|
+
this.lastErrorAt = Date.now();
|
|
506
|
+
this.reconnectAttemptsTotal += 1;
|
|
507
|
+
this.recordDiscovery("signaling_error", { endpoint, detail: message });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
throw new Error(errors.join(" | "));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private updatePeersFromList(values: unknown[]): void {
|
|
514
|
+
const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
|
|
515
|
+
if (!peerIds.includes(this.peerId)) {
|
|
516
|
+
void this.joinRoom("self_missing_from_peers").catch(() => {});
|
|
517
|
+
}
|
|
518
|
+
const now = Date.now();
|
|
519
|
+
const next = new Map<string, RelayPeer>();
|
|
520
|
+
for (const peerId of peerIds) {
|
|
521
|
+
if (peerId === this.peerId) continue;
|
|
522
|
+
const existing = this.peers.get(peerId);
|
|
523
|
+
if (!existing) {
|
|
524
|
+
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
525
|
+
}
|
|
526
|
+
next.set(peerId, {
|
|
527
|
+
peer_id: peerId,
|
|
528
|
+
status: "online",
|
|
529
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
530
|
+
last_seen_at: now,
|
|
531
|
+
messages_seen: existing?.messages_seen ?? 0,
|
|
532
|
+
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
for (const peerId of this.peers.keys()) {
|
|
536
|
+
if (!next.has(peerId)) {
|
|
537
|
+
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
this.peers = next;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private scheduleNextPoll(delayMs: number): void {
|
|
544
|
+
if (this.poller) {
|
|
545
|
+
clearTimeout(this.poller);
|
|
546
|
+
}
|
|
547
|
+
const jitterMs = Math.floor(Math.random() * 400);
|
|
548
|
+
this.poller = setTimeout(() => {
|
|
549
|
+
this.pollOnce().catch(() => {});
|
|
550
|
+
}, Math.max(1000, delayMs + jitterMs));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./jsonRepo"), exports);
|
|
18
|
+
__exportStar(require("./repos"), exports);
|
|
19
|
+
__exportStar(require("./socialRuntimeRepo"), exports);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonFileRepo = void 0;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
class JsonFileRepo {
|
|
7
|
+
filePath;
|
|
8
|
+
fallback;
|
|
9
|
+
constructor(filePath, fallback) {
|
|
10
|
+
this.filePath = filePath;
|
|
11
|
+
this.fallback = fallback;
|
|
12
|
+
}
|
|
13
|
+
async get() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await (0, promises_1.readFile)(this.filePath, "utf8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const seed = this.fallback();
|
|
20
|
+
await this.set(seed);
|
|
21
|
+
return seed;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async set(value) {
|
|
25
|
+
await (0, promises_1.mkdir)((0, path_1.dirname)(this.filePath), { recursive: true });
|
|
26
|
+
await (0, promises_1.writeFile)(this.filePath, JSON.stringify(value, null, 2), "utf8");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.JsonFileRepo = JsonFileRepo;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AgentIdentity, DirectoryState, PublicProfile } from "@silicaclaw/core";
|
|
2
|
+
import { JsonFileRepo } from "./jsonRepo";
|
|
3
|
+
export type LogEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
level: "info" | "warn" | "error";
|
|
6
|
+
message: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
};
|
|
9
|
+
export declare class IdentityRepo extends JsonFileRepo<AgentIdentity | null> {
|
|
10
|
+
constructor(rootDir?: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class ProfileRepo extends JsonFileRepo<PublicProfile | null> {
|
|
13
|
+
constructor(rootDir?: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class CacheRepo extends JsonFileRepo<DirectoryState> {
|
|
16
|
+
constructor(rootDir?: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class LogRepo extends JsonFileRepo<LogEntry[]> {
|
|
19
|
+
constructor(rootDir?: string);
|
|
20
|
+
append(entry: Omit<LogEntry, "id">): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogRepo = exports.CacheRepo = exports.ProfileRepo = exports.IdentityRepo = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const core_1 = require("@silicaclaw/core");
|
|
6
|
+
const jsonRepo_1 = require("./jsonRepo");
|
|
7
|
+
class IdentityRepo extends jsonRepo_1.JsonFileRepo {
|
|
8
|
+
constructor(rootDir = process.cwd()) {
|
|
9
|
+
super((0, path_1.resolve)(rootDir, "data", "identity.json"), () => null);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.IdentityRepo = IdentityRepo;
|
|
13
|
+
class ProfileRepo extends jsonRepo_1.JsonFileRepo {
|
|
14
|
+
constructor(rootDir = process.cwd()) {
|
|
15
|
+
super((0, path_1.resolve)(rootDir, "data", "profile.json"), () => null);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ProfileRepo = ProfileRepo;
|
|
19
|
+
class CacheRepo extends jsonRepo_1.JsonFileRepo {
|
|
20
|
+
constructor(rootDir = process.cwd()) {
|
|
21
|
+
super((0, path_1.resolve)(rootDir, "data", "cache.json"), () => (0, core_1.createEmptyDirectoryState)());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.CacheRepo = CacheRepo;
|
|
25
|
+
class LogRepo extends jsonRepo_1.JsonFileRepo {
|
|
26
|
+
constructor(rootDir = process.cwd()) {
|
|
27
|
+
super((0, path_1.resolve)(rootDir, "data", "logs.json"), () => []);
|
|
28
|
+
}
|
|
29
|
+
async append(entry) {
|
|
30
|
+
const current = await this.get();
|
|
31
|
+
const next = [
|
|
32
|
+
{
|
|
33
|
+
id: `${entry.timestamp}-${Math.random().toString(36).slice(2, 8)}`,
|
|
34
|
+
...entry,
|
|
35
|
+
},
|
|
36
|
+
...current,
|
|
37
|
+
].slice(0, 50);
|
|
38
|
+
await this.set(next);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.LogRepo = LogRepo;
|