@silicaclaw/cli 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/INSTALL.md +36 -0
- package/README.md +40 -0
- package/apps/local-console/public/index.html +81 -63
- package/apps/local-console/src/server.ts +41 -21
- package/docs/CLOUDFLARE_RELAY.md +61 -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 +7 -6
- 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 +133 -0
- package/packages/network/dist/relayPreview.js +320 -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 +425 -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 +3 -3
- package/packages/storage/tsconfig.json +6 -1
- package/scripts/quickstart.sh +286 -20
- package/scripts/silicaclaw-cli.mjs +271 -1
- package/scripts/silicaclaw-gateway.mjs +411 -0
- package/scripts/webrtc-signaling-server.mjs +52 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AgentIdentity = {
|
|
2
|
+
agent_id: string;
|
|
3
|
+
public_key: string;
|
|
4
|
+
private_key: string;
|
|
5
|
+
created_at: number;
|
|
6
|
+
};
|
|
7
|
+
export type PublicProfile = {
|
|
8
|
+
agent_id: string;
|
|
9
|
+
display_name: string;
|
|
10
|
+
bio: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
avatar_url?: string;
|
|
13
|
+
public_enabled: boolean;
|
|
14
|
+
updated_at: number;
|
|
15
|
+
signature: string;
|
|
16
|
+
};
|
|
17
|
+
export type SignedProfileRecord = {
|
|
18
|
+
type: "profile";
|
|
19
|
+
profile: PublicProfile;
|
|
20
|
+
};
|
|
21
|
+
export type PresenceRecord = {
|
|
22
|
+
type: "presence";
|
|
23
|
+
agent_id: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
signature: string;
|
|
26
|
+
};
|
|
27
|
+
export type IndexRefRecord = {
|
|
28
|
+
type: "index";
|
|
29
|
+
key: string;
|
|
30
|
+
agent_id: string;
|
|
31
|
+
};
|
|
32
|
+
export type DirectoryState = {
|
|
33
|
+
profiles: Record<string, PublicProfile>;
|
|
34
|
+
presence: Record<string, number>;
|
|
35
|
+
index: Record<string, string[]>;
|
|
36
|
+
};
|
|
37
|
+
export type ProfileInput = Omit<PublicProfile, "signature" | "updated_at">;
|
|
@@ -5,7 +5,7 @@ export type SocialIdentityConfig = {
|
|
|
5
5
|
tags: string[];
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export type SocialNetworkAdapter = "mock" | "local-event-bus" | "real-preview" | "webrtc-preview";
|
|
8
|
+
export type SocialNetworkAdapter = "mock" | "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview";
|
|
9
9
|
export type SocialNetworkMode = "local" | "lan" | "global-preview";
|
|
10
10
|
|
|
11
11
|
export type SocialNetworkConfig = {
|
|
@@ -104,13 +104,13 @@ const DEFAULT_SOCIAL_CONFIG: SocialConfig = {
|
|
|
104
104
|
tags: [],
|
|
105
105
|
},
|
|
106
106
|
network: {
|
|
107
|
-
mode: "
|
|
107
|
+
mode: "global-preview",
|
|
108
108
|
namespace: "silicaclaw.preview",
|
|
109
|
-
adapter: "
|
|
109
|
+
adapter: "relay-preview",
|
|
110
110
|
port: 44123,
|
|
111
111
|
signaling_url: "http://localhost:4510",
|
|
112
112
|
signaling_urls: [],
|
|
113
|
-
room: "silicaclaw-
|
|
113
|
+
room: "silicaclaw-global-preview",
|
|
114
114
|
seed_peers: [],
|
|
115
115
|
bootstrap_hints: [],
|
|
116
116
|
},
|
|
@@ -298,7 +298,8 @@ function asAdapter(value: unknown, fallback: SocialNetworkAdapter): SocialNetwor
|
|
|
298
298
|
value === "mock" ||
|
|
299
299
|
value === "local-event-bus" ||
|
|
300
300
|
value === "real-preview" ||
|
|
301
|
-
value === "webrtc-preview"
|
|
301
|
+
value === "webrtc-preview" ||
|
|
302
|
+
value === "relay-preview"
|
|
302
303
|
) {
|
|
303
304
|
return value;
|
|
304
305
|
}
|
|
@@ -315,7 +316,7 @@ function asMode(value: unknown, fallback: SocialNetworkMode): SocialNetworkMode
|
|
|
315
316
|
function adapterForMode(mode: SocialNetworkMode): SocialNetworkAdapter {
|
|
316
317
|
if (mode === "local") return "local-event-bus";
|
|
317
318
|
if (mode === "lan") return "real-preview";
|
|
318
|
-
return "
|
|
319
|
+
return "relay-preview";
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
export function normalizeSocialConfig(input: unknown): SocialConfig {
|
|
@@ -257,13 +257,25 @@ export function resolveProfileInputWithSocial(args: {
|
|
|
257
257
|
const baseAvatarUrl = existingProfile?.avatar_url || "";
|
|
258
258
|
const baseTags = existingProfile?.tags || [];
|
|
259
259
|
|
|
260
|
+
// Preserve values saved from local-console first; only fall back to social/openclaw defaults
|
|
261
|
+
// when local profile fields are empty.
|
|
262
|
+
const displayName = baseDisplayName || socialConfig.identity.display_name || openclawProfile?.display_name || "";
|
|
263
|
+
const bio = baseBio || socialConfig.identity.bio || openclawProfile?.bio || "";
|
|
264
|
+
const avatarUrl = baseAvatarUrl || socialConfig.identity.avatar_url || openclawProfile?.avatar_url || "";
|
|
265
|
+
const tags =
|
|
266
|
+
baseTags.length > 0
|
|
267
|
+
? baseTags
|
|
268
|
+
: socialConfig.identity.tags.length > 0
|
|
269
|
+
? socialConfig.identity.tags
|
|
270
|
+
: openclawProfile?.tags || [];
|
|
271
|
+
|
|
260
272
|
return {
|
|
261
273
|
agent_id: agentId,
|
|
262
|
-
display_name:
|
|
263
|
-
bio
|
|
264
|
-
avatar_url:
|
|
265
|
-
tags
|
|
266
|
-
public_enabled: socialConfig.public_enabled,
|
|
274
|
+
display_name: displayName,
|
|
275
|
+
bio,
|
|
276
|
+
avatar_url: avatarUrl,
|
|
277
|
+
tags,
|
|
278
|
+
public_enabled: existingProfile?.public_enabled ?? socialConfig.public_enabled,
|
|
267
279
|
};
|
|
268
280
|
}
|
|
269
281
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type NetworkMessageEnvelope<TPayload = unknown> = {
|
|
2
|
+
version: 1;
|
|
3
|
+
message_id: string;
|
|
4
|
+
topic: string;
|
|
5
|
+
source_peer_id: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
payload: TPayload;
|
|
8
|
+
};
|
|
9
|
+
export type DecodedNetworkMessage = {
|
|
10
|
+
envelope: NetworkMessageEnvelope;
|
|
11
|
+
raw: Buffer;
|
|
12
|
+
};
|
|
13
|
+
export interface MessageEnvelopeCodec {
|
|
14
|
+
encode(envelope: NetworkMessageEnvelope): Buffer;
|
|
15
|
+
decode(raw: Buffer): DecodedNetworkMessage | null;
|
|
16
|
+
}
|
|
17
|
+
export type EnvelopeValidationOptions = {
|
|
18
|
+
now?: number;
|
|
19
|
+
max_future_drift_ms: number;
|
|
20
|
+
max_past_drift_ms: number;
|
|
21
|
+
};
|
|
22
|
+
export type EnvelopeValidationResult = {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
reason?: "not_object" | "invalid_version" | "invalid_message_id" | "invalid_topic" | "invalid_source_peer_id" | "invalid_timestamp" | "missing_payload" | "timestamp_future_drift" | "timestamp_past_drift";
|
|
25
|
+
envelope?: NetworkMessageEnvelope;
|
|
26
|
+
drift_ms?: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function validateNetworkMessageEnvelope(value: unknown, options: EnvelopeValidationOptions): EnvelopeValidationResult;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateNetworkMessageEnvelope = validateNetworkMessageEnvelope;
|
|
4
|
+
function validateNetworkMessageEnvelope(value, options) {
|
|
5
|
+
if (typeof value !== "object" || value === null) {
|
|
6
|
+
return { ok: false, reason: "not_object" };
|
|
7
|
+
}
|
|
8
|
+
const envelope = value;
|
|
9
|
+
if (envelope.version !== 1) {
|
|
10
|
+
return { ok: false, reason: "invalid_version" };
|
|
11
|
+
}
|
|
12
|
+
if (typeof envelope.message_id !== "string" || envelope.message_id.trim().length === 0) {
|
|
13
|
+
return { ok: false, reason: "invalid_message_id" };
|
|
14
|
+
}
|
|
15
|
+
if (typeof envelope.topic !== "string" || envelope.topic.trim().length === 0) {
|
|
16
|
+
return { ok: false, reason: "invalid_topic" };
|
|
17
|
+
}
|
|
18
|
+
if (typeof envelope.source_peer_id !== "string" || envelope.source_peer_id.trim().length === 0) {
|
|
19
|
+
return { ok: false, reason: "invalid_source_peer_id" };
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isFinite(envelope.timestamp)) {
|
|
22
|
+
return { ok: false, reason: "invalid_timestamp" };
|
|
23
|
+
}
|
|
24
|
+
if (!("payload" in envelope)) {
|
|
25
|
+
return { ok: false, reason: "missing_payload" };
|
|
26
|
+
}
|
|
27
|
+
const now = options.now ?? Date.now();
|
|
28
|
+
const drift = Number(envelope.timestamp) - now;
|
|
29
|
+
if (drift > options.max_future_drift_ms) {
|
|
30
|
+
return { ok: false, reason: "timestamp_future_drift", drift_ms: drift };
|
|
31
|
+
}
|
|
32
|
+
if (drift < -options.max_past_drift_ms) {
|
|
33
|
+
return { ok: false, reason: "timestamp_past_drift", drift_ms: drift };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true, envelope: envelope, drift_ms: drift };
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NetworkMessageEnvelope } from "./messageEnvelope";
|
|
2
|
+
export type PeerStatus = "online" | "stale";
|
|
3
|
+
export type PeerSnapshot = {
|
|
4
|
+
peer_id: string;
|
|
5
|
+
first_seen_at: number;
|
|
6
|
+
last_seen_at: number;
|
|
7
|
+
status: PeerStatus;
|
|
8
|
+
stale_since_at?: number;
|
|
9
|
+
messages_seen: number;
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export type PeerDiscoveryContext = {
|
|
13
|
+
self_peer_id: string;
|
|
14
|
+
publishControl: (topic: string, payload: unknown) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export type PeerDiscoveryStats = {
|
|
17
|
+
observe_calls: number;
|
|
18
|
+
peers_added: number;
|
|
19
|
+
peers_removed: number;
|
|
20
|
+
peers_marked_stale: number;
|
|
21
|
+
heartbeat_sent: number;
|
|
22
|
+
heartbeat_send_errors: number;
|
|
23
|
+
reconcile_runs: number;
|
|
24
|
+
last_observed_at: number;
|
|
25
|
+
last_heartbeat_at: number;
|
|
26
|
+
last_reconcile_at: number;
|
|
27
|
+
last_error_at: number;
|
|
28
|
+
};
|
|
29
|
+
export type PeerDiscoveryConfigSnapshot = {
|
|
30
|
+
discovery: string;
|
|
31
|
+
heartbeat_topic?: string;
|
|
32
|
+
heartbeat_interval_ms?: number;
|
|
33
|
+
stale_after_ms?: number;
|
|
34
|
+
remove_after_ms?: number;
|
|
35
|
+
};
|
|
36
|
+
export interface PeerDiscovery {
|
|
37
|
+
start(context: PeerDiscoveryContext): Promise<void>;
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
observeEnvelope(envelope: NetworkMessageEnvelope): void;
|
|
40
|
+
listPeers(): PeerSnapshot[];
|
|
41
|
+
getStats?(): PeerDiscoveryStats;
|
|
42
|
+
getConfig?(): PeerDiscoveryConfigSnapshot;
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type TransportMessageMeta = {
|
|
2
|
+
remote_address: string;
|
|
3
|
+
remote_port: number;
|
|
4
|
+
transport: string;
|
|
5
|
+
};
|
|
6
|
+
export type TransportLifecycleState = "stopped" | "starting" | "running" | "stopping" | "error";
|
|
7
|
+
export type TransportStats = {
|
|
8
|
+
starts: number;
|
|
9
|
+
stops: number;
|
|
10
|
+
start_errors: number;
|
|
11
|
+
stop_errors: number;
|
|
12
|
+
sent_messages: number;
|
|
13
|
+
sent_bytes: number;
|
|
14
|
+
send_errors: number;
|
|
15
|
+
received_messages: number;
|
|
16
|
+
received_bytes: number;
|
|
17
|
+
receive_errors: number;
|
|
18
|
+
last_sent_at: number;
|
|
19
|
+
last_received_at: number;
|
|
20
|
+
last_error_at: number;
|
|
21
|
+
};
|
|
22
|
+
export type TransportConfigSnapshot = {
|
|
23
|
+
transport: string;
|
|
24
|
+
state: TransportLifecycleState;
|
|
25
|
+
bind_address?: string;
|
|
26
|
+
broadcast_address?: string;
|
|
27
|
+
port?: number;
|
|
28
|
+
};
|
|
29
|
+
export interface NetworkTransport {
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
send(data: Buffer): Promise<void>;
|
|
33
|
+
onMessage(handler: (data: Buffer, meta: TransportMessageMeta) => void): () => void;
|
|
34
|
+
getStats?(): TransportStats;
|
|
35
|
+
getConfig?(): TransportConfigSnapshot;
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DecodedNetworkMessage, MessageEnvelopeCodec, NetworkMessageEnvelope } from "../abstractions/messageEnvelope";
|
|
2
|
+
export declare class JsonMessageEnvelopeCodec implements MessageEnvelopeCodec {
|
|
3
|
+
encode(envelope: NetworkMessageEnvelope): Buffer;
|
|
4
|
+
decode(raw: Buffer): DecodedNetworkMessage | null;
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonMessageEnvelopeCodec = void 0;
|
|
4
|
+
class JsonMessageEnvelopeCodec {
|
|
5
|
+
encode(envelope) {
|
|
6
|
+
return Buffer.from(JSON.stringify(envelope), "utf8");
|
|
7
|
+
}
|
|
8
|
+
decode(raw) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw.toString("utf8"));
|
|
11
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
envelope: parsed,
|
|
16
|
+
raw,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.JsonMessageEnvelopeCodec = JsonMessageEnvelopeCodec;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonTopicCodec = void 0;
|
|
4
|
+
class JsonTopicCodec {
|
|
5
|
+
encode(_topic, payload) {
|
|
6
|
+
return payload;
|
|
7
|
+
}
|
|
8
|
+
decode(_topic, payload) {
|
|
9
|
+
return payload;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.JsonTopicCodec = JsonTopicCodec;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PeerDiscovery, PeerDiscoveryConfigSnapshot, PeerDiscoveryContext, PeerDiscoveryStats, PeerSnapshot } from "../abstractions/peerDiscovery";
|
|
2
|
+
import { NetworkMessageEnvelope } from "../abstractions/messageEnvelope";
|
|
3
|
+
type HeartbeatPeerDiscoveryOptions = {
|
|
4
|
+
heartbeatIntervalMs?: number;
|
|
5
|
+
staleAfterMs?: number;
|
|
6
|
+
removeAfterMs?: number;
|
|
7
|
+
topic?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare class HeartbeatPeerDiscovery implements PeerDiscovery {
|
|
10
|
+
private peers;
|
|
11
|
+
private timer;
|
|
12
|
+
private context;
|
|
13
|
+
private heartbeatIntervalMs;
|
|
14
|
+
private staleAfterMs;
|
|
15
|
+
private removeAfterMs;
|
|
16
|
+
private topic;
|
|
17
|
+
private stats;
|
|
18
|
+
constructor(options?: HeartbeatPeerDiscoveryOptions);
|
|
19
|
+
start(context: PeerDiscoveryContext): Promise<void>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
observeEnvelope(envelope: NetworkMessageEnvelope): void;
|
|
22
|
+
listPeers(): PeerSnapshot[];
|
|
23
|
+
getStats(): PeerDiscoveryStats;
|
|
24
|
+
getConfig(): PeerDiscoveryConfigSnapshot;
|
|
25
|
+
private sendHeartbeat;
|
|
26
|
+
private reconcilePeerHealth;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeartbeatPeerDiscovery = void 0;
|
|
4
|
+
class HeartbeatPeerDiscovery {
|
|
5
|
+
peers = new Map();
|
|
6
|
+
timer = null;
|
|
7
|
+
context = null;
|
|
8
|
+
heartbeatIntervalMs;
|
|
9
|
+
staleAfterMs;
|
|
10
|
+
removeAfterMs;
|
|
11
|
+
topic;
|
|
12
|
+
stats = {
|
|
13
|
+
observe_calls: 0,
|
|
14
|
+
peers_added: 0,
|
|
15
|
+
peers_removed: 0,
|
|
16
|
+
peers_marked_stale: 0,
|
|
17
|
+
heartbeat_sent: 0,
|
|
18
|
+
heartbeat_send_errors: 0,
|
|
19
|
+
reconcile_runs: 0,
|
|
20
|
+
last_observed_at: 0,
|
|
21
|
+
last_heartbeat_at: 0,
|
|
22
|
+
last_reconcile_at: 0,
|
|
23
|
+
last_error_at: 0,
|
|
24
|
+
};
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 12_000;
|
|
27
|
+
this.staleAfterMs = options.staleAfterMs ?? 45_000;
|
|
28
|
+
this.removeAfterMs = options.removeAfterMs ?? 180_000;
|
|
29
|
+
this.topic = options.topic ?? "__discovery/heartbeat";
|
|
30
|
+
}
|
|
31
|
+
async start(context) {
|
|
32
|
+
this.context = context;
|
|
33
|
+
this.reconcilePeerHealth();
|
|
34
|
+
await this.sendHeartbeat();
|
|
35
|
+
this.timer = setInterval(async () => {
|
|
36
|
+
await this.sendHeartbeat();
|
|
37
|
+
this.reconcilePeerHealth();
|
|
38
|
+
}, this.heartbeatIntervalMs);
|
|
39
|
+
}
|
|
40
|
+
async stop() {
|
|
41
|
+
if (this.timer) {
|
|
42
|
+
clearInterval(this.timer);
|
|
43
|
+
this.timer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
observeEnvelope(envelope) {
|
|
47
|
+
this.stats.observe_calls += 1;
|
|
48
|
+
this.stats.last_observed_at = Date.now();
|
|
49
|
+
if (!this.context) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (envelope.source_peer_id === this.context.self_peer_id) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const existing = this.peers.get(envelope.source_peer_id);
|
|
57
|
+
if (!existing) {
|
|
58
|
+
this.stats.peers_added += 1;
|
|
59
|
+
}
|
|
60
|
+
this.peers.set(envelope.source_peer_id, {
|
|
61
|
+
peer_id: envelope.source_peer_id,
|
|
62
|
+
first_seen_at: existing?.first_seen_at ?? now,
|
|
63
|
+
last_seen_at: now,
|
|
64
|
+
status: "online",
|
|
65
|
+
stale_since_at: undefined,
|
|
66
|
+
messages_seen: (existing?.messages_seen ?? 0) + 1,
|
|
67
|
+
meta: envelope.topic === this.topic && typeof envelope.payload === "object" && envelope.payload !== null
|
|
68
|
+
? envelope.payload
|
|
69
|
+
: existing?.meta,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
listPeers() {
|
|
73
|
+
this.reconcilePeerHealth();
|
|
74
|
+
return Array.from(this.peers.values()).sort((a, b) => {
|
|
75
|
+
const score = (p) => (p.status === "online" ? 1 : 0);
|
|
76
|
+
const byStatus = score(b) - score(a);
|
|
77
|
+
if (byStatus !== 0) {
|
|
78
|
+
return byStatus;
|
|
79
|
+
}
|
|
80
|
+
return b.last_seen_at - a.last_seen_at;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
getStats() {
|
|
84
|
+
return { ...this.stats };
|
|
85
|
+
}
|
|
86
|
+
getConfig() {
|
|
87
|
+
return {
|
|
88
|
+
discovery: "heartbeat-peer-discovery",
|
|
89
|
+
heartbeat_topic: this.topic,
|
|
90
|
+
heartbeat_interval_ms: this.heartbeatIntervalMs,
|
|
91
|
+
stale_after_ms: this.staleAfterMs,
|
|
92
|
+
remove_after_ms: this.removeAfterMs,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async sendHeartbeat() {
|
|
96
|
+
if (!this.context) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await this.context.publishControl(this.topic, {
|
|
101
|
+
kind: "heartbeat",
|
|
102
|
+
at: Date.now(),
|
|
103
|
+
});
|
|
104
|
+
this.stats.heartbeat_sent += 1;
|
|
105
|
+
this.stats.last_heartbeat_at = Date.now();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
this.stats.heartbeat_send_errors += 1;
|
|
109
|
+
this.stats.last_error_at = Date.now();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
reconcilePeerHealth() {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
this.stats.reconcile_runs += 1;
|
|
115
|
+
this.stats.last_reconcile_at = now;
|
|
116
|
+
for (const [peerId, peer] of this.peers.entries()) {
|
|
117
|
+
const age = now - peer.last_seen_at;
|
|
118
|
+
if (age > this.removeAfterMs) {
|
|
119
|
+
this.peers.delete(peerId);
|
|
120
|
+
this.stats.peers_removed += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (age > this.staleAfterMs) {
|
|
124
|
+
if (peer.status !== "stale") {
|
|
125
|
+
this.stats.peers_marked_stale += 1;
|
|
126
|
+
}
|
|
127
|
+
this.peers.set(peerId, {
|
|
128
|
+
...peer,
|
|
129
|
+
status: "stale",
|
|
130
|
+
stale_since_at: peer.stale_since_at ?? now,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (peer.status !== "online") {
|
|
135
|
+
this.peers.set(peerId, {
|
|
136
|
+
...peer,
|
|
137
|
+
status: "online",
|
|
138
|
+
stale_since_at: undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.HeartbeatPeerDiscovery = HeartbeatPeerDiscovery;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./mock";
|
|
3
|
+
export * from "./localEventBus";
|
|
4
|
+
export * from "./realPreview";
|
|
5
|
+
export * from "./webrtcPreview";
|
|
6
|
+
export * from "./relayPreview";
|
|
7
|
+
export * from "./abstractions/messageEnvelope";
|
|
8
|
+
export * from "./abstractions/topicCodec";
|
|
9
|
+
export * from "./abstractions/transport";
|
|
10
|
+
export * from "./abstractions/peerDiscovery";
|
|
11
|
+
export * from "./codec/jsonMessageEnvelopeCodec";
|
|
12
|
+
export * from "./codec/jsonTopicCodec";
|
|
13
|
+
export * from "./discovery/heartbeatPeerDiscovery";
|
|
14
|
+
export * from "./transport/udpLanBroadcastTransport";
|
|
@@ -0,0 +1,30 @@
|
|
|
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("./types"), exports);
|
|
18
|
+
__exportStar(require("./mock"), exports);
|
|
19
|
+
__exportStar(require("./localEventBus"), exports);
|
|
20
|
+
__exportStar(require("./realPreview"), exports);
|
|
21
|
+
__exportStar(require("./webrtcPreview"), exports);
|
|
22
|
+
__exportStar(require("./relayPreview"), exports);
|
|
23
|
+
__exportStar(require("./abstractions/messageEnvelope"), exports);
|
|
24
|
+
__exportStar(require("./abstractions/topicCodec"), exports);
|
|
25
|
+
__exportStar(require("./abstractions/transport"), exports);
|
|
26
|
+
__exportStar(require("./abstractions/peerDiscovery"), exports);
|
|
27
|
+
__exportStar(require("./codec/jsonMessageEnvelopeCodec"), exports);
|
|
28
|
+
__exportStar(require("./codec/jsonTopicCodec"), exports);
|
|
29
|
+
__exportStar(require("./discovery/heartbeatPeerDiscovery"), exports);
|
|
30
|
+
__exportStar(require("./transport/udpLanBroadcastTransport"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NetworkAdapter } from "./types";
|
|
2
|
+
export declare class LocalEventBusAdapter implements NetworkAdapter {
|
|
3
|
+
private started;
|
|
4
|
+
private emitter;
|
|
5
|
+
start(): Promise<void>;
|
|
6
|
+
stop(): Promise<void>;
|
|
7
|
+
publish(topic: string, data: any): Promise<void>;
|
|
8
|
+
subscribe(topic: string, handler: (data: any) => void): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalEventBusAdapter = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const CHANNEL_NAME = "silicaclaw-local-event-bus";
|
|
6
|
+
function getNodeBus() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (!g.__silicaclaw_bus) {
|
|
9
|
+
g.__silicaclaw_bus = new events_1.EventEmitter();
|
|
10
|
+
}
|
|
11
|
+
return g.__silicaclaw_bus;
|
|
12
|
+
}
|
|
13
|
+
class LocalEventBusAdapter {
|
|
14
|
+
started = false;
|
|
15
|
+
emitter = getNodeBus();
|
|
16
|
+
async start() {
|
|
17
|
+
this.started = true;
|
|
18
|
+
}
|
|
19
|
+
async stop() {
|
|
20
|
+
this.started = false;
|
|
21
|
+
}
|
|
22
|
+
async publish(topic, data) {
|
|
23
|
+
if (!this.started) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
27
|
+
const channel = new BroadcastChannel(CHANNEL_NAME);
|
|
28
|
+
channel.postMessage({ topic, data });
|
|
29
|
+
channel.close();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setImmediate(() => this.emitter.emit(topic, data));
|
|
33
|
+
}
|
|
34
|
+
subscribe(topic, handler) {
|
|
35
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
36
|
+
const channel = new BroadcastChannel(CHANNEL_NAME);
|
|
37
|
+
channel.onmessage = (event) => {
|
|
38
|
+
if (event.data?.topic === topic) {
|
|
39
|
+
handler(event.data.data);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.emitter.on(topic, handler);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.LocalEventBusAdapter = LocalEventBusAdapter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NetworkAdapter } from "./types";
|
|
2
|
+
export declare class MockNetworkAdapter implements NetworkAdapter {
|
|
3
|
+
private started;
|
|
4
|
+
start(): Promise<void>;
|
|
5
|
+
stop(): Promise<void>;
|
|
6
|
+
publish(topic: string, data: any): Promise<void>;
|
|
7
|
+
subscribe(topic: string, handler: (data: any) => void): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MockNetworkAdapter = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const bus = new events_1.EventEmitter();
|
|
6
|
+
class MockNetworkAdapter {
|
|
7
|
+
started = false;
|
|
8
|
+
async start() {
|
|
9
|
+
this.started = true;
|
|
10
|
+
}
|
|
11
|
+
async stop() {
|
|
12
|
+
this.started = false;
|
|
13
|
+
}
|
|
14
|
+
async publish(topic, data) {
|
|
15
|
+
if (!this.started) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
setImmediate(() => bus.emit(topic, data));
|
|
19
|
+
}
|
|
20
|
+
subscribe(topic, handler) {
|
|
21
|
+
bus.on(topic, handler);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.MockNetworkAdapter = MockNetworkAdapter;
|