@silicaclaw/cli 1.0.0-beta.0

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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +137 -0
  2. package/CHANGELOG.md +411 -0
  3. package/DEMO_GUIDE.md +89 -0
  4. package/INSTALL.md +156 -0
  5. package/README.md +244 -0
  6. package/RELEASE_NOTES_v1.0.md +65 -0
  7. package/ROADMAP.md +48 -0
  8. package/SOCIAL_MD_SPEC.md +122 -0
  9. package/VERSION +1 -0
  10. package/apps/local-console/package.json +23 -0
  11. package/apps/local-console/public/assets/README.md +5 -0
  12. package/apps/local-console/public/assets/silicaclaw-logo.png +0 -0
  13. package/apps/local-console/public/index.html +1602 -0
  14. package/apps/local-console/src/server.ts +1656 -0
  15. package/apps/local-console/src/socialRoutes.ts +90 -0
  16. package/apps/local-console/tsconfig.json +7 -0
  17. package/apps/public-explorer/package.json +20 -0
  18. package/apps/public-explorer/public/assets/README.md +5 -0
  19. package/apps/public-explorer/public/assets/silicaclaw-logo.png +0 -0
  20. package/apps/public-explorer/public/index.html +483 -0
  21. package/apps/public-explorer/src/server.ts +32 -0
  22. package/apps/public-explorer/tsconfig.json +7 -0
  23. package/docs/QUICK_START.md +48 -0
  24. package/docs/assets/README.md +8 -0
  25. package/docs/assets/banner.svg +25 -0
  26. package/docs/assets/silicaclaw-logo.png +0 -0
  27. package/docs/assets/silicaclaw-og.png +0 -0
  28. package/docs/release/GITHUB_RELEASE_v1.0-beta.md +143 -0
  29. package/docs/screenshots/README.md +8 -0
  30. package/docs/screenshots/v0.3.1-explorer-search.svg +9 -0
  31. package/docs/screenshots/v0.3.1-machine-a-network.svg +9 -0
  32. package/docs/screenshots/v0.3.1-machine-b-peers.svg +9 -0
  33. package/docs/screenshots/v0.3.1-stale-transition.svg +9 -0
  34. package/openclaw.social.md.example +28 -0
  35. package/package.json +64 -0
  36. package/packages/core/package.json +13 -0
  37. package/packages/core/src/crypto.ts +55 -0
  38. package/packages/core/src/directory.ts +171 -0
  39. package/packages/core/src/identity.ts +14 -0
  40. package/packages/core/src/index.ts +11 -0
  41. package/packages/core/src/indexing.ts +42 -0
  42. package/packages/core/src/presence.ts +24 -0
  43. package/packages/core/src/profile.ts +39 -0
  44. package/packages/core/src/publicProfileSummary.ts +180 -0
  45. package/packages/core/src/socialConfig.ts +440 -0
  46. package/packages/core/src/socialResolver.ts +281 -0
  47. package/packages/core/src/socialTemplate.ts +97 -0
  48. package/packages/core/src/types.ts +43 -0
  49. package/packages/core/tsconfig.json +7 -0
  50. package/packages/network/package.json +10 -0
  51. package/packages/network/src/abstractions/messageEnvelope.ts +80 -0
  52. package/packages/network/src/abstractions/peerDiscovery.ts +49 -0
  53. package/packages/network/src/abstractions/topicCodec.ts +4 -0
  54. package/packages/network/src/abstractions/transport.ts +40 -0
  55. package/packages/network/src/codec/jsonMessageEnvelopeCodec.ts +22 -0
  56. package/packages/network/src/codec/jsonTopicCodec.ts +11 -0
  57. package/packages/network/src/discovery/heartbeatPeerDiscovery.ts +173 -0
  58. package/packages/network/src/index.ts +16 -0
  59. package/packages/network/src/localEventBus.ts +61 -0
  60. package/packages/network/src/mock.ts +27 -0
  61. package/packages/network/src/realPreview.ts +436 -0
  62. package/packages/network/src/transport/udpLanBroadcastTransport.ts +173 -0
  63. package/packages/network/src/types.ts +6 -0
  64. package/packages/network/src/webrtcPreview.ts +1052 -0
  65. package/packages/network/tsconfig.json +7 -0
  66. package/packages/storage/package.json +13 -0
  67. package/packages/storage/src/index.ts +3 -0
  68. package/packages/storage/src/jsonRepo.ts +25 -0
  69. package/packages/storage/src/repos.ts +46 -0
  70. package/packages/storage/src/socialRuntimeRepo.ts +51 -0
  71. package/packages/storage/tsconfig.json +7 -0
  72. package/scripts/functional-check.mjs +165 -0
  73. package/scripts/install-logo.sh +53 -0
  74. package/scripts/quickstart.sh +144 -0
  75. package/scripts/silicaclaw-cli.mjs +88 -0
  76. package/scripts/webrtc-signaling-server.mjs +249 -0
  77. package/social.md.example +30 -0
