acn-client 0.2.1
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/README.md +244 -0
- package/dist/index.d.mts +579 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +627 -0
- package/dist/index.mjs +587 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ACNClient: () => ACNClient,
|
|
34
|
+
ACNError: () => ACNError,
|
|
35
|
+
ACNRealtime: () => ACNRealtime,
|
|
36
|
+
subscribeToACN: () => subscribeToACN
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/client.ts
|
|
41
|
+
var ACNClient = class {
|
|
42
|
+
baseUrl;
|
|
43
|
+
timeout;
|
|
44
|
+
headers;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
if (typeof options === "string") {
|
|
47
|
+
this.baseUrl = options.replace(/\/$/, "");
|
|
48
|
+
this.timeout = 3e4;
|
|
49
|
+
this.headers = {};
|
|
50
|
+
} else {
|
|
51
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
52
|
+
this.timeout = options.timeout ?? 3e4;
|
|
53
|
+
this.headers = options.headers ?? {};
|
|
54
|
+
if (options.apiKey) {
|
|
55
|
+
this.headers["X-API-Key"] = options.apiKey;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ============================================
|
|
60
|
+
// Internal HTTP Methods
|
|
61
|
+
// ============================================
|
|
62
|
+
async request(method, path, options) {
|
|
63
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
64
|
+
if (options?.params) {
|
|
65
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
|
66
|
+
if (value !== void 0) {
|
|
67
|
+
url.searchParams.append(key, String(value));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(url.toString(), {
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
...this.headers
|
|
79
|
+
},
|
|
80
|
+
body: options?.body ? JSON.stringify(options.body) : void 0,
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
85
|
+
throw new ACNError(response.status, error.detail || error.message || "Request failed");
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 204) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
return response.json();
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
get(path, params) {
|
|
96
|
+
return this.request("GET", path, { params });
|
|
97
|
+
}
|
|
98
|
+
post(path, body) {
|
|
99
|
+
return this.request("POST", path, { body });
|
|
100
|
+
}
|
|
101
|
+
delete(path) {
|
|
102
|
+
return this.request("DELETE", path);
|
|
103
|
+
}
|
|
104
|
+
// ============================================
|
|
105
|
+
// Health & Status
|
|
106
|
+
// ============================================
|
|
107
|
+
/** Check if ACN server is healthy */
|
|
108
|
+
async health() {
|
|
109
|
+
return this.get("/health");
|
|
110
|
+
}
|
|
111
|
+
/** Get server statistics */
|
|
112
|
+
async getStats() {
|
|
113
|
+
return this.get("/api/v1/stats");
|
|
114
|
+
}
|
|
115
|
+
// ============================================
|
|
116
|
+
// Agent Management
|
|
117
|
+
// ============================================
|
|
118
|
+
/** Register a new agent */
|
|
119
|
+
async registerAgent(agent) {
|
|
120
|
+
return this.post("/api/v1/agents/register", agent);
|
|
121
|
+
}
|
|
122
|
+
/** Get agent by ID */
|
|
123
|
+
async getAgent(agentId) {
|
|
124
|
+
return this.get(`/api/v1/agents/${agentId}`);
|
|
125
|
+
}
|
|
126
|
+
/** Search agents (status: online | offline | all; public list does not include verification_code) */
|
|
127
|
+
async searchAgents(options) {
|
|
128
|
+
return this.get("/api/v1/agents", {
|
|
129
|
+
skill: options?.skills,
|
|
130
|
+
status: options?.status
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Unregister an agent */
|
|
134
|
+
async unregisterAgent(agentId) {
|
|
135
|
+
return this.delete(`/api/v1/agents/${agentId}`);
|
|
136
|
+
}
|
|
137
|
+
/** Send agent heartbeat */
|
|
138
|
+
async heartbeat(agentId) {
|
|
139
|
+
return this.post(`/api/v1/agents/${agentId}/heartbeat`);
|
|
140
|
+
}
|
|
141
|
+
/** Get agent endpoint */
|
|
142
|
+
async getAgentEndpoint(agentId) {
|
|
143
|
+
return this.get(`/api/v1/agents/${agentId}/endpoint`);
|
|
144
|
+
}
|
|
145
|
+
/** List all available skills */
|
|
146
|
+
async getSkills() {
|
|
147
|
+
return this.get("/api/v1/skills");
|
|
148
|
+
}
|
|
149
|
+
// ============================================
|
|
150
|
+
// Subnet Management
|
|
151
|
+
// ============================================
|
|
152
|
+
/** Create a new subnet */
|
|
153
|
+
async createSubnet(request) {
|
|
154
|
+
return this.post("/api/v1/subnets", request);
|
|
155
|
+
}
|
|
156
|
+
/** List all subnets */
|
|
157
|
+
async listSubnets() {
|
|
158
|
+
return this.get("/api/v1/subnets");
|
|
159
|
+
}
|
|
160
|
+
/** Get subnet by ID */
|
|
161
|
+
async getSubnet(subnetId) {
|
|
162
|
+
return this.get(`/api/v1/subnets/${subnetId}`);
|
|
163
|
+
}
|
|
164
|
+
/** Delete a subnet */
|
|
165
|
+
async deleteSubnet(subnetId, force = false) {
|
|
166
|
+
return this.request("DELETE", `/api/v1/subnets/${subnetId}`, {
|
|
167
|
+
params: { force }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** Get agents in a subnet */
|
|
171
|
+
async getSubnetAgents(subnetId) {
|
|
172
|
+
return this.get(`/api/v1/subnets/${subnetId}/agents`);
|
|
173
|
+
}
|
|
174
|
+
/** Join agent to subnet */
|
|
175
|
+
async joinSubnet(agentId, subnetId) {
|
|
176
|
+
return this.post(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
|
|
177
|
+
}
|
|
178
|
+
/** Remove agent from subnet */
|
|
179
|
+
async leaveSubnet(agentId, subnetId) {
|
|
180
|
+
return this.delete(`/api/v1/agents/${agentId}/subnets/${subnetId}`);
|
|
181
|
+
}
|
|
182
|
+
/** Get agent's subnets */
|
|
183
|
+
async getAgentSubnets(agentId) {
|
|
184
|
+
return this.get(`/api/v1/agents/${agentId}/subnets`);
|
|
185
|
+
}
|
|
186
|
+
// ============================================
|
|
187
|
+
// Communication
|
|
188
|
+
// ============================================
|
|
189
|
+
/** Send message to an agent */
|
|
190
|
+
async sendMessage(request) {
|
|
191
|
+
return this.post("/api/v1/communication/send", request);
|
|
192
|
+
}
|
|
193
|
+
/** Broadcast message to multiple agents */
|
|
194
|
+
async broadcast(request) {
|
|
195
|
+
return this.post("/api/v1/communication/broadcast", request);
|
|
196
|
+
}
|
|
197
|
+
/** Broadcast message to agents with specific skill */
|
|
198
|
+
async broadcastBySkill(request) {
|
|
199
|
+
return this.post("/api/v1/communication/broadcast-by-skill", request);
|
|
200
|
+
}
|
|
201
|
+
/** Get message history for an agent */
|
|
202
|
+
async getMessageHistory(agentId, options) {
|
|
203
|
+
return this.get(`/api/v1/communication/history/${agentId}`, options);
|
|
204
|
+
}
|
|
205
|
+
// ============================================
|
|
206
|
+
// Payment Discovery
|
|
207
|
+
// ============================================
|
|
208
|
+
/** Set agent's payment capability */
|
|
209
|
+
async setPaymentCapability(agentId, capability) {
|
|
210
|
+
return this.post(`/api/v1/agents/${agentId}/payment-capability`, capability);
|
|
211
|
+
}
|
|
212
|
+
/** Get agent's payment capability */
|
|
213
|
+
async getPaymentCapability(agentId) {
|
|
214
|
+
return this.get(`/api/v1/agents/${agentId}/payment-capability`);
|
|
215
|
+
}
|
|
216
|
+
/** Discover agents that accept payments */
|
|
217
|
+
async discoverPaymentAgents(options) {
|
|
218
|
+
return this.get("/api/v1/payments/discover", {
|
|
219
|
+
method: options?.method,
|
|
220
|
+
network: options?.network,
|
|
221
|
+
min_amount: options?.min_amount,
|
|
222
|
+
max_amount: options?.max_amount
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** Get payment task by ID */
|
|
226
|
+
async getPaymentTask(taskId) {
|
|
227
|
+
return this.get(`/api/v1/payments/tasks/${taskId}`);
|
|
228
|
+
}
|
|
229
|
+
/** Get agent's payment tasks */
|
|
230
|
+
async getAgentPaymentTasks(agentId, options) {
|
|
231
|
+
return this.get(`/api/v1/payments/tasks/agent/${agentId}`, options);
|
|
232
|
+
}
|
|
233
|
+
/** Get agent's payment statistics */
|
|
234
|
+
async getPaymentStats(agentId) {
|
|
235
|
+
return this.get(`/api/v1/payments/stats/${agentId}`);
|
|
236
|
+
}
|
|
237
|
+
// ============================================
|
|
238
|
+
// Monitoring & Analytics
|
|
239
|
+
// ============================================
|
|
240
|
+
/** Get Prometheus metrics (text format) */
|
|
241
|
+
async getPrometheusMetrics() {
|
|
242
|
+
const response = await fetch(`${this.baseUrl}/metrics`);
|
|
243
|
+
return response.text();
|
|
244
|
+
}
|
|
245
|
+
/** Get all metrics */
|
|
246
|
+
async getMetrics() {
|
|
247
|
+
return this.get("/api/v1/monitoring/metrics");
|
|
248
|
+
}
|
|
249
|
+
/** Get system health */
|
|
250
|
+
async getSystemHealth() {
|
|
251
|
+
return this.get("/api/v1/monitoring/health");
|
|
252
|
+
}
|
|
253
|
+
/** Get dashboard data */
|
|
254
|
+
async getDashboard() {
|
|
255
|
+
return this.get("/api/v1/monitoring/dashboard");
|
|
256
|
+
}
|
|
257
|
+
/** Get agent analytics */
|
|
258
|
+
async getAgentAnalytics() {
|
|
259
|
+
return this.get("/api/v1/analytics/agents");
|
|
260
|
+
}
|
|
261
|
+
/** Get specific agent's activity */
|
|
262
|
+
async getAgentActivity(agentId, options) {
|
|
263
|
+
return this.get(`/api/v1/analytics/agents/${agentId}`, options);
|
|
264
|
+
}
|
|
265
|
+
/** Get message analytics */
|
|
266
|
+
async getMessageAnalytics() {
|
|
267
|
+
return this.get("/api/v1/analytics/messages");
|
|
268
|
+
}
|
|
269
|
+
/** Get latency analytics */
|
|
270
|
+
async getLatencyAnalytics() {
|
|
271
|
+
return this.get("/api/v1/analytics/latency");
|
|
272
|
+
}
|
|
273
|
+
/** Get subnet analytics */
|
|
274
|
+
async getSubnetAnalytics() {
|
|
275
|
+
return this.get("/api/v1/analytics/subnets");
|
|
276
|
+
}
|
|
277
|
+
// ============================================
|
|
278
|
+
// Audit
|
|
279
|
+
// ============================================
|
|
280
|
+
/** Get audit events */
|
|
281
|
+
async getAuditEvents(options) {
|
|
282
|
+
return this.get("/api/v1/audit/events", options);
|
|
283
|
+
}
|
|
284
|
+
/** Get recent audit events */
|
|
285
|
+
async getRecentAuditEvents(limit = 100) {
|
|
286
|
+
return this.get("/api/v1/audit/events/recent", { limit });
|
|
287
|
+
}
|
|
288
|
+
// ============================================
|
|
289
|
+
// ERC-8004 On-Chain Identity
|
|
290
|
+
// ============================================
|
|
291
|
+
/**
|
|
292
|
+
* Register the agent on ERC-8004 Identity Registry and bind to ACN.
|
|
293
|
+
*
|
|
294
|
+
* Full flow:
|
|
295
|
+
* 1. Generate wallet if privateKey is undefined (saved to saveWalletPath).
|
|
296
|
+
* 2. Construct agentURI → agent-registration.json endpoint.
|
|
297
|
+
* 3. Sign and broadcast register(agentURI) transaction via viem.
|
|
298
|
+
* 4. Extract token ID from Registered event.
|
|
299
|
+
* 5. POST /api/v1/onchain/agents/{agentId}/bind to inform ACN.
|
|
300
|
+
*
|
|
301
|
+
* @param agentId - ACN agent ID (from join response).
|
|
302
|
+
* @param options - Chain, RPC, private key, wallet save path.
|
|
303
|
+
*/
|
|
304
|
+
async registerOnchain(agentId, options = {}) {
|
|
305
|
+
const {
|
|
306
|
+
chain = "base",
|
|
307
|
+
rpcUrl,
|
|
308
|
+
saveWalletPath = ".env"
|
|
309
|
+
} = options;
|
|
310
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
311
|
+
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
|
312
|
+
const { base, baseSepolia } = await import("viem/chains");
|
|
313
|
+
const chainConfigs = {
|
|
314
|
+
base: {
|
|
315
|
+
viemChain: base,
|
|
316
|
+
identityContract: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
317
|
+
namespace: "eip155:8453"
|
|
318
|
+
},
|
|
319
|
+
"base-sepolia": {
|
|
320
|
+
viemChain: baseSepolia,
|
|
321
|
+
identityContract: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
|
|
322
|
+
namespace: "eip155:84532"
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const cfg = chainConfigs[chain];
|
|
326
|
+
let walletGenerated = false;
|
|
327
|
+
let privateKey;
|
|
328
|
+
if (!options.privateKey) {
|
|
329
|
+
privateKey = generatePrivateKey();
|
|
330
|
+
walletGenerated = true;
|
|
331
|
+
if (saveWalletPath) {
|
|
332
|
+
await this._saveWalletToEnv(saveWalletPath, privateKey);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
privateKey = options.privateKey;
|
|
336
|
+
}
|
|
337
|
+
const account = privateKeyToAccount(privateKey);
|
|
338
|
+
const agentRegistrationUrl = `${this.baseUrl}/api/v1/agents/${agentId}/.well-known/agent-registration.json`;
|
|
339
|
+
const abi = [
|
|
340
|
+
{
|
|
341
|
+
name: "register",
|
|
342
|
+
type: "function",
|
|
343
|
+
stateMutability: "nonpayable",
|
|
344
|
+
inputs: [{ type: "string", name: "agentURI" }],
|
|
345
|
+
outputs: [{ type: "uint256", name: "agentId" }]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "Registered",
|
|
349
|
+
type: "event",
|
|
350
|
+
inputs: [
|
|
351
|
+
{ type: "uint256", name: "agentId", indexed: true },
|
|
352
|
+
{ type: "string", name: "agentURI", indexed: false },
|
|
353
|
+
{ type: "address", name: "owner", indexed: true }
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
];
|
|
357
|
+
const transport = http(rpcUrl ?? void 0);
|
|
358
|
+
const walletClient = createWalletClient({ account, chain: cfg.viemChain, transport });
|
|
359
|
+
const publicClient = createPublicClient({ chain: cfg.viemChain, transport });
|
|
360
|
+
const txHash = await walletClient.writeContract({
|
|
361
|
+
address: cfg.identityContract,
|
|
362
|
+
abi,
|
|
363
|
+
functionName: "register",
|
|
364
|
+
args: [agentRegistrationUrl]
|
|
365
|
+
});
|
|
366
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
367
|
+
const { decodeEventLog } = await import("viem");
|
|
368
|
+
let tokenId;
|
|
369
|
+
for (const log of receipt.logs) {
|
|
370
|
+
try {
|
|
371
|
+
const decoded = decodeEventLog({ abi, data: log.data, topics: log.topics });
|
|
372
|
+
if (decoded.eventName === "Registered") {
|
|
373
|
+
tokenId = decoded.args.agentId;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (tokenId === void 0) {
|
|
380
|
+
throw new Error("Registered event not found in transaction receipt");
|
|
381
|
+
}
|
|
382
|
+
await this.post(`/api/v1/onchain/agents/${agentId}/bind`, {
|
|
383
|
+
token_id: Number(tokenId),
|
|
384
|
+
chain: cfg.namespace,
|
|
385
|
+
tx_hash: txHash
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
tokenId,
|
|
389
|
+
txHash,
|
|
390
|
+
chain: cfg.namespace,
|
|
391
|
+
agentRegistrationUrl,
|
|
392
|
+
walletAddress: account.address,
|
|
393
|
+
walletGenerated
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/** @internal Save generated wallet credentials to a .env file. */
|
|
397
|
+
async _saveWalletToEnv(path, privateKey) {
|
|
398
|
+
if (typeof window !== "undefined") return;
|
|
399
|
+
try {
|
|
400
|
+
const fs = await import("fs/promises");
|
|
401
|
+
let content = "";
|
|
402
|
+
try {
|
|
403
|
+
content = await fs.readFile(path, "utf8");
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
const existing = new Set(content.split("\n").map((l) => l.split("=")[0].trim()));
|
|
407
|
+
const toAdd = [];
|
|
408
|
+
if (!existing.has("WALLET_PRIVATE_KEY")) toAdd.push(`WALLET_PRIVATE_KEY=${privateKey}`);
|
|
409
|
+
if (toAdd.length) await fs.appendFile(path, "\n" + toAdd.join("\n") + "\n");
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Get audit statistics */
|
|
414
|
+
async getAuditStats(options) {
|
|
415
|
+
return this.get("/api/v1/audit/stats", options);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var ACNError = class extends Error {
|
|
419
|
+
constructor(status, message) {
|
|
420
|
+
super(message);
|
|
421
|
+
this.status = status;
|
|
422
|
+
this.name = "ACNError";
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/realtime.ts
|
|
427
|
+
var ACNRealtime = class {
|
|
428
|
+
baseUrl;
|
|
429
|
+
options;
|
|
430
|
+
ws = null;
|
|
431
|
+
state = "disconnected";
|
|
432
|
+
reconnectAttempts = 0;
|
|
433
|
+
heartbeatTimer = null;
|
|
434
|
+
reconnectTimer = null;
|
|
435
|
+
channels = /* @__PURE__ */ new Map();
|
|
436
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
437
|
+
stateHandlers = /* @__PURE__ */ new Set();
|
|
438
|
+
constructor(baseUrl, options) {
|
|
439
|
+
this.baseUrl = baseUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "");
|
|
440
|
+
this.options = {
|
|
441
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
442
|
+
reconnectInterval: options?.reconnectInterval ?? 3e3,
|
|
443
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
444
|
+
heartbeatInterval: options?.heartbeatInterval ?? 3e4
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** Current connection state */
|
|
448
|
+
get connectionState() {
|
|
449
|
+
return this.state;
|
|
450
|
+
}
|
|
451
|
+
/** Whether currently connected */
|
|
452
|
+
get isConnected() {
|
|
453
|
+
return this.state === "connected";
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Connect to a channel
|
|
457
|
+
*/
|
|
458
|
+
async connect(channel = "default") {
|
|
459
|
+
if (this.ws && this.state === "connected") {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
this.setState("connecting");
|
|
464
|
+
try {
|
|
465
|
+
this.ws = new WebSocket(`${this.baseUrl}/ws/${channel}`);
|
|
466
|
+
this.ws.onopen = () => {
|
|
467
|
+
this.setState("connected");
|
|
468
|
+
this.reconnectAttempts = 0;
|
|
469
|
+
this.startHeartbeat();
|
|
470
|
+
resolve();
|
|
471
|
+
};
|
|
472
|
+
this.ws.onclose = (event) => {
|
|
473
|
+
this.stopHeartbeat();
|
|
474
|
+
if (event.wasClean) {
|
|
475
|
+
this.setState("disconnected");
|
|
476
|
+
} else if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
477
|
+
this.scheduleReconnect(channel);
|
|
478
|
+
} else {
|
|
479
|
+
this.setState("disconnected");
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
this.ws.onerror = (error) => {
|
|
483
|
+
if (this.state === "connecting") {
|
|
484
|
+
reject(new Error("WebSocket connection failed"));
|
|
485
|
+
}
|
|
486
|
+
this.handleError(error);
|
|
487
|
+
};
|
|
488
|
+
this.ws.onmessage = (event) => {
|
|
489
|
+
this.handleMessage(event.data);
|
|
490
|
+
};
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.setState("disconnected");
|
|
493
|
+
reject(error);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Disconnect from server
|
|
499
|
+
*/
|
|
500
|
+
disconnect() {
|
|
501
|
+
this.stopHeartbeat();
|
|
502
|
+
this.clearReconnectTimer();
|
|
503
|
+
if (this.ws) {
|
|
504
|
+
this.ws.close(1e3, "Client disconnect");
|
|
505
|
+
this.ws = null;
|
|
506
|
+
}
|
|
507
|
+
this.setState("disconnected");
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Subscribe to a channel
|
|
511
|
+
*/
|
|
512
|
+
subscribe(channel, handler) {
|
|
513
|
+
let handlers = this.channels.get(channel);
|
|
514
|
+
if (!handlers) {
|
|
515
|
+
handlers = /* @__PURE__ */ new Set();
|
|
516
|
+
this.channels.set(channel, handlers);
|
|
517
|
+
}
|
|
518
|
+
handlers.add(handler);
|
|
519
|
+
return () => {
|
|
520
|
+
handlers?.delete(handler);
|
|
521
|
+
if (handlers?.size === 0) {
|
|
522
|
+
this.channels.delete(channel);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Subscribe to all messages
|
|
528
|
+
*/
|
|
529
|
+
onMessage(handler) {
|
|
530
|
+
this.globalHandlers.add(handler);
|
|
531
|
+
return () => {
|
|
532
|
+
this.globalHandlers.delete(handler);
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Subscribe to state changes
|
|
537
|
+
*/
|
|
538
|
+
onStateChange(handler) {
|
|
539
|
+
this.stateHandlers.add(handler);
|
|
540
|
+
return () => {
|
|
541
|
+
this.stateHandlers.delete(handler);
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Send a message
|
|
546
|
+
*/
|
|
547
|
+
send(message) {
|
|
548
|
+
if (!this.ws || this.state !== "connected") {
|
|
549
|
+
throw new Error("WebSocket not connected");
|
|
550
|
+
}
|
|
551
|
+
this.ws.send(JSON.stringify(message));
|
|
552
|
+
}
|
|
553
|
+
// ============================================
|
|
554
|
+
// Private Methods
|
|
555
|
+
// ============================================
|
|
556
|
+
setState(state) {
|
|
557
|
+
this.state = state;
|
|
558
|
+
this.stateHandlers.forEach((handler) => handler(state));
|
|
559
|
+
}
|
|
560
|
+
handleMessage(data) {
|
|
561
|
+
try {
|
|
562
|
+
const message = JSON.parse(data);
|
|
563
|
+
this.globalHandlers.forEach((handler) => handler(message));
|
|
564
|
+
const channelHandlers = this.channels.get(message.channel);
|
|
565
|
+
if (channelHandlers) {
|
|
566
|
+
channelHandlers.forEach((handler) => handler(message));
|
|
567
|
+
}
|
|
568
|
+
const typeHandlers = this.channels.get(message.type);
|
|
569
|
+
if (typeHandlers) {
|
|
570
|
+
typeHandlers.forEach((handler) => handler(message));
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
handleError(error) {
|
|
577
|
+
console.error("WebSocket error:", error);
|
|
578
|
+
}
|
|
579
|
+
startHeartbeat() {
|
|
580
|
+
this.stopHeartbeat();
|
|
581
|
+
this.heartbeatTimer = setInterval(() => {
|
|
582
|
+
if (this.ws && this.state === "connected") {
|
|
583
|
+
this.send({ type: "ping", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
584
|
+
}
|
|
585
|
+
}, this.options.heartbeatInterval);
|
|
586
|
+
}
|
|
587
|
+
stopHeartbeat() {
|
|
588
|
+
if (this.heartbeatTimer) {
|
|
589
|
+
clearInterval(this.heartbeatTimer);
|
|
590
|
+
this.heartbeatTimer = null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
scheduleReconnect(channel) {
|
|
594
|
+
this.clearReconnectTimer();
|
|
595
|
+
this.setState("reconnecting");
|
|
596
|
+
this.reconnectAttempts++;
|
|
597
|
+
const delay = this.options.reconnectInterval * Math.min(this.reconnectAttempts, 5);
|
|
598
|
+
this.reconnectTimer = setTimeout(() => {
|
|
599
|
+
this.connect(channel).catch(() => {
|
|
600
|
+
});
|
|
601
|
+
}, delay);
|
|
602
|
+
}
|
|
603
|
+
clearReconnectTimer() {
|
|
604
|
+
if (this.reconnectTimer) {
|
|
605
|
+
clearTimeout(this.reconnectTimer);
|
|
606
|
+
this.reconnectTimer = null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
function subscribeToACN(baseUrl, channel, handler) {
|
|
611
|
+
const realtime = new ACNRealtime(baseUrl);
|
|
612
|
+
const unsubscribe = realtime.subscribe(channel, handler);
|
|
613
|
+
realtime.connect(channel).catch((error) => {
|
|
614
|
+
console.error("Failed to connect to ACN:", error);
|
|
615
|
+
});
|
|
616
|
+
return () => {
|
|
617
|
+
unsubscribe();
|
|
618
|
+
realtime.disconnect();
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
622
|
+
0 && (module.exports = {
|
|
623
|
+
ACNClient,
|
|
624
|
+
ACNError,
|
|
625
|
+
ACNRealtime,
|
|
626
|
+
subscribeToACN
|
|
627
|
+
});
|