@silicaclaw/cli 1.0.0-beta.8 → 2026.3.18-2
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 +24 -13
- package/README.md +62 -18
- package/VERSION +1 -1
- package/apps/local-console/package.json +1 -0
- package/apps/local-console/public/index.html +2113 -473
- package/apps/local-console/src/server.ts +108 -31
- package/apps/public-explorer/public/index.html +283 -61
- 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 +2 -2
- package/packages/core/dist/socialConfig.d.ts +1 -1
- package/packages/core/dist/socialConfig.js +7 -6
- package/packages/core/dist/socialResolver.js +15 -5
- package/packages/core/src/socialConfig.ts +8 -7
- package/packages/core/src/socialResolver.ts +17 -5
- package/packages/network/dist/index.d.ts +1 -0
- package/packages/network/dist/index.js +1 -0
- package/packages/network/dist/relayPreview.d.ts +166 -0
- package/packages/network/dist/relayPreview.js +430 -0
- package/packages/network/src/index.ts +1 -0
- package/packages/network/src/relayPreview.ts +552 -0
- package/packages/storage/dist/socialRuntimeRepo.js +4 -4
- package/packages/storage/src/socialRuntimeRepo.ts +4 -4
- package/scripts/quickstart.sh +269 -43
- package/scripts/silicaclaw-cli.mjs +418 -56
- package/scripts/silicaclaw-gateway.mjs +197 -46
- package/scripts/webrtc-signaling-server.mjs +89 -5
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NetworkAdapter } from "./types";
|
|
2
|
+
type RelayPreviewOptions = {
|
|
3
|
+
peerId?: string;
|
|
4
|
+
namespace?: string;
|
|
5
|
+
signalingUrl?: string;
|
|
6
|
+
signalingUrls?: string[];
|
|
7
|
+
room?: string;
|
|
8
|
+
seedPeers?: string[];
|
|
9
|
+
bootstrapHints?: string[];
|
|
10
|
+
bootstrapSources?: string[];
|
|
11
|
+
maxMessageBytes?: number;
|
|
12
|
+
pollIntervalMs?: number;
|
|
13
|
+
maxFutureDriftMs?: number;
|
|
14
|
+
maxPastDriftMs?: number;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
peerRefreshIntervalMs?: number;
|
|
17
|
+
};
|
|
18
|
+
type RelayPeer = {
|
|
19
|
+
peer_id: string;
|
|
20
|
+
status: "online";
|
|
21
|
+
first_seen_at: number;
|
|
22
|
+
last_seen_at: number;
|
|
23
|
+
messages_seen: number;
|
|
24
|
+
reconnect_attempts: number;
|
|
25
|
+
};
|
|
26
|
+
type RelayDiagnostics = {
|
|
27
|
+
adapter: "relay-preview";
|
|
28
|
+
peer_id: string;
|
|
29
|
+
namespace: string;
|
|
30
|
+
room: string;
|
|
31
|
+
signaling_url: string;
|
|
32
|
+
signaling_endpoints: string[];
|
|
33
|
+
active_endpoint_index: number;
|
|
34
|
+
bootstrap_sources: string[];
|
|
35
|
+
seed_peers_count: number;
|
|
36
|
+
bootstrap_hints_count: number;
|
|
37
|
+
discovery_events_total: number;
|
|
38
|
+
last_discovery_event_at: number;
|
|
39
|
+
last_join_at: number;
|
|
40
|
+
last_poll_at: number;
|
|
41
|
+
last_publish_at: number;
|
|
42
|
+
last_peer_refresh_at: number;
|
|
43
|
+
last_error_at: number;
|
|
44
|
+
last_error: string | null;
|
|
45
|
+
discovery_events: Array<{
|
|
46
|
+
id: string;
|
|
47
|
+
type: string;
|
|
48
|
+
at: number;
|
|
49
|
+
peer_id?: string;
|
|
50
|
+
endpoint?: string;
|
|
51
|
+
detail?: string;
|
|
52
|
+
}>;
|
|
53
|
+
signaling_messages_sent_total: number;
|
|
54
|
+
signaling_messages_received_total: number;
|
|
55
|
+
reconnect_attempts_total: number;
|
|
56
|
+
active_webrtc_peers: number;
|
|
57
|
+
components: {
|
|
58
|
+
transport: string;
|
|
59
|
+
discovery: string;
|
|
60
|
+
envelope_codec: string;
|
|
61
|
+
topic_codec: string;
|
|
62
|
+
};
|
|
63
|
+
limits: {
|
|
64
|
+
max_message_bytes: number;
|
|
65
|
+
max_future_drift_ms: number;
|
|
66
|
+
max_past_drift_ms: number;
|
|
67
|
+
};
|
|
68
|
+
config: {
|
|
69
|
+
started: boolean;
|
|
70
|
+
topic_handler_count: number;
|
|
71
|
+
poll_interval_ms: number;
|
|
72
|
+
};
|
|
73
|
+
peers: {
|
|
74
|
+
total: number;
|
|
75
|
+
online: number;
|
|
76
|
+
stale: number;
|
|
77
|
+
items: RelayPeer[];
|
|
78
|
+
};
|
|
79
|
+
stats: {
|
|
80
|
+
publish_attempted: number;
|
|
81
|
+
publish_sent: number;
|
|
82
|
+
received_total: number;
|
|
83
|
+
delivered_total: number;
|
|
84
|
+
dropped_malformed: number;
|
|
85
|
+
dropped_oversized: number;
|
|
86
|
+
dropped_namespace_mismatch: number;
|
|
87
|
+
dropped_timestamp_future_drift: number;
|
|
88
|
+
dropped_timestamp_past_drift: number;
|
|
89
|
+
dropped_decode_failed: number;
|
|
90
|
+
dropped_self: number;
|
|
91
|
+
dropped_topic_decode_error: number;
|
|
92
|
+
dropped_handler_error: number;
|
|
93
|
+
signaling_errors: number;
|
|
94
|
+
invalid_signaling_payload_total: number;
|
|
95
|
+
duplicate_sdp_total: number;
|
|
96
|
+
duplicate_ice_total: number;
|
|
97
|
+
start_errors: number;
|
|
98
|
+
stop_errors: number;
|
|
99
|
+
received_validated: number;
|
|
100
|
+
join_attempted: number;
|
|
101
|
+
join_succeeded: number;
|
|
102
|
+
poll_attempted: number;
|
|
103
|
+
poll_succeeded: number;
|
|
104
|
+
peers_refresh_attempted: number;
|
|
105
|
+
peers_refresh_succeeded: number;
|
|
106
|
+
publish_succeeded: number;
|
|
107
|
+
poll_skipped_inflight: number;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export declare class RelayPreviewAdapter implements NetworkAdapter {
|
|
111
|
+
private readonly peerId;
|
|
112
|
+
private readonly namespace;
|
|
113
|
+
private readonly signalingEndpoints;
|
|
114
|
+
private readonly room;
|
|
115
|
+
private readonly seedPeers;
|
|
116
|
+
private readonly bootstrapHints;
|
|
117
|
+
private readonly bootstrapSources;
|
|
118
|
+
private readonly maxMessageBytes;
|
|
119
|
+
private readonly pollIntervalMs;
|
|
120
|
+
private readonly maxFutureDriftMs;
|
|
121
|
+
private readonly maxPastDriftMs;
|
|
122
|
+
private readonly requestTimeoutMs;
|
|
123
|
+
private readonly peerRefreshIntervalMs;
|
|
124
|
+
private readonly envelopeCodec;
|
|
125
|
+
private readonly topicCodec;
|
|
126
|
+
private started;
|
|
127
|
+
private poller;
|
|
128
|
+
private handlers;
|
|
129
|
+
private peers;
|
|
130
|
+
private seenMessageIds;
|
|
131
|
+
private activeEndpoint;
|
|
132
|
+
private discoveryEvents;
|
|
133
|
+
private discoveryEventsTotal;
|
|
134
|
+
private lastDiscoveryEventAt;
|
|
135
|
+
private signalingMessagesSentTotal;
|
|
136
|
+
private signalingMessagesReceivedTotal;
|
|
137
|
+
private reconnectAttemptsTotal;
|
|
138
|
+
private activeEndpointIndex;
|
|
139
|
+
private lastJoinAt;
|
|
140
|
+
private lastPollAt;
|
|
141
|
+
private lastPublishAt;
|
|
142
|
+
private lastPeerRefreshAt;
|
|
143
|
+
private lastErrorAt;
|
|
144
|
+
private lastError;
|
|
145
|
+
private pollInFlight;
|
|
146
|
+
private currentPollDelayMs;
|
|
147
|
+
private stats;
|
|
148
|
+
constructor(options?: RelayPreviewOptions);
|
|
149
|
+
start(): Promise<void>;
|
|
150
|
+
stop(): Promise<void>;
|
|
151
|
+
publish(topic: string, data: any): Promise<void>;
|
|
152
|
+
subscribe(topic: string, handler: (data: any) => void): void;
|
|
153
|
+
getDiagnostics(): RelayDiagnostics;
|
|
154
|
+
private pollOnce;
|
|
155
|
+
private refreshPeers;
|
|
156
|
+
private onEnvelope;
|
|
157
|
+
private recordDiscovery;
|
|
158
|
+
private joinRoom;
|
|
159
|
+
private maybeRefreshJoin;
|
|
160
|
+
private get;
|
|
161
|
+
private post;
|
|
162
|
+
private requestJson;
|
|
163
|
+
private updatePeersFromList;
|
|
164
|
+
private scheduleNextPoll;
|
|
165
|
+
}
|
|
166
|
+
export {};
|
|
@@ -0,0 +1,430 @@
|
|
|
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
|
+
requestTimeoutMs;
|
|
24
|
+
peerRefreshIntervalMs;
|
|
25
|
+
envelopeCodec;
|
|
26
|
+
topicCodec;
|
|
27
|
+
started = false;
|
|
28
|
+
poller = null;
|
|
29
|
+
handlers = new Map();
|
|
30
|
+
peers = new Map();
|
|
31
|
+
seenMessageIds = new Set();
|
|
32
|
+
activeEndpoint = "";
|
|
33
|
+
discoveryEvents = [];
|
|
34
|
+
discoveryEventsTotal = 0;
|
|
35
|
+
lastDiscoveryEventAt = 0;
|
|
36
|
+
signalingMessagesSentTotal = 0;
|
|
37
|
+
signalingMessagesReceivedTotal = 0;
|
|
38
|
+
reconnectAttemptsTotal = 0;
|
|
39
|
+
activeEndpointIndex = 0;
|
|
40
|
+
lastJoinAt = 0;
|
|
41
|
+
lastPollAt = 0;
|
|
42
|
+
lastPublishAt = 0;
|
|
43
|
+
lastPeerRefreshAt = 0;
|
|
44
|
+
lastErrorAt = 0;
|
|
45
|
+
lastError = null;
|
|
46
|
+
pollInFlight = false;
|
|
47
|
+
currentPollDelayMs = 0;
|
|
48
|
+
stats = {
|
|
49
|
+
publish_attempted: 0,
|
|
50
|
+
publish_sent: 0,
|
|
51
|
+
received_total: 0,
|
|
52
|
+
delivered_total: 0,
|
|
53
|
+
dropped_malformed: 0,
|
|
54
|
+
dropped_oversized: 0,
|
|
55
|
+
dropped_namespace_mismatch: 0,
|
|
56
|
+
dropped_timestamp_future_drift: 0,
|
|
57
|
+
dropped_timestamp_past_drift: 0,
|
|
58
|
+
dropped_decode_failed: 0,
|
|
59
|
+
dropped_self: 0,
|
|
60
|
+
dropped_topic_decode_error: 0,
|
|
61
|
+
dropped_handler_error: 0,
|
|
62
|
+
signaling_errors: 0,
|
|
63
|
+
invalid_signaling_payload_total: 0,
|
|
64
|
+
duplicate_sdp_total: 0,
|
|
65
|
+
duplicate_ice_total: 0,
|
|
66
|
+
start_errors: 0,
|
|
67
|
+
stop_errors: 0,
|
|
68
|
+
received_validated: 0,
|
|
69
|
+
join_attempted: 0,
|
|
70
|
+
join_succeeded: 0,
|
|
71
|
+
poll_attempted: 0,
|
|
72
|
+
poll_succeeded: 0,
|
|
73
|
+
peers_refresh_attempted: 0,
|
|
74
|
+
peers_refresh_succeeded: 0,
|
|
75
|
+
publish_succeeded: 0,
|
|
76
|
+
poll_skipped_inflight: 0,
|
|
77
|
+
};
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
80
|
+
this.namespace = String(options.namespace || "silicaclaw.preview").trim() || "silicaclaw.preview";
|
|
81
|
+
this.signalingEndpoints = dedupe((options.signalingUrls && options.signalingUrls.length > 0
|
|
82
|
+
? options.signalingUrls
|
|
83
|
+
: [options.signalingUrl || "http://localhost:4510"]));
|
|
84
|
+
this.activeEndpoint = this.signalingEndpoints[0] || "http://localhost:4510";
|
|
85
|
+
this.activeEndpointIndex = 0;
|
|
86
|
+
this.room = String(options.room || "silicaclaw-global-preview").trim() || "silicaclaw-global-preview";
|
|
87
|
+
this.seedPeers = dedupe(options.seedPeers || []);
|
|
88
|
+
this.bootstrapHints = dedupe(options.bootstrapHints || []);
|
|
89
|
+
this.bootstrapSources = dedupe(options.bootstrapSources || []);
|
|
90
|
+
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
91
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
92
|
+
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
93
|
+
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
94
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 4000;
|
|
95
|
+
this.peerRefreshIntervalMs = options.peerRefreshIntervalMs ?? 30_000;
|
|
96
|
+
this.envelopeCodec = new jsonMessageEnvelopeCodec_1.JsonMessageEnvelopeCodec();
|
|
97
|
+
this.topicCodec = new jsonTopicCodec_1.JsonTopicCodec();
|
|
98
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
99
|
+
}
|
|
100
|
+
async start() {
|
|
101
|
+
if (this.started)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
await this.joinRoom("start");
|
|
105
|
+
this.started = true;
|
|
106
|
+
await this.refreshPeers();
|
|
107
|
+
await this.pollOnce();
|
|
108
|
+
this.scheduleNextPoll(this.pollIntervalMs);
|
|
109
|
+
this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.stats.start_errors += 1;
|
|
113
|
+
throw new Error(`Relay start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async stop() {
|
|
117
|
+
if (!this.started)
|
|
118
|
+
return;
|
|
119
|
+
if (this.poller) {
|
|
120
|
+
clearTimeout(this.poller);
|
|
121
|
+
this.poller = null;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await this.post("/leave", { room: this.room, peer_id: this.peerId });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
this.stats.stop_errors += 1;
|
|
128
|
+
}
|
|
129
|
+
this.started = false;
|
|
130
|
+
this.recordDiscovery("signaling_disconnected", { endpoint: this.activeEndpoint });
|
|
131
|
+
}
|
|
132
|
+
async publish(topic, data) {
|
|
133
|
+
if (!this.started)
|
|
134
|
+
return;
|
|
135
|
+
this.stats.publish_attempted += 1;
|
|
136
|
+
await this.maybeRefreshJoin("publish");
|
|
137
|
+
const envelope = {
|
|
138
|
+
version: 1,
|
|
139
|
+
message_id: (0, crypto_1.randomUUID)(),
|
|
140
|
+
topic: `${this.namespace}:${topic}`,
|
|
141
|
+
source_peer_id: this.peerId,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
payload: this.topicCodec.encode(topic, data),
|
|
144
|
+
};
|
|
145
|
+
const raw = this.envelopeCodec.encode(envelope);
|
|
146
|
+
if (raw.length > this.maxMessageBytes) {
|
|
147
|
+
this.stats.dropped_oversized += 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await this.post("/relay/publish", { room: this.room, peer_id: this.peerId, envelope });
|
|
151
|
+
this.lastPublishAt = Date.now();
|
|
152
|
+
this.stats.publish_sent += 1;
|
|
153
|
+
this.stats.publish_succeeded += 1;
|
|
154
|
+
this.signalingMessagesSentTotal += 1;
|
|
155
|
+
}
|
|
156
|
+
subscribe(topic, handler) {
|
|
157
|
+
const key = `${this.namespace}:${topic}`;
|
|
158
|
+
if (!this.handlers.has(key)) {
|
|
159
|
+
this.handlers.set(key, new Set());
|
|
160
|
+
}
|
|
161
|
+
this.handlers.get(key)?.add(handler);
|
|
162
|
+
}
|
|
163
|
+
getDiagnostics() {
|
|
164
|
+
const peerItems = Array.from(this.peers.values()).sort((a, b) => b.last_seen_at - a.last_seen_at);
|
|
165
|
+
return {
|
|
166
|
+
adapter: "relay-preview",
|
|
167
|
+
peer_id: this.peerId,
|
|
168
|
+
namespace: this.namespace,
|
|
169
|
+
room: this.room,
|
|
170
|
+
signaling_url: this.activeEndpoint,
|
|
171
|
+
signaling_endpoints: this.signalingEndpoints,
|
|
172
|
+
active_endpoint_index: this.activeEndpointIndex,
|
|
173
|
+
bootstrap_sources: this.bootstrapSources,
|
|
174
|
+
seed_peers_count: this.seedPeers.length,
|
|
175
|
+
bootstrap_hints_count: this.bootstrapHints.length,
|
|
176
|
+
discovery_events_total: this.discoveryEventsTotal,
|
|
177
|
+
last_discovery_event_at: this.lastDiscoveryEventAt,
|
|
178
|
+
last_join_at: this.lastJoinAt,
|
|
179
|
+
last_poll_at: this.lastPollAt,
|
|
180
|
+
last_publish_at: this.lastPublishAt,
|
|
181
|
+
last_peer_refresh_at: this.lastPeerRefreshAt,
|
|
182
|
+
last_error_at: this.lastErrorAt,
|
|
183
|
+
last_error: this.lastError,
|
|
184
|
+
discovery_events: this.discoveryEvents,
|
|
185
|
+
signaling_messages_sent_total: this.signalingMessagesSentTotal,
|
|
186
|
+
signaling_messages_received_total: this.signalingMessagesReceivedTotal,
|
|
187
|
+
reconnect_attempts_total: this.reconnectAttemptsTotal,
|
|
188
|
+
active_webrtc_peers: peerItems.length,
|
|
189
|
+
components: {
|
|
190
|
+
transport: "HttpRelayTransport",
|
|
191
|
+
discovery: "RelayRoomPeerList",
|
|
192
|
+
envelope_codec: this.envelopeCodec.constructor.name,
|
|
193
|
+
topic_codec: this.topicCodec.constructor.name,
|
|
194
|
+
},
|
|
195
|
+
limits: {
|
|
196
|
+
max_message_bytes: this.maxMessageBytes,
|
|
197
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
198
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
199
|
+
},
|
|
200
|
+
config: {
|
|
201
|
+
started: this.started,
|
|
202
|
+
topic_handler_count: this.handlers.size,
|
|
203
|
+
poll_interval_ms: this.pollIntervalMs,
|
|
204
|
+
},
|
|
205
|
+
peers: {
|
|
206
|
+
total: peerItems.length,
|
|
207
|
+
online: peerItems.length,
|
|
208
|
+
stale: 0,
|
|
209
|
+
items: peerItems,
|
|
210
|
+
},
|
|
211
|
+
stats: { ...this.stats },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async pollOnce() {
|
|
215
|
+
if (this.pollInFlight) {
|
|
216
|
+
this.stats.poll_skipped_inflight += 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.pollInFlight = true;
|
|
220
|
+
await this.maybeRefreshJoin("poll");
|
|
221
|
+
this.stats.poll_attempted += 1;
|
|
222
|
+
try {
|
|
223
|
+
const payload = await this.get(`/relay/poll?room=${encodeURIComponent(this.room)}&peer_id=${encodeURIComponent(this.peerId)}`);
|
|
224
|
+
this.lastPollAt = Date.now();
|
|
225
|
+
this.stats.poll_succeeded += 1;
|
|
226
|
+
this.currentPollDelayMs = this.pollIntervalMs;
|
|
227
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
228
|
+
for (const message of messages) {
|
|
229
|
+
this.signalingMessagesReceivedTotal += 1;
|
|
230
|
+
this.onEnvelope(message?.envelope);
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(payload?.peers)) {
|
|
233
|
+
this.updatePeersFromList(payload.peers);
|
|
234
|
+
}
|
|
235
|
+
else if (!this.lastPeerRefreshAt || Date.now() - this.lastPeerRefreshAt >= this.peerRefreshIntervalMs) {
|
|
236
|
+
await this.refreshPeers();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.currentPollDelayMs = Math.min(15_000, Math.max(this.pollIntervalMs, this.currentPollDelayMs * 2));
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.pollInFlight = false;
|
|
245
|
+
if (this.started) {
|
|
246
|
+
this.scheduleNextPoll(this.currentPollDelayMs);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async refreshPeers() {
|
|
251
|
+
this.stats.peers_refresh_attempted += 1;
|
|
252
|
+
const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
|
|
253
|
+
this.lastPeerRefreshAt = Date.now();
|
|
254
|
+
this.stats.peers_refresh_succeeded += 1;
|
|
255
|
+
const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
|
|
256
|
+
this.updatePeersFromList(peerIds);
|
|
257
|
+
}
|
|
258
|
+
onEnvelope(envelope) {
|
|
259
|
+
this.stats.received_total += 1;
|
|
260
|
+
const validated = (0, messageEnvelope_1.validateNetworkMessageEnvelope)(envelope, {
|
|
261
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
262
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
263
|
+
});
|
|
264
|
+
if (!validated.ok || !validated.envelope) {
|
|
265
|
+
if (validated.reason === "timestamp_future_drift") {
|
|
266
|
+
this.stats.dropped_timestamp_future_drift += 1;
|
|
267
|
+
}
|
|
268
|
+
else if (validated.reason === "timestamp_past_drift") {
|
|
269
|
+
this.stats.dropped_timestamp_past_drift += 1;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
this.stats.dropped_malformed += 1;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const message = validated.envelope;
|
|
277
|
+
if (message.source_peer_id === this.peerId) {
|
|
278
|
+
this.stats.dropped_self += 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!message.topic.startsWith(`${this.namespace}:`)) {
|
|
282
|
+
this.stats.dropped_namespace_mismatch += 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (this.seenMessageIds.has(message.message_id)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.seenMessageIds.add(message.message_id);
|
|
289
|
+
if (this.seenMessageIds.size > 10000) {
|
|
290
|
+
const first = this.seenMessageIds.values().next().value;
|
|
291
|
+
if (first)
|
|
292
|
+
this.seenMessageIds.delete(first);
|
|
293
|
+
}
|
|
294
|
+
this.stats.received_validated += 1;
|
|
295
|
+
const topicKey = message.topic;
|
|
296
|
+
const topic = topicKey.slice(this.namespace.length + 1);
|
|
297
|
+
const handlers = this.handlers.get(topicKey);
|
|
298
|
+
if (!handlers || handlers.size === 0)
|
|
299
|
+
return;
|
|
300
|
+
const peer = this.peers.get(message.source_peer_id);
|
|
301
|
+
if (peer) {
|
|
302
|
+
peer.last_seen_at = Date.now();
|
|
303
|
+
peer.messages_seen += 1;
|
|
304
|
+
}
|
|
305
|
+
let payload;
|
|
306
|
+
try {
|
|
307
|
+
payload = this.topicCodec.decode(topic, message.payload);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
this.stats.dropped_topic_decode_error += 1;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
for (const handler of handlers) {
|
|
314
|
+
try {
|
|
315
|
+
handler(payload);
|
|
316
|
+
this.stats.delivered_total += 1;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
this.stats.dropped_handler_error += 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
recordDiscovery(type, extra = {}) {
|
|
324
|
+
const event = {
|
|
325
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
326
|
+
type,
|
|
327
|
+
at: Date.now(),
|
|
328
|
+
...extra,
|
|
329
|
+
};
|
|
330
|
+
this.discoveryEvents.unshift(event);
|
|
331
|
+
this.discoveryEvents = this.discoveryEvents.slice(0, 200);
|
|
332
|
+
this.discoveryEventsTotal += 1;
|
|
333
|
+
this.lastDiscoveryEventAt = event.at;
|
|
334
|
+
}
|
|
335
|
+
async joinRoom(reason) {
|
|
336
|
+
this.stats.join_attempted += 1;
|
|
337
|
+
await this.post("/join", { room: this.room, peer_id: this.peerId });
|
|
338
|
+
this.lastJoinAt = Date.now();
|
|
339
|
+
this.stats.join_succeeded += 1;
|
|
340
|
+
this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
|
|
341
|
+
}
|
|
342
|
+
async maybeRefreshJoin(reason) {
|
|
343
|
+
if (!this.lastJoinAt || Date.now() - this.lastJoinAt > Math.max(45_000, this.pollIntervalMs * 6)) {
|
|
344
|
+
await this.joinRoom(reason);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async get(path) {
|
|
348
|
+
return this.requestJson("GET", path);
|
|
349
|
+
}
|
|
350
|
+
async post(path, body) {
|
|
351
|
+
return this.requestJson("POST", path, body);
|
|
352
|
+
}
|
|
353
|
+
async requestJson(method, path, body) {
|
|
354
|
+
const errors = [];
|
|
355
|
+
for (let offset = 0; offset < this.signalingEndpoints.length; offset += 1) {
|
|
356
|
+
const index = (this.activeEndpointIndex + offset) % this.signalingEndpoints.length;
|
|
357
|
+
const endpoint = this.signalingEndpoints[index]?.replace(/\/+$/, "");
|
|
358
|
+
if (!endpoint)
|
|
359
|
+
continue;
|
|
360
|
+
try {
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
363
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
364
|
+
method,
|
|
365
|
+
headers: method === "POST" ? { "content-type": "application/json" } : undefined,
|
|
366
|
+
body: method === "POST" ? JSON.stringify(body) : undefined,
|
|
367
|
+
signal: controller.signal,
|
|
368
|
+
});
|
|
369
|
+
clearTimeout(timeout);
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
throw new Error(`${method} ${path} failed (${response.status})`);
|
|
372
|
+
}
|
|
373
|
+
this.activeEndpointIndex = index;
|
|
374
|
+
this.activeEndpoint = endpoint;
|
|
375
|
+
this.lastError = null;
|
|
376
|
+
return response.json();
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
errors.push(`${endpoint}: ${message}`);
|
|
381
|
+
this.stats.signaling_errors += 1;
|
|
382
|
+
this.lastError = message;
|
|
383
|
+
this.lastErrorAt = Date.now();
|
|
384
|
+
this.reconnectAttemptsTotal += 1;
|
|
385
|
+
this.recordDiscovery("signaling_error", { endpoint, detail: message });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
throw new Error(errors.join(" | "));
|
|
389
|
+
}
|
|
390
|
+
updatePeersFromList(values) {
|
|
391
|
+
const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
|
|
392
|
+
if (!peerIds.includes(this.peerId)) {
|
|
393
|
+
void this.joinRoom("self_missing_from_peers").catch(() => { });
|
|
394
|
+
}
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const next = new Map();
|
|
397
|
+
for (const peerId of peerIds) {
|
|
398
|
+
if (peerId === this.peerId)
|
|
399
|
+
continue;
|
|
400
|
+
const existing = this.peers.get(peerId);
|
|
401
|
+
if (!existing) {
|
|
402
|
+
this.recordDiscovery("peer_joined", { peer_id: peerId });
|
|
403
|
+
}
|
|
404
|
+
next.set(peerId, {
|
|
405
|
+
peer_id: peerId,
|
|
406
|
+
status: "online",
|
|
407
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
408
|
+
last_seen_at: now,
|
|
409
|
+
messages_seen: existing?.messages_seen ?? 0,
|
|
410
|
+
reconnect_attempts: existing?.reconnect_attempts ?? 0,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (const peerId of this.peers.keys()) {
|
|
414
|
+
if (!next.has(peerId)) {
|
|
415
|
+
this.recordDiscovery("peer_removed", { peer_id: peerId });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.peers = next;
|
|
419
|
+
}
|
|
420
|
+
scheduleNextPoll(delayMs) {
|
|
421
|
+
if (this.poller) {
|
|
422
|
+
clearTimeout(this.poller);
|
|
423
|
+
}
|
|
424
|
+
const jitterMs = Math.floor(Math.random() * 400);
|
|
425
|
+
this.poller = setTimeout(() => {
|
|
426
|
+
this.pollOnce().catch(() => { });
|
|
427
|
+
}, Math.max(1000, delayMs + jitterMs));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
exports.RelayPreviewAdapter = RelayPreviewAdapter;
|