@silicaclaw/cli 1.0.0-beta.1 → 1.0.0-beta.10
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/INSTALL.md +28 -0
- package/README.md +32 -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 +287 -0
- package/packages/core/dist/socialResolver.d.ts +46 -0
- package/packages/core/dist/socialResolver.js +227 -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/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 +13 -0
- package/packages/network/dist/index.js +29 -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/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/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/tsconfig.json +6 -1
- package/scripts/quickstart.sh +156 -15
- package/scripts/silicaclaw-cli.mjs +103 -0
- package/scripts/silicaclaw-gateway.mjs +321 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RealNetworkAdapterPreview = 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
|
+
const udpLanBroadcastTransport_1 = require("./transport/udpLanBroadcastTransport");
|
|
9
|
+
const heartbeatPeerDiscovery_1 = require("./discovery/heartbeatPeerDiscovery");
|
|
10
|
+
class RealNetworkAdapterPreview {
|
|
11
|
+
started = false;
|
|
12
|
+
peerId;
|
|
13
|
+
namespace;
|
|
14
|
+
transport;
|
|
15
|
+
envelopeCodec;
|
|
16
|
+
topicCodec;
|
|
17
|
+
peerDiscovery;
|
|
18
|
+
maxMessageBytes;
|
|
19
|
+
dedupeWindowMs;
|
|
20
|
+
dedupeMaxEntries;
|
|
21
|
+
maxFutureDriftMs;
|
|
22
|
+
maxPastDriftMs;
|
|
23
|
+
seenMessageIds = new Map();
|
|
24
|
+
offTransportMessage = null;
|
|
25
|
+
handlers = new Map();
|
|
26
|
+
stats = {
|
|
27
|
+
publish_attempted: 0,
|
|
28
|
+
publish_sent: 0,
|
|
29
|
+
received_total: 0,
|
|
30
|
+
delivered_total: 0,
|
|
31
|
+
dropped_duplicate: 0,
|
|
32
|
+
dropped_self: 0,
|
|
33
|
+
dropped_malformed: 0,
|
|
34
|
+
dropped_oversized: 0,
|
|
35
|
+
dropped_namespace_mismatch: 0,
|
|
36
|
+
dropped_timestamp_future_drift: 0,
|
|
37
|
+
dropped_timestamp_past_drift: 0,
|
|
38
|
+
dropped_decode_failed: 0,
|
|
39
|
+
dropped_topic_decode_error: 0,
|
|
40
|
+
dropped_handler_error: 0,
|
|
41
|
+
send_errors: 0,
|
|
42
|
+
discovery_errors: 0,
|
|
43
|
+
start_errors: 0,
|
|
44
|
+
stop_errors: 0,
|
|
45
|
+
received_validated: 0,
|
|
46
|
+
};
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
49
|
+
this.namespace = this.normalizeNamespace(options.namespace ?? "silicaclaw.preview");
|
|
50
|
+
this.transport = options.transport ?? new udpLanBroadcastTransport_1.UdpLanBroadcastTransport();
|
|
51
|
+
this.envelopeCodec = options.envelopeCodec ?? new jsonMessageEnvelopeCodec_1.JsonMessageEnvelopeCodec();
|
|
52
|
+
this.topicCodec = options.topicCodec ?? new jsonTopicCodec_1.JsonTopicCodec();
|
|
53
|
+
this.peerDiscovery = options.peerDiscovery ?? new heartbeatPeerDiscovery_1.HeartbeatPeerDiscovery();
|
|
54
|
+
this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
|
|
55
|
+
this.dedupeWindowMs = options.dedupeWindowMs ?? 90_000;
|
|
56
|
+
this.dedupeMaxEntries = options.dedupeMaxEntries ?? 10_000;
|
|
57
|
+
this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
|
|
58
|
+
this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
|
|
59
|
+
}
|
|
60
|
+
async start() {
|
|
61
|
+
if (this.started) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await this.transport.start();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.stats.start_errors += 1;
|
|
69
|
+
throw new Error(`Transport start failed: ${this.errorMessage(error)}`);
|
|
70
|
+
}
|
|
71
|
+
this.started = true;
|
|
72
|
+
this.offTransportMessage = this.transport.onMessage((raw) => {
|
|
73
|
+
this.onTransportMessage(raw);
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await this.peerDiscovery.start({
|
|
77
|
+
self_peer_id: this.peerId,
|
|
78
|
+
publishControl: async (topic, payload) => {
|
|
79
|
+
await this.publish(topic, payload);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.stats.start_errors += 1;
|
|
85
|
+
this.started = false;
|
|
86
|
+
if (this.offTransportMessage) {
|
|
87
|
+
this.offTransportMessage();
|
|
88
|
+
this.offTransportMessage = null;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await this.transport.stop();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
this.stats.stop_errors += 1;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Peer discovery start failed: ${this.errorMessage(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async stop() {
|
|
100
|
+
if (!this.started) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await this.peerDiscovery.stop();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
this.stats.discovery_errors += 1;
|
|
108
|
+
this.stats.stop_errors += 1;
|
|
109
|
+
}
|
|
110
|
+
if (this.offTransportMessage) {
|
|
111
|
+
this.offTransportMessage();
|
|
112
|
+
this.offTransportMessage = null;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await this.transport.stop();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
this.stats.stop_errors += 1;
|
|
119
|
+
}
|
|
120
|
+
this.started = false;
|
|
121
|
+
}
|
|
122
|
+
async publish(topic, data) {
|
|
123
|
+
if (!this.started) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.stats.publish_attempted += 1;
|
|
127
|
+
if (!this.isValidTopic(topic)) {
|
|
128
|
+
this.stats.dropped_malformed += 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const envelope = {
|
|
132
|
+
version: 1,
|
|
133
|
+
message_id: (0, crypto_1.randomUUID)(),
|
|
134
|
+
topic: this.topicKey(topic),
|
|
135
|
+
source_peer_id: this.peerId,
|
|
136
|
+
timestamp: Date.now(),
|
|
137
|
+
payload: this.topicCodec.encode(topic, data),
|
|
138
|
+
};
|
|
139
|
+
const raw = this.envelopeCodec.encode(envelope);
|
|
140
|
+
if (raw.length > this.maxMessageBytes) {
|
|
141
|
+
this.stats.dropped_oversized += 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await this.transport.send(raw);
|
|
146
|
+
this.stats.publish_sent += 1;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
this.stats.send_errors += 1;
|
|
150
|
+
throw new Error("Transport send failed");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
subscribe(topic, handler) {
|
|
154
|
+
if (!this.isValidTopic(topic)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const key = this.topicKey(topic);
|
|
158
|
+
if (!this.handlers.has(key)) {
|
|
159
|
+
this.handlers.set(key, new Set());
|
|
160
|
+
}
|
|
161
|
+
this.handlers.get(key)?.add(handler);
|
|
162
|
+
}
|
|
163
|
+
listPeers() {
|
|
164
|
+
return this.peerDiscovery.listPeers();
|
|
165
|
+
}
|
|
166
|
+
getDiagnostics() {
|
|
167
|
+
const peers = this.listPeers();
|
|
168
|
+
const online = peers.filter((peer) => peer.status === "online").length;
|
|
169
|
+
return {
|
|
170
|
+
adapter: "real-preview",
|
|
171
|
+
peer_id: this.peerId,
|
|
172
|
+
namespace: this.namespace,
|
|
173
|
+
components: {
|
|
174
|
+
transport: this.transport.constructor.name,
|
|
175
|
+
discovery: this.peerDiscovery.constructor.name,
|
|
176
|
+
envelope_codec: this.envelopeCodec.constructor.name,
|
|
177
|
+
topic_codec: this.topicCodec.constructor.name,
|
|
178
|
+
},
|
|
179
|
+
limits: {
|
|
180
|
+
max_message_bytes: this.maxMessageBytes,
|
|
181
|
+
dedupe_window_ms: this.dedupeWindowMs,
|
|
182
|
+
dedupe_max_entries: this.dedupeMaxEntries,
|
|
183
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
184
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
185
|
+
},
|
|
186
|
+
config: {
|
|
187
|
+
started: this.started,
|
|
188
|
+
topic_handler_count: this.handlers.size,
|
|
189
|
+
transport: this.transport.getConfig?.() ?? null,
|
|
190
|
+
discovery: this.peerDiscovery.getConfig?.() ?? null,
|
|
191
|
+
},
|
|
192
|
+
peers: {
|
|
193
|
+
total: peers.length,
|
|
194
|
+
online,
|
|
195
|
+
stale: Math.max(0, peers.length - online),
|
|
196
|
+
items: peers,
|
|
197
|
+
},
|
|
198
|
+
stats: { ...this.stats },
|
|
199
|
+
transport_stats: this.transport.getStats?.() ?? null,
|
|
200
|
+
discovery_stats: this.peerDiscovery.getStats?.() ?? null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
onTransportMessage(raw) {
|
|
204
|
+
this.stats.received_total += 1;
|
|
205
|
+
if (raw.length > this.maxMessageBytes) {
|
|
206
|
+
this.stats.dropped_oversized += 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const decoded = this.envelopeCodec.decode(raw);
|
|
210
|
+
if (!decoded) {
|
|
211
|
+
this.stats.dropped_decode_failed += 1;
|
|
212
|
+
this.stats.dropped_malformed += 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const validated = (0, messageEnvelope_1.validateNetworkMessageEnvelope)(decoded.envelope, {
|
|
216
|
+
max_future_drift_ms: this.maxFutureDriftMs,
|
|
217
|
+
max_past_drift_ms: this.maxPastDriftMs,
|
|
218
|
+
});
|
|
219
|
+
if (!validated.ok || !validated.envelope) {
|
|
220
|
+
if (validated.reason === "timestamp_future_drift") {
|
|
221
|
+
this.stats.dropped_timestamp_future_drift += 1;
|
|
222
|
+
}
|
|
223
|
+
else if (validated.reason === "timestamp_past_drift") {
|
|
224
|
+
this.stats.dropped_timestamp_past_drift += 1;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.stats.dropped_malformed += 1;
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.stats.received_validated += 1;
|
|
232
|
+
const envelope = validated.envelope;
|
|
233
|
+
if (!envelope.topic.startsWith(`${this.namespace}:`)) {
|
|
234
|
+
this.stats.dropped_namespace_mismatch += 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (this.isDuplicateMessage(envelope.message_id, envelope.timestamp)) {
|
|
238
|
+
this.stats.dropped_duplicate += 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
this.peerDiscovery.observeEnvelope(envelope);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
this.stats.discovery_errors += 1;
|
|
246
|
+
}
|
|
247
|
+
if (envelope.source_peer_id === this.peerId) {
|
|
248
|
+
this.stats.dropped_self += 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const topic = this.stripNamespace(envelope.topic);
|
|
252
|
+
if (!topic) {
|
|
253
|
+
this.stats.dropped_namespace_mismatch += 1;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const handlers = this.handlers.get(envelope.topic);
|
|
257
|
+
if (!handlers || handlers.size === 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const payload = this.topicCodec.decode(topic, envelope.payload);
|
|
262
|
+
for (const handler of handlers) {
|
|
263
|
+
try {
|
|
264
|
+
handler(payload);
|
|
265
|
+
this.stats.delivered_total += 1;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
this.stats.dropped_handler_error += 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
this.stats.dropped_topic_decode_error += 1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
topicKey(topic) {
|
|
277
|
+
return `${this.namespace}:${topic}`;
|
|
278
|
+
}
|
|
279
|
+
stripNamespace(topicKey) {
|
|
280
|
+
const prefix = `${this.namespace}:`;
|
|
281
|
+
if (!topicKey.startsWith(prefix)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return topicKey.slice(prefix.length);
|
|
285
|
+
}
|
|
286
|
+
isDuplicateMessage(messageId, timestamp) {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
this.cleanupSeenMessageIds(now);
|
|
289
|
+
const existing = this.seenMessageIds.get(messageId);
|
|
290
|
+
if (existing && now - existing <= this.dedupeWindowMs) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
this.seenMessageIds.set(messageId, Number.isFinite(timestamp) ? timestamp : now);
|
|
294
|
+
if (this.seenMessageIds.size > this.dedupeMaxEntries) {
|
|
295
|
+
const oldestKey = this.seenMessageIds.keys().next().value;
|
|
296
|
+
if (oldestKey) {
|
|
297
|
+
this.seenMessageIds.delete(oldestKey);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
cleanupSeenMessageIds(now) {
|
|
303
|
+
for (const [id, ts] of this.seenMessageIds.entries()) {
|
|
304
|
+
if (now - ts > this.dedupeWindowMs) {
|
|
305
|
+
this.seenMessageIds.delete(id);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
isValidTopic(topic) {
|
|
310
|
+
if (typeof topic !== "string") {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const normalized = topic.trim();
|
|
314
|
+
return normalized.length > 0 && !normalized.includes(":");
|
|
315
|
+
}
|
|
316
|
+
normalizeNamespace(namespace) {
|
|
317
|
+
const normalized = namespace.trim();
|
|
318
|
+
return normalized.length > 0 ? normalized : "silicaclaw.preview";
|
|
319
|
+
}
|
|
320
|
+
errorMessage(error) {
|
|
321
|
+
if (error instanceof Error) {
|
|
322
|
+
return error.message;
|
|
323
|
+
}
|
|
324
|
+
return String(error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
exports.RealNetworkAdapterPreview = RealNetworkAdapterPreview;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NetworkTransport, TransportConfigSnapshot, TransportMessageMeta, TransportStats } from "../abstractions/transport";
|
|
2
|
+
type UdpLanBroadcastTransportOptions = {
|
|
3
|
+
port?: number;
|
|
4
|
+
bindAddress?: string;
|
|
5
|
+
broadcastAddress?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class UdpLanBroadcastTransport implements NetworkTransport {
|
|
8
|
+
private socket;
|
|
9
|
+
private handlers;
|
|
10
|
+
private state;
|
|
11
|
+
private stats;
|
|
12
|
+
private port;
|
|
13
|
+
private bindAddress;
|
|
14
|
+
private broadcastAddress;
|
|
15
|
+
constructor(options?: UdpLanBroadcastTransportOptions);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
send(data: Buffer): Promise<void>;
|
|
19
|
+
onMessage(handler: (data: Buffer, meta: TransportMessageMeta) => void): () => void;
|
|
20
|
+
getStats(): TransportStats;
|
|
21
|
+
getConfig(): TransportConfigSnapshot;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UdpLanBroadcastTransport = void 0;
|
|
7
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
8
|
+
class UdpLanBroadcastTransport {
|
|
9
|
+
socket = null;
|
|
10
|
+
handlers = new Set();
|
|
11
|
+
state = "stopped";
|
|
12
|
+
stats = {
|
|
13
|
+
starts: 0,
|
|
14
|
+
stops: 0,
|
|
15
|
+
start_errors: 0,
|
|
16
|
+
stop_errors: 0,
|
|
17
|
+
sent_messages: 0,
|
|
18
|
+
sent_bytes: 0,
|
|
19
|
+
send_errors: 0,
|
|
20
|
+
received_messages: 0,
|
|
21
|
+
received_bytes: 0,
|
|
22
|
+
receive_errors: 0,
|
|
23
|
+
last_sent_at: 0,
|
|
24
|
+
last_received_at: 0,
|
|
25
|
+
last_error_at: 0,
|
|
26
|
+
};
|
|
27
|
+
port;
|
|
28
|
+
bindAddress;
|
|
29
|
+
broadcastAddress;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.port = options.port ?? 44123;
|
|
32
|
+
this.bindAddress = options.bindAddress ?? "0.0.0.0";
|
|
33
|
+
this.broadcastAddress = options.broadcastAddress ?? "255.255.255.255";
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.socket) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.state = "starting";
|
|
40
|
+
this.socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
41
|
+
this.socket.on("error", () => {
|
|
42
|
+
this.stats.receive_errors += 1;
|
|
43
|
+
this.stats.last_error_at = Date.now();
|
|
44
|
+
this.state = "error";
|
|
45
|
+
});
|
|
46
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
47
|
+
this.stats.received_messages += 1;
|
|
48
|
+
this.stats.received_bytes += msg.length;
|
|
49
|
+
this.stats.last_received_at = Date.now();
|
|
50
|
+
const meta = {
|
|
51
|
+
remote_address: rinfo.address,
|
|
52
|
+
remote_port: rinfo.port,
|
|
53
|
+
transport: "udp-lan-broadcast",
|
|
54
|
+
};
|
|
55
|
+
for (const handler of this.handlers) {
|
|
56
|
+
try {
|
|
57
|
+
handler(msg, meta);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
this.stats.receive_errors += 1;
|
|
61
|
+
this.stats.last_error_at = Date.now();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
if (!this.socket) {
|
|
67
|
+
this.state = "error";
|
|
68
|
+
reject(new Error("Transport socket unavailable"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.socket.once("error", reject);
|
|
72
|
+
this.socket.bind(this.port, this.bindAddress, () => {
|
|
73
|
+
if (!this.socket) {
|
|
74
|
+
this.state = "error";
|
|
75
|
+
reject(new Error("Transport socket unavailable after bind"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.socket.setBroadcast(true);
|
|
79
|
+
this.socket.off("error", reject);
|
|
80
|
+
this.stats.starts += 1;
|
|
81
|
+
this.state = "running";
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
}).catch((error) => {
|
|
85
|
+
this.stats.start_errors += 1;
|
|
86
|
+
this.stats.last_error_at = Date.now();
|
|
87
|
+
this.state = "error";
|
|
88
|
+
this.socket = null;
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async stop() {
|
|
93
|
+
if (!this.socket) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.state = "stopping";
|
|
97
|
+
const socket = this.socket;
|
|
98
|
+
this.socket = null;
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
socket.close(() => resolve());
|
|
101
|
+
}).then(() => {
|
|
102
|
+
this.stats.stops += 1;
|
|
103
|
+
this.state = "stopped";
|
|
104
|
+
}).catch((error) => {
|
|
105
|
+
this.stats.stop_errors += 1;
|
|
106
|
+
this.stats.last_error_at = Date.now();
|
|
107
|
+
this.state = "error";
|
|
108
|
+
throw error;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async send(data) {
|
|
112
|
+
if (!this.socket) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
if (!this.socket) {
|
|
117
|
+
resolve();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.socket.send(data, this.port, this.broadcastAddress, (error) => {
|
|
121
|
+
if (error) {
|
|
122
|
+
this.stats.send_errors += 1;
|
|
123
|
+
this.stats.last_error_at = Date.now();
|
|
124
|
+
reject(error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.stats.sent_messages += 1;
|
|
128
|
+
this.stats.sent_bytes += data.length;
|
|
129
|
+
this.stats.last_sent_at = Date.now();
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
onMessage(handler) {
|
|
135
|
+
this.handlers.add(handler);
|
|
136
|
+
return () => {
|
|
137
|
+
this.handlers.delete(handler);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
getStats() {
|
|
141
|
+
return { ...this.stats };
|
|
142
|
+
}
|
|
143
|
+
getConfig() {
|
|
144
|
+
return {
|
|
145
|
+
transport: "udp-lan-broadcast",
|
|
146
|
+
state: this.state,
|
|
147
|
+
bind_address: this.bindAddress,
|
|
148
|
+
broadcast_address: this.broadcastAddress,
|
|
149
|
+
port: this.port,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.UdpLanBroadcastTransport = UdpLanBroadcastTransport;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { NetworkAdapter } from "./types";
|
|
2
|
+
type WebRTCPreviewOptions = {
|
|
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
|
+
discoveryEventsLimit?: number;
|
|
16
|
+
};
|
|
17
|
+
type PeerStatus = "connecting" | "online" | "stale";
|
|
18
|
+
type WebRTCConnectionState = "new" | "connecting" | "connected" | "disconnected" | "failed" | "closed" | "unknown";
|
|
19
|
+
type WebRTCDataChannelState = "connecting" | "open" | "closing" | "closed" | "unknown";
|
|
20
|
+
type StateSummary<T extends string> = Record<T, number>;
|
|
21
|
+
type WebRTCDiagnostics = {
|
|
22
|
+
adapter: "webrtc-preview";
|
|
23
|
+
peer_id: string;
|
|
24
|
+
namespace: string;
|
|
25
|
+
room: string;
|
|
26
|
+
signaling_url: string;
|
|
27
|
+
signaling_endpoints: string[];
|
|
28
|
+
bootstrap_sources: string[];
|
|
29
|
+
seed_peers_count: number;
|
|
30
|
+
bootstrap_hints_count: number;
|
|
31
|
+
discovery_events_total: number;
|
|
32
|
+
last_discovery_event_at: number;
|
|
33
|
+
discovery_events: DiscoveryEvent[];
|
|
34
|
+
connection_states_summary: StateSummary<WebRTCConnectionState>;
|
|
35
|
+
datachannel_states_summary: StateSummary<WebRTCDataChannelState>;
|
|
36
|
+
signaling_messages_sent_total: number;
|
|
37
|
+
signaling_messages_received_total: number;
|
|
38
|
+
reconnect_attempts_total: number;
|
|
39
|
+
active_webrtc_peers: number;
|
|
40
|
+
components: {
|
|
41
|
+
transport: string;
|
|
42
|
+
discovery: string;
|
|
43
|
+
envelope_codec: string;
|
|
44
|
+
topic_codec: string;
|
|
45
|
+
};
|
|
46
|
+
limits: {
|
|
47
|
+
max_message_bytes: number;
|
|
48
|
+
max_future_drift_ms: number;
|
|
49
|
+
max_past_drift_ms: number;
|
|
50
|
+
};
|
|
51
|
+
config: {
|
|
52
|
+
started: boolean;
|
|
53
|
+
topic_handler_count: number;
|
|
54
|
+
poll_interval_ms: number;
|
|
55
|
+
};
|
|
56
|
+
peers: {
|
|
57
|
+
total: number;
|
|
58
|
+
online: number;
|
|
59
|
+
stale: number;
|
|
60
|
+
items: Array<{
|
|
61
|
+
peer_id: string;
|
|
62
|
+
status: PeerStatus;
|
|
63
|
+
first_seen_at: number;
|
|
64
|
+
last_seen_at: number;
|
|
65
|
+
messages_seen: number;
|
|
66
|
+
reconnect_attempts: number;
|
|
67
|
+
connection_state: WebRTCConnectionState;
|
|
68
|
+
datachannel_state: WebRTCDataChannelState;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
stats: {
|
|
72
|
+
publish_attempted: number;
|
|
73
|
+
publish_sent: number;
|
|
74
|
+
received_total: number;
|
|
75
|
+
delivered_total: number;
|
|
76
|
+
dropped_malformed: number;
|
|
77
|
+
dropped_oversized: number;
|
|
78
|
+
dropped_namespace_mismatch: number;
|
|
79
|
+
dropped_timestamp_future_drift: number;
|
|
80
|
+
dropped_timestamp_past_drift: number;
|
|
81
|
+
dropped_decode_failed: number;
|
|
82
|
+
dropped_self: number;
|
|
83
|
+
dropped_topic_decode_error: number;
|
|
84
|
+
dropped_handler_error: number;
|
|
85
|
+
signaling_errors: number;
|
|
86
|
+
invalid_signaling_payload_total: number;
|
|
87
|
+
duplicate_sdp_total: number;
|
|
88
|
+
duplicate_ice_total: number;
|
|
89
|
+
start_errors: number;
|
|
90
|
+
stop_errors: number;
|
|
91
|
+
received_validated: number;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
type DiscoveryEventType = "peer_joined" | "peer_stale" | "peer_removed" | "signaling_connected" | "signaling_disconnected" | "reconnect_started" | "reconnect_succeeded" | "reconnect_failed" | "malformed_signal_dropped" | "duplicate_signal_dropped";
|
|
95
|
+
type DiscoveryEvent = {
|
|
96
|
+
id: string;
|
|
97
|
+
type: DiscoveryEventType;
|
|
98
|
+
at: number;
|
|
99
|
+
peer_id?: string;
|
|
100
|
+
endpoint?: string;
|
|
101
|
+
detail?: string;
|
|
102
|
+
};
|
|
103
|
+
export declare class WebRTCPreviewAdapter implements NetworkAdapter {
|
|
104
|
+
private readonly peerId;
|
|
105
|
+
private readonly namespace;
|
|
106
|
+
private readonly signalingUrl;
|
|
107
|
+
private readonly signalingEndpoints;
|
|
108
|
+
private readonly room;
|
|
109
|
+
private readonly seedPeers;
|
|
110
|
+
private readonly bootstrapHints;
|
|
111
|
+
private readonly bootstrapSources;
|
|
112
|
+
private readonly maxMessageBytes;
|
|
113
|
+
private readonly pollIntervalMs;
|
|
114
|
+
private readonly maxFutureDriftMs;
|
|
115
|
+
private readonly maxPastDriftMs;
|
|
116
|
+
private readonly discoveryEventsLimit;
|
|
117
|
+
private readonly envelopeCodec;
|
|
118
|
+
private readonly topicCodec;
|
|
119
|
+
private readonly handlers;
|
|
120
|
+
private readonly sessions;
|
|
121
|
+
private started;
|
|
122
|
+
private poller;
|
|
123
|
+
private wrtc;
|
|
124
|
+
private processedSignalIds;
|
|
125
|
+
private signalingConnectivity;
|
|
126
|
+
private signalingIndex;
|
|
127
|
+
private activeSignalingEndpoint;
|
|
128
|
+
private discoveryEvents;
|
|
129
|
+
private discoveryEventsTotal;
|
|
130
|
+
private lastDiscoveryEventAt;
|
|
131
|
+
private signalingMessagesSentTotal;
|
|
132
|
+
private signalingMessagesReceivedTotal;
|
|
133
|
+
private reconnectAttemptsTotal;
|
|
134
|
+
private stats;
|
|
135
|
+
constructor(options?: WebRTCPreviewOptions);
|
|
136
|
+
start(): Promise<void>;
|
|
137
|
+
stop(): Promise<void>;
|
|
138
|
+
publish(topic: string, data: any): Promise<void>;
|
|
139
|
+
subscribe(topic: string, handler: (data: any) => void): void;
|
|
140
|
+
getDiagnostics(): WebRTCDiagnostics;
|
|
141
|
+
private resolveWebRTCImplementation;
|
|
142
|
+
private pollOnce;
|
|
143
|
+
private syncPeersFromSignaling;
|
|
144
|
+
private ensureSession;
|
|
145
|
+
private shouldAttemptConnect;
|
|
146
|
+
private attemptReconnect;
|
|
147
|
+
private createPeerConnection;
|
|
148
|
+
private bindDataChannel;
|
|
149
|
+
private handleSignalMessage;
|
|
150
|
+
private flushBufferedIce;
|
|
151
|
+
private onDataMessage;
|
|
152
|
+
private cleanupProcessedSignalIds;
|
|
153
|
+
private isInitiatorFor;
|
|
154
|
+
private closePeerSession;
|
|
155
|
+
private sendSignal;
|
|
156
|
+
private postJson;
|
|
157
|
+
private getJson;
|
|
158
|
+
private requestJson;
|
|
159
|
+
private markSignalingConnected;
|
|
160
|
+
private markSignalingDisconnected;
|
|
161
|
+
private recordDiscoveryEvent;
|
|
162
|
+
}
|
|
163
|
+
export {};
|