@@ -0,0 +1,436 @@
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 { NetworkTransport } from "./abstractions/transport";
10
+ import { PeerDiscovery, PeerSnapshot } from "./abstractions/peerDiscovery";
11
+ import { JsonMessageEnvelopeCodec } from "./codec/jsonMessageEnvelopeCodec";
12
+ import { JsonTopicCodec } from "./codec/jsonTopicCodec";
13
+ import { UdpLanBroadcastTransport } from "./transport/udpLanBroadcastTransport";
14
+ import { HeartbeatPeerDiscovery } from "./discovery/heartbeatPeerDiscovery";
15
+
16
+ type RealNetworkAdapterPreviewOptions = {
17
+ peerId?: string;
18
+ namespace?: string;
19
+ transport?: NetworkTransport;
20
+ envelopeCodec?: MessageEnvelopeCodec;
21
+ topicCodec?: TopicCodec;
22
+ peerDiscovery?: PeerDiscovery;
23
+ maxMessageBytes?: number;
24
+ dedupeWindowMs?: number;
25
+ dedupeMaxEntries?: number;
26
+ maxFutureDriftMs?: number;
27
+ maxPastDriftMs?: number;
28
+ };
29
+
30
+ type NetworkDiagnostics = {
31
+ adapter: string;
32
+ peer_id: string;
33
+ namespace: string;
34
+ components: {
35
+ transport: string;
36
+ discovery: string;
37
+ envelope_codec: string;
38
+ topic_codec: string;
39
+ };
40
+ limits: {
41
+ max_message_bytes: number;
42
+ dedupe_window_ms: number;
43
+ dedupe_max_entries: number;
44
+ max_future_drift_ms: number;
45
+ max_past_drift_ms: number;
46
+ };
47
+ config: {
48
+ started: boolean;
49
+ topic_handler_count: number;
50
+ transport: ReturnType<NonNullable<NetworkTransport["getConfig"]>> | null;
51
+ discovery: ReturnType<NonNullable<PeerDiscovery["getConfig"]>> | null;
52
+ };
53
+ peers: {
54
+ total: number;
55
+ online: number;
56
+ stale: number;
57
+ items: PeerSnapshot[];
58
+ };
59
+ stats: {
60
+ publish_attempted: number;
61
+ publish_sent: number;
62
+ received_total: number;
63
+ delivered_total: number;
64
+ dropped_duplicate: number;
65
+ dropped_self: number;
66
+ dropped_malformed: number;
67
+ dropped_oversized: number;
68
+ dropped_namespace_mismatch: number;
69
+ dropped_timestamp_future_drift: number;
70
+ dropped_timestamp_past_drift: number;
71
+ dropped_decode_failed: number;
72
+ dropped_topic_decode_error: number;
73
+ dropped_handler_error: number;
74
+ send_errors: number;
75
+ discovery_errors: number;
76
+ start_errors: number;
77
+ stop_errors: number;
78
+ received_validated: number;
79
+ };
80
+ transport_stats: ReturnType<NonNullable<NetworkTransport["getStats"]>> | null;
81
+ discovery_stats: ReturnType<NonNullable<PeerDiscovery["getStats"]>> | null;
82
+ };
83
+
84
+ export class RealNetworkAdapterPreview implements NetworkAdapter {
85
+ private started = false;
86
+ private peerId: string;
87
+ private namespace: string;
88
+ private transport: NetworkTransport;
89
+ private envelopeCodec: MessageEnvelopeCodec;
90
+ private topicCodec: TopicCodec;
91
+ private peerDiscovery: PeerDiscovery;
92
+
93
+ private maxMessageBytes: number;
94
+ private dedupeWindowMs: number;
95
+ private dedupeMaxEntries: number;
96
+ private maxFutureDriftMs: number;
97
+ private maxPastDriftMs: number;
98
+ private seenMessageIds = new Map<string, number>();
99
+
100
+ private offTransportMessage: (() => void) | null = null;
101
+ private handlers = new Map<string, Set<(data: any) => void>>();
102
+
103
+ private stats: NetworkDiagnostics["stats"] = {
104
+ publish_attempted: 0,
105
+ publish_sent: 0,
106
+ received_total: 0,
107
+ delivered_total: 0,
108
+ dropped_duplicate: 0,
109
+ dropped_self: 0,
110
+ dropped_malformed: 0,
111
+ dropped_oversized: 0,
112
+ dropped_namespace_mismatch: 0,
113
+ dropped_timestamp_future_drift: 0,
114
+ dropped_timestamp_past_drift: 0,
115
+ dropped_decode_failed: 0,
116
+ dropped_topic_decode_error: 0,
117
+ dropped_handler_error: 0,
118
+ send_errors: 0,
119
+ discovery_errors: 0,
120
+ start_errors: 0,
121
+ stop_errors: 0,
122
+ received_validated: 0,
123
+ };
124
+
125
+ constructor(options: RealNetworkAdapterPreviewOptions = {}) {
126
+ this.peerId = options.peerId ?? `peer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
127
+ this.namespace = this.normalizeNamespace(options.namespace ?? "silicaclaw.preview");
128
+ this.transport = options.transport ?? new UdpLanBroadcastTransport();
129
+ this.envelopeCodec = options.envelopeCodec ?? new JsonMessageEnvelopeCodec();
130
+ this.topicCodec = options.topicCodec ?? new JsonTopicCodec();
131
+ this.peerDiscovery = options.peerDiscovery ?? new HeartbeatPeerDiscovery();
132
+
133
+ this.maxMessageBytes = options.maxMessageBytes ?? 64 * 1024;
134
+ this.dedupeWindowMs = options.dedupeWindowMs ?? 90_000;
135
+ this.dedupeMaxEntries = options.dedupeMaxEntries ?? 10_000;
136
+ this.maxFutureDriftMs = options.maxFutureDriftMs ?? 30_000;
137
+ this.maxPastDriftMs = options.maxPastDriftMs ?? 120_000;
138
+ }
139
+
140
+ async start(): Promise<void> {
141
+ if (this.started) {
142
+ return;
143
+ }
144
+
145
+ try {
146
+ await this.transport.start();
147
+ } catch (error) {
148
+ this.stats.start_errors += 1;
149
+ throw new Error(`Transport start failed: ${this.errorMessage(error)}`);
150
+ }
151
+
152
+ this.started = true;
153
+ this.offTransportMessage = this.transport.onMessage((raw) => {
154
+ this.onTransportMessage(raw);
155
+ });
156
+
157
+ try {
158
+ await this.peerDiscovery.start({
159
+ self_peer_id: this.peerId,
160
+ publishControl: async (topic, payload) => {
161
+ await this.publish(topic, payload);
162
+ },
163
+ });
164
+ } catch (error) {
165
+ this.stats.start_errors += 1;
166
+ this.started = false;
167
+ if (this.offTransportMessage) {
168
+ this.offTransportMessage();
169
+ this.offTransportMessage = null;
170
+ }
171
+ try {
172
+ await this.transport.stop();
173
+ } catch {
174
+ this.stats.stop_errors += 1;
175
+ }
176
+ throw new Error(`Peer discovery start failed: ${this.errorMessage(error)}`);
177
+ }
178
+ }
179
+
180
+ async stop(): Promise<void> {
181
+ if (!this.started) {
182
+ return;
183
+ }
184
+
185
+ try {
186
+ await this.peerDiscovery.stop();
187
+ } catch {
188
+ this.stats.discovery_errors += 1;
189
+ this.stats.stop_errors += 1;
190
+ }
191
+
192
+ if (this.offTransportMessage) {
193
+ this.offTransportMessage();
194
+ this.offTransportMessage = null;
195
+ }
196
+
197
+ try {
198
+ await this.transport.stop();
199
+ } catch {
200
+ this.stats.stop_errors += 1;
201
+ }
202
+
203
+ this.started = false;
204
+ }
205
+
206
+ async publish(topic: string, data: any): Promise<void> {
207
+ if (!this.started) {
208
+ return;
209
+ }
210
+
211
+ this.stats.publish_attempted += 1;
212
+
213
+ if (!this.isValidTopic(topic)) {
214
+ this.stats.dropped_malformed += 1;
215
+ return;
216
+ }
217
+
218
+ const envelope: NetworkMessageEnvelope = {
219
+ version: 1,
220
+ message_id: randomUUID(),
221
+ topic: this.topicKey(topic),
222
+ source_peer_id: this.peerId,
223
+ timestamp: Date.now(),
224
+ payload: this.topicCodec.encode(topic, data),
225
+ };
226
+
227
+ const raw = this.envelopeCodec.encode(envelope);
228
+ if (raw.length > this.maxMessageBytes) {
229
+ this.stats.dropped_oversized += 1;
230
+ return;
231
+ }
232
+
233
+ try {
234
+ await this.transport.send(raw);
235
+ this.stats.publish_sent += 1;
236
+ } catch {
237
+ this.stats.send_errors += 1;
238
+ throw new Error("Transport send failed");
239
+ }
240
+ }
241
+
242
+ subscribe(topic: string, handler: (data: any) => void): void {
243
+ if (!this.isValidTopic(topic)) {
244
+ return;
245
+ }
246
+
247
+ const key = this.topicKey(topic);
248
+ if (!this.handlers.has(key)) {
249
+ this.handlers.set(key, new Set());
250
+ }
251
+ this.handlers.get(key)?.add(handler);
252
+ }
253
+
254
+ listPeers(): PeerSnapshot[] {
255
+ return this.peerDiscovery.listPeers();
256
+ }
257
+
258
+ getDiagnostics(): NetworkDiagnostics {
259
+ const peers = this.listPeers();
260
+ const online = peers.filter((peer) => peer.status === "online").length;
261
+
262
+ return {
263
+ adapter: "real-preview",
264
+ peer_id: this.peerId,
265
+ namespace: this.namespace,
266
+ components: {
267
+ transport: this.transport.constructor.name,
268
+ discovery: this.peerDiscovery.constructor.name,
269
+ envelope_codec: this.envelopeCodec.constructor.name,
270
+ topic_codec: this.topicCodec.constructor.name,
271
+ },
272
+ limits: {
273
+ max_message_bytes: this.maxMessageBytes,
274
+ dedupe_window_ms: this.dedupeWindowMs,
275
+ dedupe_max_entries: this.dedupeMaxEntries,
276
+ max_future_drift_ms: this.maxFutureDriftMs,
277
+ max_past_drift_ms: this.maxPastDriftMs,
278
+ },
279
+ config: {
280
+ started: this.started,
281
+ topic_handler_count: this.handlers.size,
282
+ transport: this.transport.getConfig?.() ?? null,
283
+ discovery: this.peerDiscovery.getConfig?.() ?? null,
284
+ },
285
+ peers: {
286
+ total: peers.length,
287
+ online,
288
+ stale: Math.max(0, peers.length - online),
289
+ items: peers,
290
+ },
291
+ stats: { ...this.stats },
292
+ transport_stats: this.transport.getStats?.() ?? null,
293
+ discovery_stats: this.peerDiscovery.getStats?.() ?? null,
294
+ };
295
+ }
296
+
297
+ private onTransportMessage(raw: Buffer): void {
298
+ this.stats.received_total += 1;
299
+
300
+ if (raw.length > this.maxMessageBytes) {
301
+ this.stats.dropped_oversized += 1;
302
+ return;
303
+ }
304
+
305
+ const decoded = this.envelopeCodec.decode(raw);
306
+ if (!decoded) {
307
+ this.stats.dropped_decode_failed += 1;
308
+ this.stats.dropped_malformed += 1;
309
+ return;
310
+ }
311
+
312
+ const validated = validateNetworkMessageEnvelope(decoded.envelope, {
313
+ max_future_drift_ms: this.maxFutureDriftMs,
314
+ max_past_drift_ms: this.maxPastDriftMs,
315
+ });
316
+ if (!validated.ok || !validated.envelope) {
317
+ if (validated.reason === "timestamp_future_drift") {
318
+ this.stats.dropped_timestamp_future_drift += 1;
319
+ } else if (validated.reason === "timestamp_past_drift") {
320
+ this.stats.dropped_timestamp_past_drift += 1;
321
+ } else {
322
+ this.stats.dropped_malformed += 1;
323
+ }
324
+ return;
325
+ }
326
+ this.stats.received_validated += 1;
327
+
328
+ const envelope = validated.envelope;
329
+
330
+ if (!envelope.topic.startsWith(`${this.namespace}:`)) {
331
+ this.stats.dropped_namespace_mismatch += 1;
332
+ return;
333
+ }
334
+
335
+ if (this.isDuplicateMessage(envelope.message_id, envelope.timestamp)) {
336
+ this.stats.dropped_duplicate += 1;
337
+ return;
338
+ }
339
+
340
+ try {
341
+ this.peerDiscovery.observeEnvelope(envelope);
342
+ } catch {
343
+ this.stats.discovery_errors += 1;
344
+ }
345
+
346
+ if (envelope.source_peer_id === this.peerId) {
347
+ this.stats.dropped_self += 1;
348
+ return;
349
+ }
350
+
351
+ const topic = this.stripNamespace(envelope.topic);
352
+ if (!topic) {
353
+ this.stats.dropped_namespace_mismatch += 1;
354
+ return;
355
+ }
356
+
357
+ const handlers = this.handlers.get(envelope.topic);
358
+ if (!handlers || handlers.size === 0) {
359
+ return;
360
+ }
361
+
362
+ try {
363
+ const payload = this.topicCodec.decode(topic, envelope.payload);
364
+ for (const handler of handlers) {
365
+ try {
366
+ handler(payload);
367
+ this.stats.delivered_total += 1;
368
+ } catch {
369
+ this.stats.dropped_handler_error += 1;
370
+ }
371
+ }
372
+ } catch {
373
+ this.stats.dropped_topic_decode_error += 1;
374
+ }
375
+ }
376
+
377
+ private topicKey(topic: string): string {
378
+ return `${this.namespace}:${topic}`;
379
+ }
380
+
381
+ private stripNamespace(topicKey: string): string | null {
382
+ const prefix = `${this.namespace}:`;
383
+ if (!topicKey.startsWith(prefix)) {
384
+ return null;
385
+ }
386
+ return topicKey.slice(prefix.length);
387
+ }
388
+
389
+ private isDuplicateMessage(messageId: string, timestamp: number): boolean {
390
+ const now = Date.now();
391
+ this.cleanupSeenMessageIds(now);
392
+
393
+ const existing = this.seenMessageIds.get(messageId);
394
+ if (existing && now - existing <= this.dedupeWindowMs) {
395
+ return true;
396
+ }
397
+
398
+ this.seenMessageIds.set(messageId, Number.isFinite(timestamp) ? timestamp : now);
399
+ if (this.seenMessageIds.size > this.dedupeMaxEntries) {
400
+ const oldestKey = this.seenMessageIds.keys().next().value;
401
+ if (oldestKey) {
402
+ this.seenMessageIds.delete(oldestKey);
403
+ }
404
+ }
405
+
406
+ return false;
407
+ }
408
+
409
+ private cleanupSeenMessageIds(now: number): void {
410
+ for (const [id, ts] of this.seenMessageIds.entries()) {
411
+ if (now - ts > this.dedupeWindowMs) {
412
+ this.seenMessageIds.delete(id);
413
+ }
414
+ }
415
+ }
416
+
417
+ private isValidTopic(topic: string): boolean {
418
+ if (typeof topic !== "string") {
419
+ return false;
420
+ }
421
+ const normalized = topic.trim();
422
+ return normalized.length > 0 && !normalized.includes(":");
423
+ }
424
+
425
+ private normalizeNamespace(namespace: string): string {
426
+ const normalized = namespace.trim();
427
+ return normalized.length > 0 ? normalized : "silicaclaw.preview";
428
+ }
429
+
430
+ private errorMessage(error: unknown): string {
431
+ if (error instanceof Error) {
432
+ return error.message;
433
+ }
434
+ return String(error);
435
+ }
436
+ }
@@ -0,0 +1,173 @@
1
+ import dgram from "dgram";
2
+ import {
3
+ NetworkTransport,
4
+ TransportConfigSnapshot,
5
+ TransportLifecycleState,
6
+ TransportMessageMeta,
7
+ TransportStats,
8
+ } from "../abstractions/transport";
9
+
10
+ type UdpLanBroadcastTransportOptions = {
11
+ port?: number;
12
+ bindAddress?: string;
13
+ broadcastAddress?: string;
14
+ };
15
+
16
+ export class UdpLanBroadcastTransport implements NetworkTransport {
17
+ private socket: dgram.Socket | null = null;
18
+ private handlers = new Set<(data: Buffer, meta: TransportMessageMeta) => void>();
19
+ private state: TransportLifecycleState = "stopped";
20
+ private stats: TransportStats = {
21
+ starts: 0,
22
+ stops: 0,
23
+ start_errors: 0,
24
+ stop_errors: 0,
25
+ sent_messages: 0,
26
+ sent_bytes: 0,
27
+ send_errors: 0,
28
+ received_messages: 0,
29
+ received_bytes: 0,
30
+ receive_errors: 0,
31
+ last_sent_at: 0,
32
+ last_received_at: 0,
33
+ last_error_at: 0,
34
+ };
35
+
36
+ private port: number;
37
+ private bindAddress: string;
38
+ private broadcastAddress: string;
39
+
40
+ constructor(options: UdpLanBroadcastTransportOptions = {}) {
41
+ this.port = options.port ?? 44123;
42
+ this.bindAddress = options.bindAddress ?? "0.0.0.0";
43
+ this.broadcastAddress = options.broadcastAddress ?? "255.255.255.255";
44
+ }
45
+
46
+ async start(): Promise<void> {
47
+ if (this.socket) {
48
+ return;
49
+ }
50
+ this.state = "starting";
51
+
52
+ this.socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
53
+ this.socket.on("error", () => {
54
+ this.stats.receive_errors += 1;
55
+ this.stats.last_error_at = Date.now();
56
+ this.state = "error";
57
+ });
58
+ this.socket.on("message", (msg, rinfo) => {
59
+ this.stats.received_messages += 1;
60
+ this.stats.received_bytes += msg.length;
61
+ this.stats.last_received_at = Date.now();
62
+ const meta: TransportMessageMeta = {
63
+ remote_address: rinfo.address,
64
+ remote_port: rinfo.port,
65
+ transport: "udp-lan-broadcast",
66
+ };
67
+ for (const handler of this.handlers) {
68
+ try {
69
+ handler(msg, meta);
70
+ } catch {
71
+ this.stats.receive_errors += 1;
72
+ this.stats.last_error_at = Date.now();
73
+ }
74
+ }
75
+ });
76
+
77
+ await new Promise<void>((resolve, reject) => {
78
+ if (!this.socket) {
79
+ this.state = "error";
80
+ reject(new Error("Transport socket unavailable"));
81
+ return;
82
+ }
83
+
84
+ this.socket.once("error", reject);
85
+ this.socket.bind(this.port, this.bindAddress, () => {
86
+ if (!this.socket) {
87
+ this.state = "error";
88
+ reject(new Error("Transport socket unavailable after bind"));
89
+ return;
90
+ }
91
+ this.socket.setBroadcast(true);
92
+ this.socket.off("error", reject);
93
+ this.stats.starts += 1;
94
+ this.state = "running";
95
+ resolve();
96
+ });
97
+ }).catch((error) => {
98
+ this.stats.start_errors += 1;
99
+ this.stats.last_error_at = Date.now();
100
+ this.state = "error";
101
+ this.socket = null;
102
+ throw error;
103
+ });
104
+ }
105
+
106
+ async stop(): Promise<void> {
107
+ if (!this.socket) {
108
+ return;
109
+ }
110
+ this.state = "stopping";
111
+
112
+ const socket = this.socket;
113
+ this.socket = null;
114
+
115
+ await new Promise<void>((resolve) => {
116
+ socket.close(() => resolve());
117
+ }).then(() => {
118
+ this.stats.stops += 1;
119
+ this.state = "stopped";
120
+ }).catch((error) => {
121
+ this.stats.stop_errors += 1;
122
+ this.stats.last_error_at = Date.now();
123
+ this.state = "error";
124
+ throw error;
125
+ });
126
+ }
127
+
128
+ async send(data: Buffer): Promise<void> {
129
+ if (!this.socket) {
130
+ return;
131
+ }
132
+
133
+ await new Promise<void>((resolve, reject) => {
134
+ if (!this.socket) {
135
+ resolve();
136
+ return;
137
+ }
138
+ this.socket.send(data, this.port, this.broadcastAddress, (error) => {
139
+ if (error) {
140
+ this.stats.send_errors += 1;
141
+ this.stats.last_error_at = Date.now();
142
+ reject(error);
143
+ return;
144
+ }
145
+ this.stats.sent_messages += 1;
146
+ this.stats.sent_bytes += data.length;
147
+ this.stats.last_sent_at = Date.now();
148
+ resolve();
149
+ });
150
+ });
151
+ }
152
+
153
+ onMessage(handler: (data: Buffer, meta: TransportMessageMeta) => void): () => void {
154
+ this.handlers.add(handler);
155
+ return () => {
156
+ this.handlers.delete(handler);
157
+ };
158
+ }
159
+
160
+ getStats(): TransportStats {
161
+ return { ...this.stats };
162
+ }
163
+
164
+ getConfig(): TransportConfigSnapshot {
165
+ return {
166
+ transport: "udp-lan-broadcast",
167
+ state: this.state,
168
+ bind_address: this.bindAddress,
169
+ broadcast_address: this.broadcastAddress,
170
+ port: this.port,
171
+ };
172
+ }
173
+ }
@@ -0,0 +1,6 @@
1
+ export interface NetworkAdapter {
2
+ start(): Promise<void>;
3
+ stop(): Promise<void>;
4
+ publish(topic: string, data: any): Promise<void>;
5
+ subscribe(topic: string, handler: (data: any) => void): void;
6
+ }