@multi-agent-protocol/sdk 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -0
- package/dist/index-C7XPWnxS.d.cts +3052 -0
- package/dist/index-C7XPWnxS.d.ts +3052 -0
- package/dist/index.cjs +4528 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2288 -0
- package/dist/index.d.ts +2288 -0
- package/dist/index.js +4353 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +4004 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +367 -0
- package/dist/testing.d.ts +367 -0
- package/dist/testing.js +4000 -0
- package/dist/testing.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4353 @@
|
|
|
1
|
+
export { monotonicFactory, ulid } from 'ulid';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// src/types/index.ts
|
|
5
|
+
function isOrphanedAgent(agent) {
|
|
6
|
+
return agent.ownerId === null;
|
|
7
|
+
}
|
|
8
|
+
var EVENT_TYPES = {
|
|
9
|
+
// Agent lifecycle events
|
|
10
|
+
AGENT_REGISTERED: "agent_registered",
|
|
11
|
+
AGENT_UNREGISTERED: "agent_unregistered",
|
|
12
|
+
AGENT_STATE_CHANGED: "agent_state_changed",
|
|
13
|
+
AGENT_ORPHANED: "agent_orphaned",
|
|
14
|
+
// Participant lifecycle events
|
|
15
|
+
PARTICIPANT_CONNECTED: "participant_connected",
|
|
16
|
+
PARTICIPANT_DISCONNECTED: "participant_disconnected",
|
|
17
|
+
// Message events
|
|
18
|
+
MESSAGE_SENT: "message_sent",
|
|
19
|
+
MESSAGE_DELIVERED: "message_delivered",
|
|
20
|
+
MESSAGE_FAILED: "message_failed",
|
|
21
|
+
// Scope events
|
|
22
|
+
SCOPE_CREATED: "scope_created",
|
|
23
|
+
SCOPE_DELETED: "scope_deleted",
|
|
24
|
+
SCOPE_MEMBER_JOINED: "scope_member_joined",
|
|
25
|
+
SCOPE_MEMBER_LEFT: "scope_member_left",
|
|
26
|
+
// Permission events
|
|
27
|
+
PERMISSIONS_CLIENT_UPDATED: "permissions_client_updated",
|
|
28
|
+
PERMISSIONS_AGENT_UPDATED: "permissions_agent_updated",
|
|
29
|
+
// System events
|
|
30
|
+
SYSTEM_ERROR: "system_error",
|
|
31
|
+
// Federation events
|
|
32
|
+
FEDERATION_CONNECTED: "federation_connected",
|
|
33
|
+
FEDERATION_DISCONNECTED: "federation_disconnected"
|
|
34
|
+
};
|
|
35
|
+
function createEvent(input) {
|
|
36
|
+
return {
|
|
37
|
+
id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
38
|
+
timestamp: input.timestamp ?? Date.now(),
|
|
39
|
+
type: input.type,
|
|
40
|
+
source: input.source,
|
|
41
|
+
data: input.data,
|
|
42
|
+
causedBy: input.causedBy,
|
|
43
|
+
_meta: input._meta
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var JSONRPC_VERSION = "2.0";
|
|
47
|
+
var CORE_METHODS = {
|
|
48
|
+
CONNECT: "map/connect",
|
|
49
|
+
DISCONNECT: "map/disconnect",
|
|
50
|
+
SEND: "map/send",
|
|
51
|
+
SUBSCRIBE: "map/subscribe",
|
|
52
|
+
UNSUBSCRIBE: "map/unsubscribe",
|
|
53
|
+
REPLAY: "map/replay"
|
|
54
|
+
};
|
|
55
|
+
var OBSERVATION_METHODS = {
|
|
56
|
+
AGENTS_LIST: "map/agents/list",
|
|
57
|
+
AGENTS_GET: "map/agents/get",
|
|
58
|
+
SCOPES_LIST: "map/scopes/list",
|
|
59
|
+
SCOPES_GET: "map/scopes/get",
|
|
60
|
+
SCOPES_MEMBERS: "map/scopes/members",
|
|
61
|
+
STRUCTURE_GRAPH: "map/structure/graph"
|
|
62
|
+
};
|
|
63
|
+
var LIFECYCLE_METHODS = {
|
|
64
|
+
AGENTS_REGISTER: "map/agents/register",
|
|
65
|
+
AGENTS_UNREGISTER: "map/agents/unregister",
|
|
66
|
+
AGENTS_SPAWN: "map/agents/spawn"
|
|
67
|
+
};
|
|
68
|
+
var STATE_METHODS = {
|
|
69
|
+
AGENTS_UPDATE: "map/agents/update",
|
|
70
|
+
AGENTS_SUSPEND: "map/agents/suspend",
|
|
71
|
+
AGENTS_RESUME: "map/agents/resume",
|
|
72
|
+
AGENTS_STOP: "map/agents/stop"
|
|
73
|
+
};
|
|
74
|
+
var STEERING_METHODS = {
|
|
75
|
+
INJECT: "map/inject"
|
|
76
|
+
};
|
|
77
|
+
var SCOPE_METHODS = {
|
|
78
|
+
SCOPES_CREATE: "map/scopes/create",
|
|
79
|
+
SCOPES_DELETE: "map/scopes/delete",
|
|
80
|
+
SCOPES_JOIN: "map/scopes/join",
|
|
81
|
+
SCOPES_LEAVE: "map/scopes/leave"
|
|
82
|
+
};
|
|
83
|
+
var SESSION_METHODS = {
|
|
84
|
+
SESSION_LIST: "map/session/list",
|
|
85
|
+
SESSION_LOAD: "map/session/load",
|
|
86
|
+
SESSION_CLOSE: "map/session/close"
|
|
87
|
+
};
|
|
88
|
+
var AUTH_METHODS = {
|
|
89
|
+
AUTH_REFRESH: "map/auth/refresh"
|
|
90
|
+
};
|
|
91
|
+
var PERMISSION_METHODS = {
|
|
92
|
+
PERMISSIONS_UPDATE: "map/permissions/update"
|
|
93
|
+
};
|
|
94
|
+
var FEDERATION_METHODS = {
|
|
95
|
+
FEDERATION_CONNECT: "map/federation/connect",
|
|
96
|
+
FEDERATION_ROUTE: "map/federation/route"
|
|
97
|
+
};
|
|
98
|
+
var NOTIFICATION_METHODS = {
|
|
99
|
+
EVENT: "map/event",
|
|
100
|
+
MESSAGE: "map/message",
|
|
101
|
+
/** Client acknowledges received events (for backpressure) */
|
|
102
|
+
SUBSCRIBE_ACK: "map/subscribe.ack"
|
|
103
|
+
};
|
|
104
|
+
var MAP_METHODS = {
|
|
105
|
+
...CORE_METHODS,
|
|
106
|
+
...OBSERVATION_METHODS,
|
|
107
|
+
...LIFECYCLE_METHODS,
|
|
108
|
+
...STATE_METHODS,
|
|
109
|
+
...STEERING_METHODS,
|
|
110
|
+
...SCOPE_METHODS,
|
|
111
|
+
...SESSION_METHODS,
|
|
112
|
+
...AUTH_METHODS,
|
|
113
|
+
...PERMISSION_METHODS,
|
|
114
|
+
...FEDERATION_METHODS
|
|
115
|
+
};
|
|
116
|
+
var STRUCTURE_METHODS = {
|
|
117
|
+
...LIFECYCLE_METHODS,
|
|
118
|
+
...STATE_METHODS,
|
|
119
|
+
...SCOPE_METHODS,
|
|
120
|
+
STRUCTURE_GRAPH: OBSERVATION_METHODS.STRUCTURE_GRAPH
|
|
121
|
+
};
|
|
122
|
+
var EXTENSION_METHODS = {
|
|
123
|
+
...STEERING_METHODS,
|
|
124
|
+
...FEDERATION_METHODS
|
|
125
|
+
};
|
|
126
|
+
var PROTOCOL_ERROR_CODES = {
|
|
127
|
+
PARSE_ERROR: -32700,
|
|
128
|
+
INVALID_REQUEST: -32600,
|
|
129
|
+
METHOD_NOT_FOUND: -32601,
|
|
130
|
+
INVALID_PARAMS: -32602,
|
|
131
|
+
INTERNAL_ERROR: -32603
|
|
132
|
+
};
|
|
133
|
+
var AUTH_ERROR_CODES = {
|
|
134
|
+
AUTH_REQUIRED: 1e3,
|
|
135
|
+
AUTH_FAILED: 1001,
|
|
136
|
+
TOKEN_EXPIRED: 1002,
|
|
137
|
+
PERMISSION_DENIED: 1003
|
|
138
|
+
};
|
|
139
|
+
var ROUTING_ERROR_CODES = {
|
|
140
|
+
ADDRESS_NOT_FOUND: 2e3,
|
|
141
|
+
AGENT_NOT_FOUND: 2001,
|
|
142
|
+
SCOPE_NOT_FOUND: 2002,
|
|
143
|
+
DELIVERY_FAILED: 2003,
|
|
144
|
+
ADDRESS_AMBIGUOUS: 2004
|
|
145
|
+
};
|
|
146
|
+
var AGENT_ERROR_CODES = {
|
|
147
|
+
AGENT_EXISTS: 3e3,
|
|
148
|
+
STATE_INVALID: 3001,
|
|
149
|
+
NOT_RESPONDING: 3002,
|
|
150
|
+
TERMINATED: 3003,
|
|
151
|
+
SPAWN_FAILED: 3004
|
|
152
|
+
};
|
|
153
|
+
var RESOURCE_ERROR_CODES = {
|
|
154
|
+
EXHAUSTED: 4e3,
|
|
155
|
+
RATE_LIMITED: 4001,
|
|
156
|
+
QUOTA_EXCEEDED: 4002
|
|
157
|
+
};
|
|
158
|
+
var FEDERATION_ERROR_CODES = {
|
|
159
|
+
FEDERATION_UNAVAILABLE: 5e3,
|
|
160
|
+
FEDERATION_SYSTEM_NOT_FOUND: 5001,
|
|
161
|
+
FEDERATION_AUTH_FAILED: 5002,
|
|
162
|
+
FEDERATION_ROUTE_REJECTED: 5003,
|
|
163
|
+
/** Message has already visited this system (loop detected) */
|
|
164
|
+
FEDERATION_LOOP_DETECTED: 5010,
|
|
165
|
+
/** Message exceeded maximum hop count */
|
|
166
|
+
FEDERATION_MAX_HOPS_EXCEEDED: 5011
|
|
167
|
+
};
|
|
168
|
+
var ERROR_CODES = {
|
|
169
|
+
...PROTOCOL_ERROR_CODES,
|
|
170
|
+
...AUTH_ERROR_CODES,
|
|
171
|
+
...ROUTING_ERROR_CODES,
|
|
172
|
+
...AGENT_ERROR_CODES,
|
|
173
|
+
...RESOURCE_ERROR_CODES,
|
|
174
|
+
...FEDERATION_ERROR_CODES
|
|
175
|
+
};
|
|
176
|
+
var PROTOCOL_VERSION = 1;
|
|
177
|
+
var CAPABILITY_REQUIREMENTS = {
|
|
178
|
+
// Core
|
|
179
|
+
[CORE_METHODS.CONNECT]: [],
|
|
180
|
+
[CORE_METHODS.DISCONNECT]: [],
|
|
181
|
+
[CORE_METHODS.SEND]: ["messaging.canSend"],
|
|
182
|
+
[CORE_METHODS.SUBSCRIBE]: ["observation.canObserve"],
|
|
183
|
+
[CORE_METHODS.UNSUBSCRIBE]: ["observation.canObserve"],
|
|
184
|
+
// Observation
|
|
185
|
+
[OBSERVATION_METHODS.AGENTS_LIST]: ["observation.canQuery"],
|
|
186
|
+
[OBSERVATION_METHODS.AGENTS_GET]: ["observation.canQuery"],
|
|
187
|
+
[OBSERVATION_METHODS.SCOPES_LIST]: ["observation.canQuery"],
|
|
188
|
+
[OBSERVATION_METHODS.SCOPES_GET]: ["observation.canQuery"],
|
|
189
|
+
[OBSERVATION_METHODS.SCOPES_MEMBERS]: ["observation.canQuery"],
|
|
190
|
+
[OBSERVATION_METHODS.STRUCTURE_GRAPH]: ["observation.canQuery"],
|
|
191
|
+
// Lifecycle
|
|
192
|
+
[LIFECYCLE_METHODS.AGENTS_REGISTER]: ["lifecycle.canRegister"],
|
|
193
|
+
[LIFECYCLE_METHODS.AGENTS_UNREGISTER]: ["lifecycle.canUnregister"],
|
|
194
|
+
[LIFECYCLE_METHODS.AGENTS_SPAWN]: ["lifecycle.canSpawn"],
|
|
195
|
+
// State
|
|
196
|
+
[STATE_METHODS.AGENTS_UPDATE]: ["lifecycle.canRegister"],
|
|
197
|
+
[STATE_METHODS.AGENTS_SUSPEND]: ["lifecycle.canStop"],
|
|
198
|
+
[STATE_METHODS.AGENTS_RESUME]: ["lifecycle.canStop"],
|
|
199
|
+
[STATE_METHODS.AGENTS_STOP]: ["lifecycle.canStop"],
|
|
200
|
+
// Steering
|
|
201
|
+
[STEERING_METHODS.INJECT]: ["lifecycle.canSteer"],
|
|
202
|
+
// Scopes
|
|
203
|
+
[SCOPE_METHODS.SCOPES_CREATE]: ["scopes.canCreateScopes"],
|
|
204
|
+
[SCOPE_METHODS.SCOPES_DELETE]: ["scopes.canManageScopes"],
|
|
205
|
+
[SCOPE_METHODS.SCOPES_JOIN]: [],
|
|
206
|
+
[SCOPE_METHODS.SCOPES_LEAVE]: [],
|
|
207
|
+
// Session
|
|
208
|
+
[SESSION_METHODS.SESSION_LIST]: [],
|
|
209
|
+
[SESSION_METHODS.SESSION_LOAD]: [],
|
|
210
|
+
[SESSION_METHODS.SESSION_CLOSE]: [],
|
|
211
|
+
// Auth
|
|
212
|
+
[AUTH_METHODS.AUTH_REFRESH]: [],
|
|
213
|
+
// Permissions (system-only, no capability check - enforced by participant type)
|
|
214
|
+
[PERMISSION_METHODS.PERMISSIONS_UPDATE]: [],
|
|
215
|
+
// Federation
|
|
216
|
+
[FEDERATION_METHODS.FEDERATION_CONNECT]: ["federation.canFederate"],
|
|
217
|
+
[FEDERATION_METHODS.FEDERATION_ROUTE]: ["federation.canFederate"]
|
|
218
|
+
};
|
|
219
|
+
function isSuccessResponse(response) {
|
|
220
|
+
return "result" in response;
|
|
221
|
+
}
|
|
222
|
+
function isDirectAddress(address) {
|
|
223
|
+
return typeof address === "object" && "agent" in address && !("system" in address);
|
|
224
|
+
}
|
|
225
|
+
function isFederatedAddress(address) {
|
|
226
|
+
return typeof address === "object" && "system" in address && "agent" in address;
|
|
227
|
+
}
|
|
228
|
+
function isScopeAddress(address) {
|
|
229
|
+
return typeof address === "object" && "scope" in address;
|
|
230
|
+
}
|
|
231
|
+
function isBroadcastAddress(address) {
|
|
232
|
+
return typeof address === "object" && "broadcast" in address;
|
|
233
|
+
}
|
|
234
|
+
function isHierarchicalAddress(address) {
|
|
235
|
+
return typeof address === "object" && ("parent" in address || "children" in address || "ancestors" in address || "descendants" in address || "siblings" in address);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/jsonrpc/index.ts
|
|
239
|
+
function isRequest(message) {
|
|
240
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "id" in message && "method" in message;
|
|
241
|
+
}
|
|
242
|
+
function isNotification(message) {
|
|
243
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "method" in message && !("id" in message);
|
|
244
|
+
}
|
|
245
|
+
function isResponse(message) {
|
|
246
|
+
return typeof message === "object" && message !== null && "jsonrpc" in message && message.jsonrpc === "2.0" && "id" in message && !("method" in message);
|
|
247
|
+
}
|
|
248
|
+
function isErrorResponse(response) {
|
|
249
|
+
return "error" in response;
|
|
250
|
+
}
|
|
251
|
+
function createRequest(id, method, params) {
|
|
252
|
+
const request = {
|
|
253
|
+
jsonrpc: "2.0",
|
|
254
|
+
id,
|
|
255
|
+
method
|
|
256
|
+
};
|
|
257
|
+
if (params !== void 0) {
|
|
258
|
+
request.params = params;
|
|
259
|
+
}
|
|
260
|
+
return request;
|
|
261
|
+
}
|
|
262
|
+
function createNotification(method, params) {
|
|
263
|
+
const notification = {
|
|
264
|
+
jsonrpc: "2.0",
|
|
265
|
+
method
|
|
266
|
+
};
|
|
267
|
+
if (params !== void 0) {
|
|
268
|
+
notification.params = params;
|
|
269
|
+
}
|
|
270
|
+
return notification;
|
|
271
|
+
}
|
|
272
|
+
function createSuccessResponse(id, result) {
|
|
273
|
+
return {
|
|
274
|
+
jsonrpc: "2.0",
|
|
275
|
+
id,
|
|
276
|
+
result
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function createErrorResponse(id, error) {
|
|
280
|
+
return {
|
|
281
|
+
jsonrpc: "2.0",
|
|
282
|
+
id,
|
|
283
|
+
error
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/errors/index.ts
|
|
288
|
+
var MAPRequestError = class _MAPRequestError extends Error {
|
|
289
|
+
code;
|
|
290
|
+
data;
|
|
291
|
+
constructor(code, message, data) {
|
|
292
|
+
super(message);
|
|
293
|
+
this.name = "MAPRequestError";
|
|
294
|
+
this.code = code;
|
|
295
|
+
this.data = data;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Convert to MAP error object
|
|
299
|
+
*/
|
|
300
|
+
toError() {
|
|
301
|
+
const error = {
|
|
302
|
+
code: this.code,
|
|
303
|
+
message: this.message
|
|
304
|
+
};
|
|
305
|
+
if (this.data) {
|
|
306
|
+
error.data = this.data;
|
|
307
|
+
}
|
|
308
|
+
return error;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Convert to JSON-RPC error response
|
|
312
|
+
*/
|
|
313
|
+
toResponse(id) {
|
|
314
|
+
return createErrorResponse(id, this.toError());
|
|
315
|
+
}
|
|
316
|
+
// ==========================================================================
|
|
317
|
+
// Protocol Errors (-32xxx)
|
|
318
|
+
// ==========================================================================
|
|
319
|
+
static parseError(details) {
|
|
320
|
+
return new _MAPRequestError(
|
|
321
|
+
PROTOCOL_ERROR_CODES.PARSE_ERROR,
|
|
322
|
+
details ?? "Parse error",
|
|
323
|
+
{ category: "protocol" }
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
static invalidRequest(details) {
|
|
327
|
+
return new _MAPRequestError(
|
|
328
|
+
PROTOCOL_ERROR_CODES.INVALID_REQUEST,
|
|
329
|
+
details ?? "Invalid request",
|
|
330
|
+
{ category: "protocol" }
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
static methodNotFound(method) {
|
|
334
|
+
return new _MAPRequestError(
|
|
335
|
+
PROTOCOL_ERROR_CODES.METHOD_NOT_FOUND,
|
|
336
|
+
`Method not found: ${method}`,
|
|
337
|
+
{ category: "protocol" }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
static invalidParams(details) {
|
|
341
|
+
return new _MAPRequestError(
|
|
342
|
+
PROTOCOL_ERROR_CODES.INVALID_PARAMS,
|
|
343
|
+
"Invalid params",
|
|
344
|
+
{ category: "protocol", details }
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
static internalError(details) {
|
|
348
|
+
return new _MAPRequestError(
|
|
349
|
+
PROTOCOL_ERROR_CODES.INTERNAL_ERROR,
|
|
350
|
+
details ?? "Internal error",
|
|
351
|
+
{ category: "internal" }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
// ==========================================================================
|
|
355
|
+
// Auth Errors (1xxx)
|
|
356
|
+
// ==========================================================================
|
|
357
|
+
static authRequired() {
|
|
358
|
+
return new _MAPRequestError(
|
|
359
|
+
AUTH_ERROR_CODES.AUTH_REQUIRED,
|
|
360
|
+
"Authentication required",
|
|
361
|
+
{ category: "auth" }
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
static authFailed(details) {
|
|
365
|
+
return new _MAPRequestError(
|
|
366
|
+
AUTH_ERROR_CODES.AUTH_FAILED,
|
|
367
|
+
details ?? "Authentication failed",
|
|
368
|
+
{ category: "auth" }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
static tokenExpired() {
|
|
372
|
+
return new _MAPRequestError(
|
|
373
|
+
AUTH_ERROR_CODES.TOKEN_EXPIRED,
|
|
374
|
+
"Token expired",
|
|
375
|
+
{ category: "auth", retryable: true }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
static permissionDenied(required) {
|
|
379
|
+
return new _MAPRequestError(
|
|
380
|
+
AUTH_ERROR_CODES.PERMISSION_DENIED,
|
|
381
|
+
required ? `Permission denied: ${required}` : "Permission denied",
|
|
382
|
+
{ category: "auth" }
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// ==========================================================================
|
|
386
|
+
// Routing Errors (2xxx)
|
|
387
|
+
// ==========================================================================
|
|
388
|
+
static addressNotFound(address) {
|
|
389
|
+
return new _MAPRequestError(
|
|
390
|
+
ROUTING_ERROR_CODES.ADDRESS_NOT_FOUND,
|
|
391
|
+
`Address not found: ${address}`,
|
|
392
|
+
{ category: "routing" }
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
static agentNotFound(agentId) {
|
|
396
|
+
return new _MAPRequestError(
|
|
397
|
+
ROUTING_ERROR_CODES.AGENT_NOT_FOUND,
|
|
398
|
+
`Agent not found: ${agentId}`,
|
|
399
|
+
{ category: "routing" }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
static scopeNotFound(scopeId) {
|
|
403
|
+
return new _MAPRequestError(
|
|
404
|
+
ROUTING_ERROR_CODES.SCOPE_NOT_FOUND,
|
|
405
|
+
`Scope not found: ${scopeId}`,
|
|
406
|
+
{ category: "routing" }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
static deliveryFailed(details) {
|
|
410
|
+
return new _MAPRequestError(
|
|
411
|
+
ROUTING_ERROR_CODES.DELIVERY_FAILED,
|
|
412
|
+
details ?? "Message delivery failed",
|
|
413
|
+
{ category: "routing", retryable: true }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
static addressAmbiguous(address) {
|
|
417
|
+
return new _MAPRequestError(
|
|
418
|
+
ROUTING_ERROR_CODES.ADDRESS_AMBIGUOUS,
|
|
419
|
+
`Address is ambiguous: ${address}`,
|
|
420
|
+
{ category: "routing" }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
// ==========================================================================
|
|
424
|
+
// Agent Errors (3xxx)
|
|
425
|
+
// ==========================================================================
|
|
426
|
+
static agentExists(agentId) {
|
|
427
|
+
return new _MAPRequestError(
|
|
428
|
+
AGENT_ERROR_CODES.AGENT_EXISTS,
|
|
429
|
+
`Agent already exists: ${agentId}`,
|
|
430
|
+
{ category: "agent" }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
static stateInvalid(currentState, requestedAction) {
|
|
434
|
+
return new _MAPRequestError(
|
|
435
|
+
AGENT_ERROR_CODES.STATE_INVALID,
|
|
436
|
+
`Cannot ${requestedAction} agent in state: ${currentState}`,
|
|
437
|
+
{ category: "agent" }
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
static agentNotResponding(agentId) {
|
|
441
|
+
return new _MAPRequestError(
|
|
442
|
+
AGENT_ERROR_CODES.NOT_RESPONDING,
|
|
443
|
+
`Agent not responding: ${agentId}`,
|
|
444
|
+
{ category: "agent", retryable: true }
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
static agentTerminated(agentId) {
|
|
448
|
+
return new _MAPRequestError(
|
|
449
|
+
AGENT_ERROR_CODES.TERMINATED,
|
|
450
|
+
`Agent terminated: ${agentId}`,
|
|
451
|
+
{ category: "agent" }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
static spawnFailed(details) {
|
|
455
|
+
return new _MAPRequestError(
|
|
456
|
+
AGENT_ERROR_CODES.SPAWN_FAILED,
|
|
457
|
+
details ?? "Failed to spawn agent",
|
|
458
|
+
{ category: "agent" }
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
// ==========================================================================
|
|
462
|
+
// Resource Errors (4xxx)
|
|
463
|
+
// ==========================================================================
|
|
464
|
+
static resourceExhausted(resource) {
|
|
465
|
+
return new _MAPRequestError(
|
|
466
|
+
RESOURCE_ERROR_CODES.EXHAUSTED,
|
|
467
|
+
resource ? `Resource exhausted: ${resource}` : "Resource exhausted",
|
|
468
|
+
{ category: "resource", retryable: true }
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
static rateLimited(retryAfterMs) {
|
|
472
|
+
return new _MAPRequestError(
|
|
473
|
+
RESOURCE_ERROR_CODES.RATE_LIMITED,
|
|
474
|
+
"Rate limited",
|
|
475
|
+
{ category: "resource", retryable: true, retryAfterMs }
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
static quotaExceeded(quota) {
|
|
479
|
+
return new _MAPRequestError(
|
|
480
|
+
RESOURCE_ERROR_CODES.QUOTA_EXCEEDED,
|
|
481
|
+
quota ? `Quota exceeded: ${quota}` : "Quota exceeded",
|
|
482
|
+
{ category: "resource" }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
// ==========================================================================
|
|
486
|
+
// Federation Errors (5xxx)
|
|
487
|
+
// ==========================================================================
|
|
488
|
+
static federationUnavailable(systemId) {
|
|
489
|
+
return new _MAPRequestError(
|
|
490
|
+
FEDERATION_ERROR_CODES.FEDERATION_UNAVAILABLE,
|
|
491
|
+
systemId ? `Federation unavailable: ${systemId}` : "Federation unavailable",
|
|
492
|
+
{ category: "federation", retryable: true }
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
static federationSystemNotFound(systemId) {
|
|
496
|
+
return new _MAPRequestError(
|
|
497
|
+
FEDERATION_ERROR_CODES.FEDERATION_SYSTEM_NOT_FOUND,
|
|
498
|
+
`System not found: ${systemId}`,
|
|
499
|
+
{ category: "federation" }
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
static federationAuthFailed(systemId) {
|
|
503
|
+
return new _MAPRequestError(
|
|
504
|
+
FEDERATION_ERROR_CODES.FEDERATION_AUTH_FAILED,
|
|
505
|
+
`Federation authentication failed: ${systemId}`,
|
|
506
|
+
{ category: "federation" }
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
static federationRouteRejected(systemId, reason) {
|
|
510
|
+
return new _MAPRequestError(
|
|
511
|
+
FEDERATION_ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
512
|
+
reason ? `Route rejected by ${systemId}: ${reason}` : `Route rejected by ${systemId}`,
|
|
513
|
+
{ category: "federation" }
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
// ==========================================================================
|
|
517
|
+
// Utility
|
|
518
|
+
// ==========================================================================
|
|
519
|
+
/**
|
|
520
|
+
* Create from a MAP error object
|
|
521
|
+
*/
|
|
522
|
+
static fromError(error) {
|
|
523
|
+
return new _MAPRequestError(error.code, error.message, error.data);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if this error is retryable
|
|
527
|
+
*/
|
|
528
|
+
get retryable() {
|
|
529
|
+
return this.data?.retryable ?? false;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get retry delay in milliseconds, if specified
|
|
533
|
+
*/
|
|
534
|
+
get retryAfterMs() {
|
|
535
|
+
return this.data?.retryAfterMs;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get error category
|
|
539
|
+
*/
|
|
540
|
+
get category() {
|
|
541
|
+
return this.data?.category;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
var MAPConnectionError = class _MAPConnectionError extends Error {
|
|
545
|
+
constructor(message) {
|
|
546
|
+
super(message);
|
|
547
|
+
this.name = "MAPConnectionError";
|
|
548
|
+
}
|
|
549
|
+
static closed() {
|
|
550
|
+
return new _MAPConnectionError("Connection closed");
|
|
551
|
+
}
|
|
552
|
+
static timeout() {
|
|
553
|
+
return new _MAPConnectionError("Connection timeout");
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var MAPTimeoutError = class extends Error {
|
|
557
|
+
timeoutMs;
|
|
558
|
+
constructor(operation, timeoutMs) {
|
|
559
|
+
super(`Operation timed out after ${timeoutMs}ms: ${operation}`);
|
|
560
|
+
this.name = "MAPTimeoutError";
|
|
561
|
+
this.timeoutMs = timeoutMs;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/stream/index.ts
|
|
566
|
+
function ndJsonStream(readable, writable) {
|
|
567
|
+
const encoder = new TextEncoder();
|
|
568
|
+
const decoder = new TextDecoder();
|
|
569
|
+
let buffer = "";
|
|
570
|
+
const messageReadable = new ReadableStream({
|
|
571
|
+
async start(controller) {
|
|
572
|
+
const reader = readable.getReader();
|
|
573
|
+
try {
|
|
574
|
+
while (true) {
|
|
575
|
+
const { done, value } = await reader.read();
|
|
576
|
+
if (done) {
|
|
577
|
+
if (buffer.trim()) {
|
|
578
|
+
try {
|
|
579
|
+
const message = JSON.parse(buffer.trim());
|
|
580
|
+
controller.enqueue(message);
|
|
581
|
+
} catch {
|
|
582
|
+
console.error("MAP: Failed to parse final message:", buffer);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
controller.close();
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
buffer += decoder.decode(value, { stream: true });
|
|
589
|
+
const lines = buffer.split("\n");
|
|
590
|
+
buffer = lines.pop() ?? "";
|
|
591
|
+
for (const line of lines) {
|
|
592
|
+
const trimmed = line.trim();
|
|
593
|
+
if (trimmed) {
|
|
594
|
+
try {
|
|
595
|
+
const message = JSON.parse(trimmed);
|
|
596
|
+
controller.enqueue(message);
|
|
597
|
+
} catch {
|
|
598
|
+
console.error("MAP: Failed to parse message:", trimmed);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
controller.error(error);
|
|
605
|
+
} finally {
|
|
606
|
+
reader.releaseLock();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
const messageWritable = new WritableStream({
|
|
611
|
+
async write(message) {
|
|
612
|
+
const writer = writable.getWriter();
|
|
613
|
+
try {
|
|
614
|
+
const json = JSON.stringify(message) + "\n";
|
|
615
|
+
await writer.write(encoder.encode(json));
|
|
616
|
+
} finally {
|
|
617
|
+
writer.releaseLock();
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
async close() {
|
|
621
|
+
await writable.close();
|
|
622
|
+
},
|
|
623
|
+
abort(reason) {
|
|
624
|
+
writable.abort(reason);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
readable: messageReadable,
|
|
629
|
+
writable: messageWritable
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function websocketStream(ws) {
|
|
633
|
+
const messageQueue = [];
|
|
634
|
+
let messageResolver = null;
|
|
635
|
+
let closed = false;
|
|
636
|
+
let closeError = null;
|
|
637
|
+
ws.addEventListener("message", (event) => {
|
|
638
|
+
try {
|
|
639
|
+
const message = JSON.parse(event.data);
|
|
640
|
+
if (messageResolver) {
|
|
641
|
+
messageResolver({ value: message, done: false });
|
|
642
|
+
messageResolver = null;
|
|
643
|
+
} else {
|
|
644
|
+
messageQueue.push(message);
|
|
645
|
+
}
|
|
646
|
+
} catch {
|
|
647
|
+
console.error("MAP: Failed to parse WebSocket message:", event.data);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
ws.addEventListener("close", () => {
|
|
651
|
+
closed = true;
|
|
652
|
+
if (messageResolver) {
|
|
653
|
+
messageResolver({ value: void 0, done: true });
|
|
654
|
+
messageResolver = null;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
ws.addEventListener("error", () => {
|
|
658
|
+
closeError = new Error("WebSocket error");
|
|
659
|
+
closed = true;
|
|
660
|
+
if (messageResolver) {
|
|
661
|
+
messageResolver({ value: void 0, done: true });
|
|
662
|
+
messageResolver = null;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
const readable = new ReadableStream({
|
|
666
|
+
async pull(controller) {
|
|
667
|
+
if (messageQueue.length > 0) {
|
|
668
|
+
controller.enqueue(messageQueue.shift());
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (closed) {
|
|
672
|
+
if (closeError) {
|
|
673
|
+
controller.error(closeError);
|
|
674
|
+
} else {
|
|
675
|
+
controller.close();
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
await new Promise((resolve) => {
|
|
680
|
+
messageResolver = resolve;
|
|
681
|
+
}).then((result) => {
|
|
682
|
+
if (result.done) {
|
|
683
|
+
controller.close();
|
|
684
|
+
} else {
|
|
685
|
+
controller.enqueue(result.value);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
const writable = new WritableStream({
|
|
691
|
+
async write(message) {
|
|
692
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
693
|
+
await new Promise((resolve, reject) => {
|
|
694
|
+
const onOpen = () => {
|
|
695
|
+
ws.removeEventListener("error", onError);
|
|
696
|
+
resolve();
|
|
697
|
+
};
|
|
698
|
+
const onError = () => {
|
|
699
|
+
ws.removeEventListener("open", onOpen);
|
|
700
|
+
reject(new Error("WebSocket failed to connect"));
|
|
701
|
+
};
|
|
702
|
+
ws.addEventListener("open", onOpen, { once: true });
|
|
703
|
+
ws.addEventListener("error", onError, { once: true });
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
707
|
+
throw new Error("WebSocket is not open");
|
|
708
|
+
}
|
|
709
|
+
ws.send(JSON.stringify(message));
|
|
710
|
+
},
|
|
711
|
+
close() {
|
|
712
|
+
ws.close();
|
|
713
|
+
},
|
|
714
|
+
abort() {
|
|
715
|
+
ws.close();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
return { readable, writable };
|
|
719
|
+
}
|
|
720
|
+
function createStreamPair() {
|
|
721
|
+
const clientToServer = [];
|
|
722
|
+
const serverToClient = [];
|
|
723
|
+
let clientToServerResolver = null;
|
|
724
|
+
let serverToClientResolver = null;
|
|
725
|
+
let clientToServerClosed = false;
|
|
726
|
+
let serverToClientClosed = false;
|
|
727
|
+
function createReadable(queue, _getResolver, setResolver, isClosed) {
|
|
728
|
+
return new ReadableStream({
|
|
729
|
+
async pull(controller) {
|
|
730
|
+
if (queue.length > 0) {
|
|
731
|
+
controller.enqueue(queue.shift());
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (isClosed()) {
|
|
735
|
+
controller.close();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const message = await new Promise((resolve) => {
|
|
739
|
+
setResolver((msg) => {
|
|
740
|
+
setResolver(null);
|
|
741
|
+
resolve(msg);
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
if (message === null) {
|
|
745
|
+
controller.close();
|
|
746
|
+
} else {
|
|
747
|
+
controller.enqueue(message);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function createWritable(queue, getResolver, setClosed) {
|
|
753
|
+
return new WritableStream({
|
|
754
|
+
write(message) {
|
|
755
|
+
const resolver = getResolver();
|
|
756
|
+
if (resolver) {
|
|
757
|
+
resolver(message);
|
|
758
|
+
} else {
|
|
759
|
+
queue.push(message);
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
close() {
|
|
763
|
+
setClosed();
|
|
764
|
+
const resolver = getResolver();
|
|
765
|
+
if (resolver) {
|
|
766
|
+
resolver(null);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const clientStream = {
|
|
772
|
+
// Client writes to server
|
|
773
|
+
writable: createWritable(
|
|
774
|
+
clientToServer,
|
|
775
|
+
() => clientToServerResolver,
|
|
776
|
+
() => {
|
|
777
|
+
clientToServerClosed = true;
|
|
778
|
+
}
|
|
779
|
+
),
|
|
780
|
+
// Client reads from server
|
|
781
|
+
readable: createReadable(
|
|
782
|
+
serverToClient,
|
|
783
|
+
() => serverToClientResolver,
|
|
784
|
+
(r) => {
|
|
785
|
+
serverToClientResolver = r;
|
|
786
|
+
},
|
|
787
|
+
() => serverToClientClosed
|
|
788
|
+
)
|
|
789
|
+
};
|
|
790
|
+
const serverStream = {
|
|
791
|
+
// Server writes to client
|
|
792
|
+
writable: createWritable(
|
|
793
|
+
serverToClient,
|
|
794
|
+
() => serverToClientResolver,
|
|
795
|
+
() => {
|
|
796
|
+
serverToClientClosed = true;
|
|
797
|
+
}
|
|
798
|
+
),
|
|
799
|
+
// Server reads from client
|
|
800
|
+
readable: createReadable(
|
|
801
|
+
clientToServer,
|
|
802
|
+
() => clientToServerResolver,
|
|
803
|
+
(r) => {
|
|
804
|
+
clientToServerResolver = r;
|
|
805
|
+
},
|
|
806
|
+
() => clientToServerClosed
|
|
807
|
+
)
|
|
808
|
+
};
|
|
809
|
+
return [clientStream, serverStream];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/subscription/index.ts
|
|
813
|
+
var Subscription = class {
|
|
814
|
+
id;
|
|
815
|
+
filter;
|
|
816
|
+
#eventHandlers = /* @__PURE__ */ new Set();
|
|
817
|
+
#overflowHandlers = /* @__PURE__ */ new Set();
|
|
818
|
+
#eventQueue = [];
|
|
819
|
+
#bufferSize;
|
|
820
|
+
#unsubscribe;
|
|
821
|
+
#sendAck;
|
|
822
|
+
// Deduplication tracking
|
|
823
|
+
#seenEventIds = /* @__PURE__ */ new Set();
|
|
824
|
+
#seenEventIdOrder = [];
|
|
825
|
+
// For LRU eviction
|
|
826
|
+
#maxSeenEventIds;
|
|
827
|
+
#eventResolver = null;
|
|
828
|
+
#pauseResolver = null;
|
|
829
|
+
#state = "active";
|
|
830
|
+
#lastSequenceNumber = -1;
|
|
831
|
+
#lastEventId;
|
|
832
|
+
#lastTimestamp;
|
|
833
|
+
// Overflow tracking
|
|
834
|
+
#totalDropped = 0;
|
|
835
|
+
#oldestDroppedId;
|
|
836
|
+
#newestDroppedId;
|
|
837
|
+
// Ack support
|
|
838
|
+
#serverSupportsAck = false;
|
|
839
|
+
constructor(id, unsubscribe, options = {}, sendAck) {
|
|
840
|
+
this.id = id;
|
|
841
|
+
this.filter = options.filter;
|
|
842
|
+
this.#bufferSize = options.bufferSize ?? 1e3;
|
|
843
|
+
this.#maxSeenEventIds = options.maxSeenEventIds ?? 1e4;
|
|
844
|
+
this.#unsubscribe = unsubscribe;
|
|
845
|
+
this.#sendAck = sendAck;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Current subscription state
|
|
849
|
+
*/
|
|
850
|
+
get state() {
|
|
851
|
+
return this.#state;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Whether the subscription is closed
|
|
855
|
+
*/
|
|
856
|
+
get isClosed() {
|
|
857
|
+
return this.#state === "closed";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Whether the subscription is paused
|
|
861
|
+
*/
|
|
862
|
+
get isPaused() {
|
|
863
|
+
return this.#state === "paused";
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Last received sequence number (for ordering verification)
|
|
867
|
+
*/
|
|
868
|
+
get lastSequenceNumber() {
|
|
869
|
+
return this.#lastSequenceNumber;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Last received eventId (for replay positioning)
|
|
873
|
+
*/
|
|
874
|
+
get lastEventId() {
|
|
875
|
+
return this.#lastEventId;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Last received server timestamp
|
|
879
|
+
*/
|
|
880
|
+
get lastTimestamp() {
|
|
881
|
+
return this.#lastTimestamp;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Number of events currently buffered
|
|
885
|
+
*/
|
|
886
|
+
get bufferedCount() {
|
|
887
|
+
return this.#eventQueue.length;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Number of eventIds being tracked for deduplication
|
|
891
|
+
*/
|
|
892
|
+
get trackedEventIdCount() {
|
|
893
|
+
return this.#seenEventIds.size;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Total number of events dropped due to buffer overflow
|
|
897
|
+
*/
|
|
898
|
+
get totalDropped() {
|
|
899
|
+
return this.#totalDropped;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Whether the server supports acknowledgments
|
|
903
|
+
*/
|
|
904
|
+
get supportsAck() {
|
|
905
|
+
return this.#serverSupportsAck && !!this.#sendAck;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Pause event delivery from the async iterator.
|
|
909
|
+
* Events are still buffered but not yielded until resume() is called.
|
|
910
|
+
* Event handlers (on('event', ...)) still receive events while paused.
|
|
911
|
+
*/
|
|
912
|
+
pause() {
|
|
913
|
+
if (this.#state === "closed") return;
|
|
914
|
+
this.#state = "paused";
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Resume event delivery from the async iterator.
|
|
918
|
+
* Any events buffered during pause will be yielded.
|
|
919
|
+
*/
|
|
920
|
+
resume() {
|
|
921
|
+
if (this.#state === "closed") return;
|
|
922
|
+
this.#state = "active";
|
|
923
|
+
if (this.#pauseResolver) {
|
|
924
|
+
this.#pauseResolver();
|
|
925
|
+
this.#pauseResolver = null;
|
|
926
|
+
}
|
|
927
|
+
if (this.#eventResolver && this.#eventQueue.length > 0) {
|
|
928
|
+
const event = this.#eventQueue.shift();
|
|
929
|
+
this.#eventResolver(event);
|
|
930
|
+
this.#eventResolver = null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Acknowledge events up to a sequence number.
|
|
935
|
+
* No-op if server doesn't support acks.
|
|
936
|
+
*
|
|
937
|
+
* @param upToSequence - Acknowledge all events up to and including this sequence.
|
|
938
|
+
* If omitted, acknowledges up to lastSequenceNumber.
|
|
939
|
+
*/
|
|
940
|
+
ack(upToSequence) {
|
|
941
|
+
if (!this.supportsAck) return;
|
|
942
|
+
const seq = upToSequence ?? this.#lastSequenceNumber;
|
|
943
|
+
if (seq < 0) return;
|
|
944
|
+
this.#sendAck({
|
|
945
|
+
subscriptionId: this.id,
|
|
946
|
+
upToSequence: seq
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
on(type, handler) {
|
|
950
|
+
if (type === "event") {
|
|
951
|
+
this.#eventHandlers.add(handler);
|
|
952
|
+
} else if (type === "overflow") {
|
|
953
|
+
this.#overflowHandlers.add(handler);
|
|
954
|
+
}
|
|
955
|
+
return this;
|
|
956
|
+
}
|
|
957
|
+
off(type, handler) {
|
|
958
|
+
if (type === "event") {
|
|
959
|
+
this.#eventHandlers.delete(handler);
|
|
960
|
+
} else if (type === "overflow") {
|
|
961
|
+
this.#overflowHandlers.delete(handler);
|
|
962
|
+
}
|
|
963
|
+
return this;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Register a one-time event handler
|
|
967
|
+
*/
|
|
968
|
+
once(type, handler) {
|
|
969
|
+
if (type === "event") {
|
|
970
|
+
const wrapper = (event) => {
|
|
971
|
+
this.off("event", wrapper);
|
|
972
|
+
handler(event);
|
|
973
|
+
};
|
|
974
|
+
this.on("event", wrapper);
|
|
975
|
+
}
|
|
976
|
+
return this;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Unsubscribe and close the subscription
|
|
980
|
+
*/
|
|
981
|
+
async unsubscribe() {
|
|
982
|
+
if (this.#state === "closed") return;
|
|
983
|
+
this.#state = "closed";
|
|
984
|
+
if (this.#eventResolver) {
|
|
985
|
+
this.#eventResolver(null);
|
|
986
|
+
this.#eventResolver = null;
|
|
987
|
+
}
|
|
988
|
+
if (this.#pauseResolver) {
|
|
989
|
+
this.#pauseResolver();
|
|
990
|
+
this.#pauseResolver = null;
|
|
991
|
+
}
|
|
992
|
+
this.#eventHandlers.clear();
|
|
993
|
+
this.#overflowHandlers.clear();
|
|
994
|
+
this.#seenEventIds.clear();
|
|
995
|
+
this.#seenEventIdOrder.length = 0;
|
|
996
|
+
await this.#unsubscribe();
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Set whether server supports acknowledgments.
|
|
1000
|
+
* Called by connection after capability negotiation.
|
|
1001
|
+
* @internal
|
|
1002
|
+
*/
|
|
1003
|
+
_setServerSupportsAck(supports) {
|
|
1004
|
+
this.#serverSupportsAck = supports;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Push an event to the subscription (called by connection)
|
|
1008
|
+
* @internal
|
|
1009
|
+
*/
|
|
1010
|
+
_pushEvent(params) {
|
|
1011
|
+
if (this.#state === "closed") return;
|
|
1012
|
+
const { sequenceNumber, eventId, timestamp, event } = params;
|
|
1013
|
+
if (eventId) {
|
|
1014
|
+
if (this.#seenEventIds.has(eventId)) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
this.#seenEventIds.add(eventId);
|
|
1018
|
+
this.#seenEventIdOrder.push(eventId);
|
|
1019
|
+
while (this.#seenEventIds.size > this.#maxSeenEventIds) {
|
|
1020
|
+
const oldestId = this.#seenEventIdOrder.shift();
|
|
1021
|
+
if (oldestId) {
|
|
1022
|
+
this.#seenEventIds.delete(oldestId);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
this.#lastEventId = eventId;
|
|
1026
|
+
}
|
|
1027
|
+
if (timestamp !== void 0) {
|
|
1028
|
+
this.#lastTimestamp = timestamp;
|
|
1029
|
+
}
|
|
1030
|
+
if (this.#lastSequenceNumber >= 0 && sequenceNumber !== this.#lastSequenceNumber + 1) {
|
|
1031
|
+
console.warn(
|
|
1032
|
+
`MAP: Subscription ${this.id} sequence gap: expected ${this.#lastSequenceNumber + 1}, got ${sequenceNumber}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
this.#lastSequenceNumber = sequenceNumber;
|
|
1036
|
+
for (const handler of this.#eventHandlers) {
|
|
1037
|
+
try {
|
|
1038
|
+
handler(event);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
console.error("MAP: Event handler error:", error);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (this.#eventResolver && this.#state === "active") {
|
|
1044
|
+
this.#eventResolver(event);
|
|
1045
|
+
this.#eventResolver = null;
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (this.#eventQueue.length < this.#bufferSize) {
|
|
1049
|
+
this.#eventQueue.push(event);
|
|
1050
|
+
} else {
|
|
1051
|
+
this.#totalDropped++;
|
|
1052
|
+
if (eventId) {
|
|
1053
|
+
if (this.#oldestDroppedId === void 0) {
|
|
1054
|
+
this.#oldestDroppedId = eventId;
|
|
1055
|
+
}
|
|
1056
|
+
this.#newestDroppedId = eventId;
|
|
1057
|
+
}
|
|
1058
|
+
const info = {
|
|
1059
|
+
eventsDropped: 1,
|
|
1060
|
+
oldestDroppedId: this.#oldestDroppedId,
|
|
1061
|
+
newestDroppedId: this.#newestDroppedId,
|
|
1062
|
+
timestamp: Date.now(),
|
|
1063
|
+
totalDropped: this.#totalDropped
|
|
1064
|
+
};
|
|
1065
|
+
for (const handler of this.#overflowHandlers) {
|
|
1066
|
+
try {
|
|
1067
|
+
handler(info);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
console.error("MAP: Overflow handler error:", error);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
console.warn(`MAP: Subscription ${this.id} buffer full, dropping event`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Mark the subscription as closed (called by connection)
|
|
1077
|
+
* @internal
|
|
1078
|
+
*/
|
|
1079
|
+
_close() {
|
|
1080
|
+
this.#state = "closed";
|
|
1081
|
+
if (this.#eventResolver) {
|
|
1082
|
+
this.#eventResolver(null);
|
|
1083
|
+
this.#eventResolver = null;
|
|
1084
|
+
}
|
|
1085
|
+
if (this.#pauseResolver) {
|
|
1086
|
+
this.#pauseResolver();
|
|
1087
|
+
this.#pauseResolver = null;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Async iterator implementation
|
|
1092
|
+
*/
|
|
1093
|
+
async *[Symbol.asyncIterator]() {
|
|
1094
|
+
while (!this.isClosed) {
|
|
1095
|
+
while (this.isPaused) {
|
|
1096
|
+
await new Promise((resolve) => {
|
|
1097
|
+
this.#pauseResolver = resolve;
|
|
1098
|
+
});
|
|
1099
|
+
if (this.isClosed) {
|
|
1100
|
+
while (this.#eventQueue.length > 0) {
|
|
1101
|
+
yield this.#eventQueue.shift();
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (this.#eventQueue.length > 0) {
|
|
1107
|
+
yield this.#eventQueue.shift();
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const event = await new Promise((resolve) => {
|
|
1111
|
+
this.#eventResolver = resolve;
|
|
1112
|
+
});
|
|
1113
|
+
if (event === null) {
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
yield event;
|
|
1117
|
+
}
|
|
1118
|
+
while (this.#eventQueue.length > 0) {
|
|
1119
|
+
yield this.#eventQueue.shift();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
function createSubscription(id, unsubscribe, options, sendAck) {
|
|
1124
|
+
return new Subscription(id, unsubscribe, options, sendAck);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/connection/base.ts
|
|
1128
|
+
var BaseConnection = class {
|
|
1129
|
+
#stream;
|
|
1130
|
+
#pendingResponses = /* @__PURE__ */ new Map();
|
|
1131
|
+
#abortController = new AbortController();
|
|
1132
|
+
#closedPromise;
|
|
1133
|
+
#defaultTimeout;
|
|
1134
|
+
#stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
1135
|
+
#nextRequestId = 1;
|
|
1136
|
+
#writeQueue = Promise.resolve();
|
|
1137
|
+
#requestHandler = null;
|
|
1138
|
+
#notificationHandler = null;
|
|
1139
|
+
#closed = false;
|
|
1140
|
+
#closeResolver;
|
|
1141
|
+
#state = "initial";
|
|
1142
|
+
constructor(stream, options = {}) {
|
|
1143
|
+
this.#stream = stream;
|
|
1144
|
+
this.#defaultTimeout = options.defaultTimeout ?? 3e4;
|
|
1145
|
+
this.#closedPromise = new Promise((resolve) => {
|
|
1146
|
+
this.#closeResolver = resolve;
|
|
1147
|
+
});
|
|
1148
|
+
void this.#startReceiving();
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* AbortSignal that triggers when the connection closes.
|
|
1152
|
+
* Useful for cancelling operations tied to this connection.
|
|
1153
|
+
*/
|
|
1154
|
+
get signal() {
|
|
1155
|
+
return this.#abortController.signal;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Promise that resolves when the connection is closed.
|
|
1159
|
+
*/
|
|
1160
|
+
get closed() {
|
|
1161
|
+
return this.#closedPromise;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Whether the connection is closed
|
|
1165
|
+
*/
|
|
1166
|
+
get isClosed() {
|
|
1167
|
+
return this.#closed;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Set the handler for incoming requests
|
|
1171
|
+
*/
|
|
1172
|
+
setRequestHandler(handler) {
|
|
1173
|
+
this.#requestHandler = handler;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Set the handler for incoming notifications
|
|
1177
|
+
*/
|
|
1178
|
+
setNotificationHandler(handler) {
|
|
1179
|
+
this.#notificationHandler = handler;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Current connection state
|
|
1183
|
+
*/
|
|
1184
|
+
get state() {
|
|
1185
|
+
return this.#state;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Register a handler for state changes.
|
|
1189
|
+
*
|
|
1190
|
+
* @param handler - Function called when state changes
|
|
1191
|
+
* @returns Unsubscribe function to remove the handler
|
|
1192
|
+
*/
|
|
1193
|
+
onStateChange(handler) {
|
|
1194
|
+
this.#stateChangeHandlers.add(handler);
|
|
1195
|
+
return () => this.#stateChangeHandlers.delete(handler);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Transition to a new state and notify handlers.
|
|
1199
|
+
* @internal
|
|
1200
|
+
*/
|
|
1201
|
+
_transitionTo(newState) {
|
|
1202
|
+
if (this.#state === newState) return;
|
|
1203
|
+
const oldState = this.#state;
|
|
1204
|
+
this.#state = newState;
|
|
1205
|
+
for (const handler of this.#stateChangeHandlers) {
|
|
1206
|
+
try {
|
|
1207
|
+
handler(newState, oldState);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.error("MAP: State change handler error:", error);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Reconnect with a new stream.
|
|
1215
|
+
*
|
|
1216
|
+
* This method is used by role-specific connections to replace the
|
|
1217
|
+
* underlying transport after a disconnect.
|
|
1218
|
+
*
|
|
1219
|
+
* @param newStream - The new stream to use
|
|
1220
|
+
* @throws If the connection is permanently closed
|
|
1221
|
+
*/
|
|
1222
|
+
async reconnect(newStream) {
|
|
1223
|
+
if (this.#state === "closed") {
|
|
1224
|
+
throw new Error("Cannot reconnect a permanently closed connection");
|
|
1225
|
+
}
|
|
1226
|
+
this.#stream = newStream;
|
|
1227
|
+
this.#closed = false;
|
|
1228
|
+
this.#writeQueue = Promise.resolve();
|
|
1229
|
+
void this.#startReceiving();
|
|
1230
|
+
this._transitionTo("connected");
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Send a request and wait for response
|
|
1234
|
+
*/
|
|
1235
|
+
async sendRequest(method, params, options) {
|
|
1236
|
+
if (this.#closed) {
|
|
1237
|
+
throw MAPConnectionError.closed();
|
|
1238
|
+
}
|
|
1239
|
+
const id = this.#nextRequestId++;
|
|
1240
|
+
const request = createRequest(id, method, params);
|
|
1241
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
1242
|
+
const pending = { resolve, reject };
|
|
1243
|
+
const timeout = options?.timeout ?? this.#defaultTimeout;
|
|
1244
|
+
if (timeout > 0) {
|
|
1245
|
+
pending.timeoutId = setTimeout(() => {
|
|
1246
|
+
this.#pendingResponses.delete(id);
|
|
1247
|
+
reject(new MAPTimeoutError(method, timeout));
|
|
1248
|
+
}, timeout);
|
|
1249
|
+
}
|
|
1250
|
+
this.#pendingResponses.set(id, pending);
|
|
1251
|
+
});
|
|
1252
|
+
await this.#sendMessage(request);
|
|
1253
|
+
return responsePromise;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Send a notification (no response expected)
|
|
1257
|
+
*/
|
|
1258
|
+
async sendNotification(method, params) {
|
|
1259
|
+
if (this.#closed) {
|
|
1260
|
+
throw MAPConnectionError.closed();
|
|
1261
|
+
}
|
|
1262
|
+
const notification = createNotification(method, params);
|
|
1263
|
+
await this.#sendMessage(notification);
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Send a response to a request
|
|
1267
|
+
*/
|
|
1268
|
+
async sendResponse(id, result) {
|
|
1269
|
+
if (this.#closed) {
|
|
1270
|
+
throw MAPConnectionError.closed();
|
|
1271
|
+
}
|
|
1272
|
+
const response = createSuccessResponse(id, result);
|
|
1273
|
+
await this.#sendMessage(response);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Send an error response to a request
|
|
1277
|
+
*/
|
|
1278
|
+
async sendErrorResponse(id, error) {
|
|
1279
|
+
if (this.#closed) {
|
|
1280
|
+
throw MAPConnectionError.closed();
|
|
1281
|
+
}
|
|
1282
|
+
const response = createErrorResponse(id, error);
|
|
1283
|
+
await this.#sendMessage(response);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Close the connection
|
|
1287
|
+
*/
|
|
1288
|
+
async close() {
|
|
1289
|
+
if (this.#closed) return;
|
|
1290
|
+
this.#closed = true;
|
|
1291
|
+
this._transitionTo("closed");
|
|
1292
|
+
this.#abortController.abort();
|
|
1293
|
+
for (const [, pending] of this.#pendingResponses) {
|
|
1294
|
+
if (pending.timeoutId) {
|
|
1295
|
+
clearTimeout(pending.timeoutId);
|
|
1296
|
+
}
|
|
1297
|
+
pending.reject(MAPConnectionError.closed());
|
|
1298
|
+
}
|
|
1299
|
+
this.#pendingResponses.clear();
|
|
1300
|
+
try {
|
|
1301
|
+
const writer = this.#stream.writable.getWriter();
|
|
1302
|
+
await writer.close();
|
|
1303
|
+
writer.releaseLock();
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
this.#closeResolver();
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Start receiving messages from the stream
|
|
1310
|
+
*/
|
|
1311
|
+
async #startReceiving() {
|
|
1312
|
+
const reader = this.#stream.readable.getReader();
|
|
1313
|
+
try {
|
|
1314
|
+
while (!this.#closed) {
|
|
1315
|
+
const { done, value } = await reader.read();
|
|
1316
|
+
if (done) {
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
await this.#handleMessage(value);
|
|
1320
|
+
}
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (!this.#closed) {
|
|
1323
|
+
console.error("MAP: Error receiving message:", error);
|
|
1324
|
+
}
|
|
1325
|
+
} finally {
|
|
1326
|
+
reader.releaseLock();
|
|
1327
|
+
await this.close();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Handle an incoming message
|
|
1332
|
+
*/
|
|
1333
|
+
async #handleMessage(message) {
|
|
1334
|
+
try {
|
|
1335
|
+
if (isRequest(message)) {
|
|
1336
|
+
await this.#handleRequest(message);
|
|
1337
|
+
} else if (isNotification(message)) {
|
|
1338
|
+
await this.#handleNotification(message);
|
|
1339
|
+
} else if (isResponse(message)) {
|
|
1340
|
+
this.#handleResponse(message);
|
|
1341
|
+
} else {
|
|
1342
|
+
console.error("MAP: Unknown message type:", message);
|
|
1343
|
+
}
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.error("MAP: Error handling message:", error);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Handle an incoming request
|
|
1350
|
+
*/
|
|
1351
|
+
async #handleRequest(request) {
|
|
1352
|
+
const { id, method, params } = request;
|
|
1353
|
+
if (!this.#requestHandler) {
|
|
1354
|
+
await this.sendErrorResponse(
|
|
1355
|
+
id,
|
|
1356
|
+
MAPRequestError.methodNotFound(method).toError()
|
|
1357
|
+
);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
const result = await this.#requestHandler(method, params);
|
|
1362
|
+
await this.sendResponse(id, result ?? null);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error instanceof MAPRequestError) {
|
|
1365
|
+
await this.sendErrorResponse(id, error.toError());
|
|
1366
|
+
} else {
|
|
1367
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1368
|
+
await this.sendErrorResponse(
|
|
1369
|
+
id,
|
|
1370
|
+
MAPRequestError.internalError(message).toError()
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Handle an incoming notification
|
|
1377
|
+
*/
|
|
1378
|
+
async #handleNotification(notification) {
|
|
1379
|
+
const { method, params } = notification;
|
|
1380
|
+
if (!this.#notificationHandler) {
|
|
1381
|
+
console.warn("MAP: No notification handler for:", method);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
await this.#notificationHandler(method, params);
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
console.error("MAP: Error handling notification:", method, error);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Handle an incoming response
|
|
1392
|
+
*/
|
|
1393
|
+
#handleResponse(response) {
|
|
1394
|
+
const { id } = response;
|
|
1395
|
+
const pending = this.#pendingResponses.get(id);
|
|
1396
|
+
if (!pending) {
|
|
1397
|
+
console.warn("MAP: Received response for unknown request:", id);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
this.#pendingResponses.delete(id);
|
|
1401
|
+
if (pending.timeoutId) {
|
|
1402
|
+
clearTimeout(pending.timeoutId);
|
|
1403
|
+
}
|
|
1404
|
+
if (isErrorResponse(response)) {
|
|
1405
|
+
pending.reject(MAPRequestError.fromError(response.error));
|
|
1406
|
+
} else {
|
|
1407
|
+
pending.resolve(response.result);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Send a message through the stream with write queue serialization
|
|
1412
|
+
*/
|
|
1413
|
+
async #sendMessage(message) {
|
|
1414
|
+
this.#writeQueue = this.#writeQueue.then(async () => {
|
|
1415
|
+
if (this.#closed) return;
|
|
1416
|
+
const writer = this.#stream.writable.getWriter();
|
|
1417
|
+
try {
|
|
1418
|
+
await writer.write(message);
|
|
1419
|
+
} finally {
|
|
1420
|
+
writer.releaseLock();
|
|
1421
|
+
}
|
|
1422
|
+
}).catch((error) => {
|
|
1423
|
+
console.error("MAP: Write error:", error);
|
|
1424
|
+
});
|
|
1425
|
+
return this.#writeQueue;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
1429
|
+
var ENCODING_LEN = ENCODING.length;
|
|
1430
|
+
function ulidTimestamp(id) {
|
|
1431
|
+
if (id.length !== 26) {
|
|
1432
|
+
throw new Error(`Invalid ULID: expected 26 characters, got ${id.length}`);
|
|
1433
|
+
}
|
|
1434
|
+
let time = 0;
|
|
1435
|
+
for (let i = 0; i < 10; i++) {
|
|
1436
|
+
const char = id[i].toUpperCase();
|
|
1437
|
+
const idx = ENCODING.indexOf(char);
|
|
1438
|
+
if (idx === -1) {
|
|
1439
|
+
throw new Error(`Invalid ULID character: ${char}`);
|
|
1440
|
+
}
|
|
1441
|
+
time = time * ENCODING_LEN + idx;
|
|
1442
|
+
}
|
|
1443
|
+
return time;
|
|
1444
|
+
}
|
|
1445
|
+
function compareUlid(a, b) {
|
|
1446
|
+
return a.localeCompare(b);
|
|
1447
|
+
}
|
|
1448
|
+
function isValidUlid(id) {
|
|
1449
|
+
if (typeof id !== "string" || id.length !== 26) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
for (let i = 0; i < 26; i++) {
|
|
1453
|
+
if (ENCODING.indexOf(id[i].toUpperCase()) === -1) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/utils/retry.ts
|
|
1461
|
+
var DEFAULT_RETRY_POLICY = {
|
|
1462
|
+
maxRetries: 10,
|
|
1463
|
+
baseDelayMs: 1e3,
|
|
1464
|
+
maxDelayMs: 3e4,
|
|
1465
|
+
jitter: true
|
|
1466
|
+
};
|
|
1467
|
+
function calculateDelay(attempt, policy) {
|
|
1468
|
+
let delay = Math.min(
|
|
1469
|
+
policy.baseDelayMs * Math.pow(2, attempt - 1),
|
|
1470
|
+
policy.maxDelayMs
|
|
1471
|
+
);
|
|
1472
|
+
if (policy.jitter) {
|
|
1473
|
+
delay = delay * (0.5 + Math.random());
|
|
1474
|
+
}
|
|
1475
|
+
return Math.floor(delay);
|
|
1476
|
+
}
|
|
1477
|
+
async function withRetry(operation, policy = DEFAULT_RETRY_POLICY, callbacks) {
|
|
1478
|
+
let lastError;
|
|
1479
|
+
for (let attempt = 1; attempt <= policy.maxRetries + 1; attempt++) {
|
|
1480
|
+
try {
|
|
1481
|
+
const result = await operation();
|
|
1482
|
+
callbacks?.onSuccess?.(result, attempt);
|
|
1483
|
+
return result;
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
lastError = error;
|
|
1486
|
+
const isRetryable = policy.isRetryable?.(lastError) ?? true;
|
|
1487
|
+
const hasMoreAttempts = attempt <= policy.maxRetries;
|
|
1488
|
+
if (!isRetryable || !hasMoreAttempts) {
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
const delay = calculateDelay(attempt, policy);
|
|
1492
|
+
callbacks?.onRetry?.({
|
|
1493
|
+
attempt,
|
|
1494
|
+
nextDelayMs: delay,
|
|
1495
|
+
lastError
|
|
1496
|
+
});
|
|
1497
|
+
await sleep(delay);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
callbacks?.onFailure?.(lastError, policy.maxRetries + 1);
|
|
1501
|
+
throw lastError;
|
|
1502
|
+
}
|
|
1503
|
+
function retryable(fn, policy = DEFAULT_RETRY_POLICY) {
|
|
1504
|
+
return (...args) => withRetry(() => fn(...args), policy);
|
|
1505
|
+
}
|
|
1506
|
+
function createRetryPolicy(options = {}) {
|
|
1507
|
+
return { ...DEFAULT_RETRY_POLICY, ...options };
|
|
1508
|
+
}
|
|
1509
|
+
function sleep(ms) {
|
|
1510
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/utils/causal-buffer.ts
|
|
1514
|
+
var CausalEventBuffer = class {
|
|
1515
|
+
#options;
|
|
1516
|
+
/** Events seen (by eventId) - used to check if predecessors exist */
|
|
1517
|
+
#seen = /* @__PURE__ */ new Set();
|
|
1518
|
+
/** Events waiting for predecessors */
|
|
1519
|
+
#pending = /* @__PURE__ */ new Map();
|
|
1520
|
+
/** Map from eventId to events waiting for it */
|
|
1521
|
+
#waitingFor = /* @__PURE__ */ new Map();
|
|
1522
|
+
constructor(options = {}) {
|
|
1523
|
+
this.#options = {
|
|
1524
|
+
maxWaitTime: options.maxWaitTime ?? 5e3,
|
|
1525
|
+
maxBufferSize: options.maxBufferSize ?? 1e3,
|
|
1526
|
+
onForcedRelease: options.onForcedRelease
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Push an event into the buffer.
|
|
1531
|
+
*
|
|
1532
|
+
* @param event - The event to buffer
|
|
1533
|
+
* @returns Events that are ready to be processed (in causal order)
|
|
1534
|
+
*/
|
|
1535
|
+
push(event) {
|
|
1536
|
+
const ready = [];
|
|
1537
|
+
if (this.#seen.has(event.eventId)) {
|
|
1538
|
+
return { ready, pending: this.#pending.size };
|
|
1539
|
+
}
|
|
1540
|
+
this.#seen.add(event.eventId);
|
|
1541
|
+
if (!event.receivedAt) {
|
|
1542
|
+
event = { ...event, receivedAt: Date.now() };
|
|
1543
|
+
}
|
|
1544
|
+
const missingPredecessors = this.#getMissingPredecessors(event);
|
|
1545
|
+
if (missingPredecessors.length === 0) {
|
|
1546
|
+
ready.push(event);
|
|
1547
|
+
this.#releaseWaiting(event.eventId, ready);
|
|
1548
|
+
} else {
|
|
1549
|
+
this.#pending.set(event.eventId, event);
|
|
1550
|
+
for (const predecessorId of missingPredecessors) {
|
|
1551
|
+
if (!this.#waitingFor.has(predecessorId)) {
|
|
1552
|
+
this.#waitingFor.set(predecessorId, /* @__PURE__ */ new Set());
|
|
1553
|
+
}
|
|
1554
|
+
this.#waitingFor.get(predecessorId).add(event.eventId);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
this.#handleBufferOverflow(ready);
|
|
1558
|
+
this.#handleTimeouts(ready);
|
|
1559
|
+
return { ready, pending: this.#pending.size };
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Get the number of events waiting for predecessors
|
|
1563
|
+
*/
|
|
1564
|
+
get pendingCount() {
|
|
1565
|
+
return this.#pending.size;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Get the number of unique events seen
|
|
1569
|
+
*/
|
|
1570
|
+
get seenCount() {
|
|
1571
|
+
return this.#seen.size;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Check if a specific event has been seen
|
|
1575
|
+
*/
|
|
1576
|
+
hasSeen(eventId) {
|
|
1577
|
+
return this.#seen.has(eventId);
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Force release all pending events, regardless of missing predecessors.
|
|
1581
|
+
* Useful for cleanup or when you know no more events are coming.
|
|
1582
|
+
*
|
|
1583
|
+
* @returns All pending events in the order they would be released
|
|
1584
|
+
*/
|
|
1585
|
+
flush() {
|
|
1586
|
+
const ready = [];
|
|
1587
|
+
const pendingList = Array.from(this.#pending.values()).sort(
|
|
1588
|
+
(a, b) => (a.receivedAt ?? 0) - (b.receivedAt ?? 0)
|
|
1589
|
+
);
|
|
1590
|
+
for (const event of pendingList) {
|
|
1591
|
+
const missingPredecessors = this.#getMissingPredecessors(event);
|
|
1592
|
+
if (missingPredecessors.length > 0) {
|
|
1593
|
+
this.#options.onForcedRelease?.(event, missingPredecessors);
|
|
1594
|
+
}
|
|
1595
|
+
ready.push(event);
|
|
1596
|
+
}
|
|
1597
|
+
this.#pending.clear();
|
|
1598
|
+
this.#waitingFor.clear();
|
|
1599
|
+
return ready;
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Clear all state (seen events, pending events)
|
|
1603
|
+
*/
|
|
1604
|
+
clear() {
|
|
1605
|
+
this.#seen.clear();
|
|
1606
|
+
this.#pending.clear();
|
|
1607
|
+
this.#waitingFor.clear();
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Get missing predecessors for an event.
|
|
1611
|
+
* A predecessor is considered "missing" if it hasn't been released yet
|
|
1612
|
+
* (either not seen at all, or seen but still pending).
|
|
1613
|
+
*/
|
|
1614
|
+
#getMissingPredecessors(event) {
|
|
1615
|
+
if (!event.causedBy || event.causedBy.length === 0) {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
return event.causedBy.filter((predecessorId) => {
|
|
1619
|
+
return !this.#seen.has(predecessorId) || this.#pending.has(predecessorId);
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Release events that were waiting for a specific predecessor
|
|
1624
|
+
*/
|
|
1625
|
+
#releaseWaiting(predecessorId, ready) {
|
|
1626
|
+
const waitingEventIds = this.#waitingFor.get(predecessorId);
|
|
1627
|
+
if (!waitingEventIds) return;
|
|
1628
|
+
this.#waitingFor.delete(predecessorId);
|
|
1629
|
+
for (const waitingEventId of waitingEventIds) {
|
|
1630
|
+
const waitingEvent = this.#pending.get(waitingEventId);
|
|
1631
|
+
if (!waitingEvent) continue;
|
|
1632
|
+
const stillMissing = this.#getMissingPredecessors(waitingEvent);
|
|
1633
|
+
if (stillMissing.length === 0) {
|
|
1634
|
+
this.#pending.delete(waitingEventId);
|
|
1635
|
+
ready.push(waitingEvent);
|
|
1636
|
+
this.#releaseWaiting(waitingEventId, ready);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Handle buffer overflow by force-releasing oldest events
|
|
1642
|
+
*/
|
|
1643
|
+
#handleBufferOverflow(ready) {
|
|
1644
|
+
while (this.#pending.size > this.#options.maxBufferSize) {
|
|
1645
|
+
let oldest = null;
|
|
1646
|
+
for (const event of this.#pending.values()) {
|
|
1647
|
+
if (!oldest || (event.receivedAt ?? 0) < (oldest.receivedAt ?? 0)) {
|
|
1648
|
+
oldest = event;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (oldest) {
|
|
1652
|
+
this.#forceRelease(oldest, ready);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Handle events that have been waiting too long
|
|
1658
|
+
*/
|
|
1659
|
+
#handleTimeouts(ready) {
|
|
1660
|
+
if (this.#options.maxWaitTime <= 0 || this.#options.maxWaitTime === Infinity) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const now = Date.now();
|
|
1664
|
+
const toRelease = [];
|
|
1665
|
+
for (const event of this.#pending.values()) {
|
|
1666
|
+
const waitTime = now - (event.receivedAt ?? now);
|
|
1667
|
+
if (waitTime >= this.#options.maxWaitTime) {
|
|
1668
|
+
toRelease.push(event);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (const event of toRelease) {
|
|
1672
|
+
this.#forceRelease(event, ready);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Force release an event despite missing predecessors
|
|
1677
|
+
*/
|
|
1678
|
+
#forceRelease(event, ready) {
|
|
1679
|
+
const missingPredecessors = this.#getMissingPredecessors(event);
|
|
1680
|
+
this.#pending.delete(event.eventId);
|
|
1681
|
+
for (const predecessorId of event.causedBy ?? []) {
|
|
1682
|
+
const waiting = this.#waitingFor.get(predecessorId);
|
|
1683
|
+
if (waiting) {
|
|
1684
|
+
waiting.delete(event.eventId);
|
|
1685
|
+
if (waiting.size === 0) {
|
|
1686
|
+
this.#waitingFor.delete(predecessorId);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (missingPredecessors.length > 0) {
|
|
1691
|
+
this.#options.onForcedRelease?.(event, missingPredecessors);
|
|
1692
|
+
}
|
|
1693
|
+
ready.push(event);
|
|
1694
|
+
this.#releaseWaiting(event.eventId, ready);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
function validateCausalOrder(events) {
|
|
1698
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1699
|
+
for (const event of events) {
|
|
1700
|
+
if (event.causedBy) {
|
|
1701
|
+
for (const predecessorId of event.causedBy) {
|
|
1702
|
+
if (!seen.has(predecessorId)) {
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
seen.add(event.eventId);
|
|
1708
|
+
}
|
|
1709
|
+
return true;
|
|
1710
|
+
}
|
|
1711
|
+
function sortCausalOrder(events) {
|
|
1712
|
+
const eventMap = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (const event of events) {
|
|
1714
|
+
eventMap.set(event.eventId, event);
|
|
1715
|
+
}
|
|
1716
|
+
const result = [];
|
|
1717
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1718
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1719
|
+
function visit(eventId) {
|
|
1720
|
+
if (visited.has(eventId)) return;
|
|
1721
|
+
if (visiting.has(eventId)) {
|
|
1722
|
+
throw new Error(`Cycle detected involving event: ${eventId}`);
|
|
1723
|
+
}
|
|
1724
|
+
const event = eventMap.get(eventId);
|
|
1725
|
+
if (!event) {
|
|
1726
|
+
throw new Error(`Missing event: ${eventId}`);
|
|
1727
|
+
}
|
|
1728
|
+
visiting.add(eventId);
|
|
1729
|
+
if (event.causedBy) {
|
|
1730
|
+
for (const predecessorId of event.causedBy) {
|
|
1731
|
+
if (!eventMap.has(predecessorId)) {
|
|
1732
|
+
throw new Error(`Missing predecessor: ${predecessorId} for event: ${eventId}`);
|
|
1733
|
+
}
|
|
1734
|
+
visit(predecessorId);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
visiting.delete(eventId);
|
|
1738
|
+
visited.add(eventId);
|
|
1739
|
+
result.push(event);
|
|
1740
|
+
}
|
|
1741
|
+
for (const event of events) {
|
|
1742
|
+
visit(event.eventId);
|
|
1743
|
+
}
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/connection/client.ts
|
|
1748
|
+
var ClientConnection = class {
|
|
1749
|
+
#connection;
|
|
1750
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
1751
|
+
#subscriptionStates = /* @__PURE__ */ new Map();
|
|
1752
|
+
#reconnectionHandlers = /* @__PURE__ */ new Set();
|
|
1753
|
+
#options;
|
|
1754
|
+
#sessionId = null;
|
|
1755
|
+
#serverCapabilities = null;
|
|
1756
|
+
#connected = false;
|
|
1757
|
+
#lastConnectOptions;
|
|
1758
|
+
#isReconnecting = false;
|
|
1759
|
+
constructor(stream, options = {}) {
|
|
1760
|
+
this.#connection = new BaseConnection(stream, options);
|
|
1761
|
+
this.#options = options;
|
|
1762
|
+
this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
|
|
1763
|
+
if (options.reconnection?.enabled && options.createStream) {
|
|
1764
|
+
this.#connection.onStateChange((newState) => {
|
|
1765
|
+
if (newState === "closed" && this.#connected && !this.#isReconnecting) {
|
|
1766
|
+
void this.#handleDisconnect();
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
// ===========================================================================
|
|
1772
|
+
// Connection Lifecycle
|
|
1773
|
+
// ===========================================================================
|
|
1774
|
+
/**
|
|
1775
|
+
* Connect to the MAP system
|
|
1776
|
+
*/
|
|
1777
|
+
async connect(options) {
|
|
1778
|
+
const params = {
|
|
1779
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1780
|
+
participantType: "client",
|
|
1781
|
+
name: this.#options.name,
|
|
1782
|
+
capabilities: this.#options.capabilities,
|
|
1783
|
+
sessionId: options?.sessionId,
|
|
1784
|
+
auth: options?.auth
|
|
1785
|
+
};
|
|
1786
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.CONNECT, params);
|
|
1787
|
+
this.#sessionId = result.sessionId;
|
|
1788
|
+
this.#serverCapabilities = result.capabilities;
|
|
1789
|
+
this.#connected = true;
|
|
1790
|
+
this.#connection._transitionTo("connected");
|
|
1791
|
+
this.#lastConnectOptions = options;
|
|
1792
|
+
return result;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Disconnect from the MAP system
|
|
1796
|
+
*/
|
|
1797
|
+
async disconnect(reason) {
|
|
1798
|
+
if (!this.#connected) return;
|
|
1799
|
+
try {
|
|
1800
|
+
await this.#connection.sendRequest(
|
|
1801
|
+
CORE_METHODS.DISCONNECT,
|
|
1802
|
+
reason ? { reason } : void 0
|
|
1803
|
+
);
|
|
1804
|
+
} finally {
|
|
1805
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
1806
|
+
subscription._close();
|
|
1807
|
+
}
|
|
1808
|
+
this.#subscriptions.clear();
|
|
1809
|
+
await this.#connection.close();
|
|
1810
|
+
this.#connected = false;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Whether the client is connected
|
|
1815
|
+
*/
|
|
1816
|
+
get isConnected() {
|
|
1817
|
+
return this.#connected && !this.#connection.isClosed;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Current session ID
|
|
1821
|
+
*/
|
|
1822
|
+
get sessionId() {
|
|
1823
|
+
return this.#sessionId;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Server capabilities
|
|
1827
|
+
*/
|
|
1828
|
+
get serverCapabilities() {
|
|
1829
|
+
return this.#serverCapabilities;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* AbortSignal that triggers when the connection closes
|
|
1833
|
+
*/
|
|
1834
|
+
get signal() {
|
|
1835
|
+
return this.#connection.signal;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Promise that resolves when the connection closes
|
|
1839
|
+
*/
|
|
1840
|
+
get closed() {
|
|
1841
|
+
return this.#connection.closed;
|
|
1842
|
+
}
|
|
1843
|
+
// ===========================================================================
|
|
1844
|
+
// Session Management
|
|
1845
|
+
// ===========================================================================
|
|
1846
|
+
/**
|
|
1847
|
+
* List available sessions
|
|
1848
|
+
*/
|
|
1849
|
+
async listSessions() {
|
|
1850
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_LIST);
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Load an existing session
|
|
1854
|
+
*/
|
|
1855
|
+
async loadSession(sessionId) {
|
|
1856
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_LOAD, { sessionId });
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Close the current session
|
|
1860
|
+
*/
|
|
1861
|
+
async closeSession(sessionId) {
|
|
1862
|
+
return this.#connection.sendRequest(SESSION_METHODS.SESSION_CLOSE, { sessionId });
|
|
1863
|
+
}
|
|
1864
|
+
// ===========================================================================
|
|
1865
|
+
// Agent Queries
|
|
1866
|
+
// ===========================================================================
|
|
1867
|
+
/**
|
|
1868
|
+
* List agents with optional filters
|
|
1869
|
+
*/
|
|
1870
|
+
async listAgents(options) {
|
|
1871
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.AGENTS_LIST, options);
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Get a single agent by ID
|
|
1875
|
+
*/
|
|
1876
|
+
async getAgent(agentId, options) {
|
|
1877
|
+
const params = { agentId, ...options };
|
|
1878
|
+
return this.#connection.sendRequest(
|
|
1879
|
+
OBSERVATION_METHODS.AGENTS_GET,
|
|
1880
|
+
params
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Get the agent structure/hierarchy graph
|
|
1885
|
+
*/
|
|
1886
|
+
async getStructureGraph(options) {
|
|
1887
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.STRUCTURE_GRAPH, options);
|
|
1888
|
+
}
|
|
1889
|
+
// ===========================================================================
|
|
1890
|
+
// Scope Queries
|
|
1891
|
+
// ===========================================================================
|
|
1892
|
+
/**
|
|
1893
|
+
* List scopes
|
|
1894
|
+
*/
|
|
1895
|
+
async listScopes(options) {
|
|
1896
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_LIST, options);
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Get a single scope by ID
|
|
1900
|
+
*/
|
|
1901
|
+
async getScope(scopeId) {
|
|
1902
|
+
const result = await this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_GET, { scopeId });
|
|
1903
|
+
return result.scope;
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* List members of a scope
|
|
1907
|
+
*/
|
|
1908
|
+
async getScopeMembers(scopeId, options) {
|
|
1909
|
+
return this.#connection.sendRequest(OBSERVATION_METHODS.SCOPES_MEMBERS, {
|
|
1910
|
+
scopeId,
|
|
1911
|
+
...options
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
// ===========================================================================
|
|
1915
|
+
// Messaging
|
|
1916
|
+
// ===========================================================================
|
|
1917
|
+
/**
|
|
1918
|
+
* Send a message to an address
|
|
1919
|
+
*/
|
|
1920
|
+
async send(to, payload, meta) {
|
|
1921
|
+
const params = { to };
|
|
1922
|
+
if (payload !== void 0) params.payload = payload;
|
|
1923
|
+
if (meta) params.meta = meta;
|
|
1924
|
+
return this.#connection.sendRequest(CORE_METHODS.SEND, params);
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Send a message to a specific agent
|
|
1928
|
+
*/
|
|
1929
|
+
async sendToAgent(agentId, payload, meta) {
|
|
1930
|
+
return this.send({ agent: agentId }, payload, meta);
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Send a message to all agents in a scope
|
|
1934
|
+
*/
|
|
1935
|
+
async sendToScope(scopeId, payload, meta) {
|
|
1936
|
+
return this.send({ scope: scopeId }, payload, meta);
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Send a message to agents with a specific role
|
|
1940
|
+
*/
|
|
1941
|
+
async sendToRole(role, payload, meta, withinScope) {
|
|
1942
|
+
return this.send({ role, within: withinScope }, payload, meta);
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Broadcast a message to all agents
|
|
1946
|
+
*/
|
|
1947
|
+
async broadcast(payload, meta) {
|
|
1948
|
+
return this.send({ broadcast: true }, payload, meta);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Send a request and wait for a correlated response
|
|
1952
|
+
*
|
|
1953
|
+
* This is a higher-level pattern for request/response messaging.
|
|
1954
|
+
* A correlationId is automatically generated.
|
|
1955
|
+
*/
|
|
1956
|
+
async request(to, payload, options) {
|
|
1957
|
+
const correlationId = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1958
|
+
const responseSub = await this.subscribe({
|
|
1959
|
+
// We'll filter in the handler since subscription filters don't support correlationId
|
|
1960
|
+
});
|
|
1961
|
+
try {
|
|
1962
|
+
await this.send(to, payload, {
|
|
1963
|
+
...options?.meta,
|
|
1964
|
+
expectsResponse: true,
|
|
1965
|
+
correlationId
|
|
1966
|
+
});
|
|
1967
|
+
const timeout = options?.timeout ?? 3e4;
|
|
1968
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1969
|
+
setTimeout(() => reject(new Error(`Request timed out after ${timeout}ms`)), timeout);
|
|
1970
|
+
});
|
|
1971
|
+
const responsePromise = (async () => {
|
|
1972
|
+
for await (const event of responseSub) {
|
|
1973
|
+
if (event.type === "message_delivered" && event.data && event.data.correlationId === correlationId) {
|
|
1974
|
+
return event.data.message;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
throw new Error("Subscription closed before response received");
|
|
1978
|
+
})();
|
|
1979
|
+
return await Promise.race([responsePromise, timeoutPromise]);
|
|
1980
|
+
} finally {
|
|
1981
|
+
await responseSub.unsubscribe();
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
// ===========================================================================
|
|
1985
|
+
// Subscriptions
|
|
1986
|
+
// ===========================================================================
|
|
1987
|
+
/**
|
|
1988
|
+
* Subscribe to events
|
|
1989
|
+
*/
|
|
1990
|
+
async subscribe(filter) {
|
|
1991
|
+
const params = {};
|
|
1992
|
+
if (filter) params.filter = filter;
|
|
1993
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.SUBSCRIBE, params);
|
|
1994
|
+
const serverSupportsAck = this.#serverCapabilities?.streaming?.supportsAck === true;
|
|
1995
|
+
const sendAck = serverSupportsAck ? (ackParams) => {
|
|
1996
|
+
this.#connection.sendNotification(NOTIFICATION_METHODS.SUBSCRIBE_ACK, ackParams);
|
|
1997
|
+
} : void 0;
|
|
1998
|
+
const subscription = createSubscription(
|
|
1999
|
+
result.subscriptionId,
|
|
2000
|
+
() => this.unsubscribe(result.subscriptionId),
|
|
2001
|
+
{ filter },
|
|
2002
|
+
sendAck
|
|
2003
|
+
);
|
|
2004
|
+
if (serverSupportsAck) {
|
|
2005
|
+
subscription._setServerSupportsAck(true);
|
|
2006
|
+
}
|
|
2007
|
+
this.#subscriptions.set(result.subscriptionId, subscription);
|
|
2008
|
+
if (this.#options.reconnection?.restoreSubscriptions !== false) {
|
|
2009
|
+
this.#subscriptionStates.set(result.subscriptionId, {
|
|
2010
|
+
filter,
|
|
2011
|
+
handlers: /* @__PURE__ */ new Set()
|
|
2012
|
+
});
|
|
2013
|
+
const originalPushEvent = subscription._pushEvent.bind(subscription);
|
|
2014
|
+
subscription._pushEvent = (event) => {
|
|
2015
|
+
const state = this.#subscriptionStates.get(result.subscriptionId);
|
|
2016
|
+
if (state && event.eventId) {
|
|
2017
|
+
state.lastEventId = event.eventId;
|
|
2018
|
+
}
|
|
2019
|
+
originalPushEvent(event);
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
return subscription;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Unsubscribe from events
|
|
2026
|
+
*/
|
|
2027
|
+
async unsubscribe(subscriptionId) {
|
|
2028
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
2029
|
+
if (subscription) {
|
|
2030
|
+
subscription._close();
|
|
2031
|
+
this.#subscriptions.delete(subscriptionId);
|
|
2032
|
+
}
|
|
2033
|
+
this.#subscriptionStates.delete(subscriptionId);
|
|
2034
|
+
await this.#connection.sendRequest(CORE_METHODS.UNSUBSCRIBE, { subscriptionId });
|
|
2035
|
+
}
|
|
2036
|
+
// ===========================================================================
|
|
2037
|
+
// Event Replay
|
|
2038
|
+
// ===========================================================================
|
|
2039
|
+
/**
|
|
2040
|
+
* Replay historical events.
|
|
2041
|
+
*
|
|
2042
|
+
* Uses keyset pagination - pass the last eventId from the previous
|
|
2043
|
+
* response to get the next page.
|
|
2044
|
+
*
|
|
2045
|
+
* @example
|
|
2046
|
+
* ```typescript
|
|
2047
|
+
* // Replay all events from the last hour
|
|
2048
|
+
* const result = await client.replay({
|
|
2049
|
+
* fromTimestamp: Date.now() - 3600000,
|
|
2050
|
+
* filter: { eventTypes: ['agent.registered'] },
|
|
2051
|
+
* limit: 100
|
|
2052
|
+
* });
|
|
2053
|
+
*
|
|
2054
|
+
* // Paginate through results
|
|
2055
|
+
* let afterEventId: string | undefined;
|
|
2056
|
+
* do {
|
|
2057
|
+
* const page = await client.replay({ afterEventId, limit: 100 });
|
|
2058
|
+
* for (const item of page.events) {
|
|
2059
|
+
* console.log(item.eventId, item.event);
|
|
2060
|
+
* }
|
|
2061
|
+
* afterEventId = page.events.at(-1)?.eventId;
|
|
2062
|
+
* } while (page.hasMore);
|
|
2063
|
+
* ```
|
|
2064
|
+
*/
|
|
2065
|
+
async replay(params = {}) {
|
|
2066
|
+
const limit = Math.min(params.limit ?? 100, 1e3);
|
|
2067
|
+
return this.#connection.sendRequest(
|
|
2068
|
+
CORE_METHODS.REPLAY,
|
|
2069
|
+
{ ...params, limit }
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Replay all events matching filter, handling pagination automatically.
|
|
2074
|
+
*
|
|
2075
|
+
* Returns an async generator for streaming through all results.
|
|
2076
|
+
*
|
|
2077
|
+
* @example
|
|
2078
|
+
* ```typescript
|
|
2079
|
+
* for await (const item of client.replayAll({
|
|
2080
|
+
* filter: { eventTypes: ['agent.registered'] }
|
|
2081
|
+
* })) {
|
|
2082
|
+
* console.log(item.eventId, item.event);
|
|
2083
|
+
* }
|
|
2084
|
+
* ```
|
|
2085
|
+
*/
|
|
2086
|
+
async *replayAll(params = {}) {
|
|
2087
|
+
let afterEventId;
|
|
2088
|
+
let hasMore = true;
|
|
2089
|
+
while (hasMore) {
|
|
2090
|
+
const result = await this.replay({ ...params, afterEventId });
|
|
2091
|
+
for (const item of result.events) {
|
|
2092
|
+
yield item;
|
|
2093
|
+
}
|
|
2094
|
+
hasMore = result.hasMore;
|
|
2095
|
+
afterEventId = result.events.at(-1)?.eventId;
|
|
2096
|
+
if (result.events.length === 0) {
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// ===========================================================================
|
|
2102
|
+
// Steering (requires canSteer capability)
|
|
2103
|
+
// ===========================================================================
|
|
2104
|
+
/**
|
|
2105
|
+
* Inject context into a running agent
|
|
2106
|
+
*/
|
|
2107
|
+
async inject(agentId, content, delivery) {
|
|
2108
|
+
const params = { agentId, content };
|
|
2109
|
+
if (delivery) params.delivery = delivery;
|
|
2110
|
+
return this.#connection.sendRequest(STEERING_METHODS.INJECT, params);
|
|
2111
|
+
}
|
|
2112
|
+
// ===========================================================================
|
|
2113
|
+
// Lifecycle Control (requires canStop capability)
|
|
2114
|
+
// ===========================================================================
|
|
2115
|
+
/**
|
|
2116
|
+
* Request an agent to stop
|
|
2117
|
+
*/
|
|
2118
|
+
async stopAgent(agentId, options) {
|
|
2119
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_STOP, {
|
|
2120
|
+
agentId,
|
|
2121
|
+
...options
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Suspend an agent
|
|
2126
|
+
*/
|
|
2127
|
+
async suspendAgent(agentId, reason) {
|
|
2128
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_SUSPEND, {
|
|
2129
|
+
agentId,
|
|
2130
|
+
reason
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Resume a suspended agent
|
|
2135
|
+
*/
|
|
2136
|
+
async resumeAgent(agentId) {
|
|
2137
|
+
return this.#connection.sendRequest(STATE_METHODS.AGENTS_RESUME, { agentId });
|
|
2138
|
+
}
|
|
2139
|
+
// ===========================================================================
|
|
2140
|
+
// Reconnection
|
|
2141
|
+
// ===========================================================================
|
|
2142
|
+
/**
|
|
2143
|
+
* Current connection state
|
|
2144
|
+
*/
|
|
2145
|
+
get state() {
|
|
2146
|
+
return this.#connection.state;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Whether the connection is currently reconnecting
|
|
2150
|
+
*/
|
|
2151
|
+
get isReconnecting() {
|
|
2152
|
+
return this.#isReconnecting;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Register a handler for reconnection events.
|
|
2156
|
+
*
|
|
2157
|
+
* @param handler - Function called when reconnection events occur
|
|
2158
|
+
* @returns Unsubscribe function to remove the handler
|
|
2159
|
+
*
|
|
2160
|
+
* @example
|
|
2161
|
+
* ```typescript
|
|
2162
|
+
* const unsubscribe = client.onReconnection((event) => {
|
|
2163
|
+
* switch (event.type) {
|
|
2164
|
+
* case 'disconnected':
|
|
2165
|
+
* console.log('Connection lost');
|
|
2166
|
+
* break;
|
|
2167
|
+
* case 'reconnecting':
|
|
2168
|
+
* console.log(`Reconnecting, attempt ${event.attempt}`);
|
|
2169
|
+
* break;
|
|
2170
|
+
* case 'reconnected':
|
|
2171
|
+
* console.log('Reconnected successfully');
|
|
2172
|
+
* break;
|
|
2173
|
+
* case 'reconnectFailed':
|
|
2174
|
+
* console.log('Failed to reconnect:', event.error);
|
|
2175
|
+
* break;
|
|
2176
|
+
* }
|
|
2177
|
+
* });
|
|
2178
|
+
* ```
|
|
2179
|
+
*/
|
|
2180
|
+
onReconnection(handler) {
|
|
2181
|
+
this.#reconnectionHandlers.add(handler);
|
|
2182
|
+
return () => this.#reconnectionHandlers.delete(handler);
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Register a handler for connection state changes.
|
|
2186
|
+
*
|
|
2187
|
+
* @param handler - Function called when state changes
|
|
2188
|
+
* @returns Unsubscribe function to remove the handler
|
|
2189
|
+
*/
|
|
2190
|
+
onStateChange(handler) {
|
|
2191
|
+
return this.#connection.onStateChange(handler);
|
|
2192
|
+
}
|
|
2193
|
+
// ===========================================================================
|
|
2194
|
+
// Internal
|
|
2195
|
+
// ===========================================================================
|
|
2196
|
+
/**
|
|
2197
|
+
* Handle incoming notifications
|
|
2198
|
+
*/
|
|
2199
|
+
async #handleNotification(method, params) {
|
|
2200
|
+
switch (method) {
|
|
2201
|
+
case NOTIFICATION_METHODS.EVENT: {
|
|
2202
|
+
const eventParams = params;
|
|
2203
|
+
const subscription = this.#subscriptions.get(eventParams.subscriptionId);
|
|
2204
|
+
if (subscription) {
|
|
2205
|
+
subscription._pushEvent(eventParams);
|
|
2206
|
+
} else {
|
|
2207
|
+
console.warn("MAP: Event for unknown subscription:", eventParams.subscriptionId);
|
|
2208
|
+
}
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
case NOTIFICATION_METHODS.MESSAGE: {
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
default:
|
|
2215
|
+
console.warn("MAP: Unknown notification:", method);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Emit a reconnection event to all registered handlers
|
|
2220
|
+
*/
|
|
2221
|
+
#emitReconnectionEvent(event) {
|
|
2222
|
+
for (const handler of this.#reconnectionHandlers) {
|
|
2223
|
+
try {
|
|
2224
|
+
handler(event);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
console.error("MAP: Reconnection event handler error:", error);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Handle disconnect when auto-reconnect is enabled
|
|
2232
|
+
*/
|
|
2233
|
+
async #handleDisconnect() {
|
|
2234
|
+
this.#isReconnecting = true;
|
|
2235
|
+
this.#connected = false;
|
|
2236
|
+
this.#emitReconnectionEvent({ type: "disconnected" });
|
|
2237
|
+
try {
|
|
2238
|
+
await this.#attemptReconnect();
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
this.#isReconnecting = false;
|
|
2241
|
+
this.#emitReconnectionEvent({
|
|
2242
|
+
type: "reconnectFailed",
|
|
2243
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Attempt to reconnect with retry logic
|
|
2249
|
+
*/
|
|
2250
|
+
async #attemptReconnect() {
|
|
2251
|
+
const options = this.#options.reconnection;
|
|
2252
|
+
const createStream = this.#options.createStream;
|
|
2253
|
+
const retryPolicy = {
|
|
2254
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
2255
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
2256
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
2257
|
+
jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
|
|
2258
|
+
};
|
|
2259
|
+
await withRetry(
|
|
2260
|
+
async () => {
|
|
2261
|
+
const newStream = await createStream();
|
|
2262
|
+
await this.#connection.reconnect(newStream);
|
|
2263
|
+
const connectResult = await this.connect(this.#lastConnectOptions);
|
|
2264
|
+
this.#sessionId = connectResult.sessionId;
|
|
2265
|
+
this.#serverCapabilities = connectResult.capabilities;
|
|
2266
|
+
},
|
|
2267
|
+
retryPolicy,
|
|
2268
|
+
{
|
|
2269
|
+
onRetry: (state) => {
|
|
2270
|
+
this.#emitReconnectionEvent({
|
|
2271
|
+
type: "reconnecting",
|
|
2272
|
+
attempt: state.attempt,
|
|
2273
|
+
delay: state.nextDelayMs,
|
|
2274
|
+
error: state.lastError
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
);
|
|
2279
|
+
this.#isReconnecting = false;
|
|
2280
|
+
this.#emitReconnectionEvent({ type: "reconnected" });
|
|
2281
|
+
if (options.restoreSubscriptions !== false) {
|
|
2282
|
+
await this.#restoreSubscriptions();
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Restore subscriptions after reconnection
|
|
2287
|
+
*/
|
|
2288
|
+
async #restoreSubscriptions() {
|
|
2289
|
+
const options = this.#options.reconnection;
|
|
2290
|
+
const subscriptionEntries = Array.from(this.#subscriptionStates.entries());
|
|
2291
|
+
this.#subscriptions.clear();
|
|
2292
|
+
this.#subscriptionStates.clear();
|
|
2293
|
+
for (const [oldId, state] of subscriptionEntries) {
|
|
2294
|
+
try {
|
|
2295
|
+
const newSubscription = await this.subscribe(state.filter);
|
|
2296
|
+
const newId = newSubscription.id;
|
|
2297
|
+
if (options.replayOnRestore !== false && state.lastEventId) {
|
|
2298
|
+
const maxEvents = options.maxReplayEventsPerSubscription ?? 1e3;
|
|
2299
|
+
try {
|
|
2300
|
+
let replayedCount = 0;
|
|
2301
|
+
let afterEventId = state.lastEventId;
|
|
2302
|
+
let hasMore = true;
|
|
2303
|
+
while (hasMore && replayedCount < maxEvents) {
|
|
2304
|
+
const result = await this.replay({
|
|
2305
|
+
afterEventId,
|
|
2306
|
+
filter: state.filter,
|
|
2307
|
+
limit: Math.min(100, maxEvents - replayedCount)
|
|
2308
|
+
});
|
|
2309
|
+
for (const replayedEvent of result.events) {
|
|
2310
|
+
if (replayedCount >= maxEvents) break;
|
|
2311
|
+
newSubscription._pushEvent({
|
|
2312
|
+
subscriptionId: newId,
|
|
2313
|
+
sequenceNumber: replayedCount + 1,
|
|
2314
|
+
eventId: replayedEvent.eventId,
|
|
2315
|
+
timestamp: replayedEvent.timestamp,
|
|
2316
|
+
event: replayedEvent.event
|
|
2317
|
+
});
|
|
2318
|
+
replayedCount++;
|
|
2319
|
+
}
|
|
2320
|
+
hasMore = result.hasMore;
|
|
2321
|
+
afterEventId = result.events.at(-1)?.eventId;
|
|
2322
|
+
if (result.events.length === 0) {
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
} catch (replayError) {
|
|
2327
|
+
console.warn("MAP: Failed to replay events for subscription:", oldId, replayError);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
this.#emitReconnectionEvent({
|
|
2331
|
+
type: "subscriptionRestored",
|
|
2332
|
+
subscriptionId: oldId,
|
|
2333
|
+
newSubscriptionId: newId
|
|
2334
|
+
});
|
|
2335
|
+
} catch (error) {
|
|
2336
|
+
this.#emitReconnectionEvent({
|
|
2337
|
+
type: "subscriptionRestoreFailed",
|
|
2338
|
+
subscriptionId: oldId,
|
|
2339
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
// src/connection/agent.ts
|
|
2347
|
+
var AgentConnection = class {
|
|
2348
|
+
#connection;
|
|
2349
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
2350
|
+
#options;
|
|
2351
|
+
#messageHandlers = /* @__PURE__ */ new Set();
|
|
2352
|
+
#reconnectionHandlers = /* @__PURE__ */ new Set();
|
|
2353
|
+
#scopeMemberships = /* @__PURE__ */ new Set();
|
|
2354
|
+
#agentId = null;
|
|
2355
|
+
#sessionId = null;
|
|
2356
|
+
#serverCapabilities = null;
|
|
2357
|
+
#currentState = "registered";
|
|
2358
|
+
#connected = false;
|
|
2359
|
+
#lastConnectOptions;
|
|
2360
|
+
#isReconnecting = false;
|
|
2361
|
+
constructor(stream, options = {}) {
|
|
2362
|
+
this.#connection = new BaseConnection(stream, options);
|
|
2363
|
+
this.#options = options;
|
|
2364
|
+
this.#connection.setNotificationHandler(this.#handleNotification.bind(this));
|
|
2365
|
+
if (options.reconnection?.enabled && options.createStream) {
|
|
2366
|
+
this.#connection.onStateChange((newState) => {
|
|
2367
|
+
if (newState === "closed" && this.#connected && !this.#isReconnecting) {
|
|
2368
|
+
void this.#handleDisconnect();
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
// ===========================================================================
|
|
2374
|
+
// Connection Lifecycle
|
|
2375
|
+
// ===========================================================================
|
|
2376
|
+
/**
|
|
2377
|
+
* Connect and register with the MAP system
|
|
2378
|
+
*/
|
|
2379
|
+
async connect(options) {
|
|
2380
|
+
const connectParams = {
|
|
2381
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2382
|
+
participantType: "agent",
|
|
2383
|
+
participantId: options?.agentId,
|
|
2384
|
+
name: this.#options.name,
|
|
2385
|
+
capabilities: this.#options.capabilities,
|
|
2386
|
+
auth: options?.auth
|
|
2387
|
+
};
|
|
2388
|
+
const connectResult = await this.#connection.sendRequest(CORE_METHODS.CONNECT, connectParams);
|
|
2389
|
+
this.#sessionId = connectResult.sessionId;
|
|
2390
|
+
this.#serverCapabilities = connectResult.capabilities;
|
|
2391
|
+
this.#connected = true;
|
|
2392
|
+
this.#lastConnectOptions = options;
|
|
2393
|
+
const registerParams = {
|
|
2394
|
+
agentId: options?.agentId,
|
|
2395
|
+
name: this.#options.name,
|
|
2396
|
+
role: this.#options.role,
|
|
2397
|
+
parent: this.#options.parent,
|
|
2398
|
+
scopes: this.#options.scopes,
|
|
2399
|
+
visibility: this.#options.visibility,
|
|
2400
|
+
capabilities: this.#options.capabilities
|
|
2401
|
+
};
|
|
2402
|
+
const registerResult = await this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_REGISTER, registerParams);
|
|
2403
|
+
this.#agentId = registerResult.agent.id;
|
|
2404
|
+
this.#currentState = registerResult.agent.state;
|
|
2405
|
+
this.#connection._transitionTo("connected");
|
|
2406
|
+
return { connection: connectResult, agent: registerResult.agent };
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Disconnect from the MAP system
|
|
2410
|
+
*/
|
|
2411
|
+
async disconnect(reason) {
|
|
2412
|
+
if (!this.#connected) return;
|
|
2413
|
+
try {
|
|
2414
|
+
if (this.#agentId) {
|
|
2415
|
+
await this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_UNREGISTER, {
|
|
2416
|
+
agentId: this.#agentId,
|
|
2417
|
+
reason
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
await this.#connection.sendRequest(
|
|
2421
|
+
CORE_METHODS.DISCONNECT,
|
|
2422
|
+
reason ? { reason } : void 0
|
|
2423
|
+
);
|
|
2424
|
+
} finally {
|
|
2425
|
+
for (const subscription of this.#subscriptions.values()) {
|
|
2426
|
+
subscription._close();
|
|
2427
|
+
}
|
|
2428
|
+
this.#subscriptions.clear();
|
|
2429
|
+
await this.#connection.close();
|
|
2430
|
+
this.#connected = false;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Whether the agent is connected
|
|
2435
|
+
*/
|
|
2436
|
+
get isConnected() {
|
|
2437
|
+
return this.#connected && !this.#connection.isClosed;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* This agent's ID
|
|
2441
|
+
*/
|
|
2442
|
+
get agentId() {
|
|
2443
|
+
return this.#agentId;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Current session ID
|
|
2447
|
+
*/
|
|
2448
|
+
get sessionId() {
|
|
2449
|
+
return this.#sessionId;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Server capabilities
|
|
2453
|
+
*/
|
|
2454
|
+
get serverCapabilities() {
|
|
2455
|
+
return this.#serverCapabilities;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Current agent state
|
|
2459
|
+
*/
|
|
2460
|
+
get state() {
|
|
2461
|
+
return this.#currentState;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* AbortSignal that triggers when the connection closes
|
|
2465
|
+
*/
|
|
2466
|
+
get signal() {
|
|
2467
|
+
return this.#connection.signal;
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Promise that resolves when the connection closes
|
|
2471
|
+
*/
|
|
2472
|
+
get closed() {
|
|
2473
|
+
return this.#connection.closed;
|
|
2474
|
+
}
|
|
2475
|
+
// ===========================================================================
|
|
2476
|
+
// Message Handling
|
|
2477
|
+
// ===========================================================================
|
|
2478
|
+
/**
|
|
2479
|
+
* Register a handler for incoming messages
|
|
2480
|
+
*/
|
|
2481
|
+
onMessage(handler) {
|
|
2482
|
+
this.#messageHandlers.add(handler);
|
|
2483
|
+
return this;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Remove a message handler
|
|
2487
|
+
*/
|
|
2488
|
+
offMessage(handler) {
|
|
2489
|
+
this.#messageHandlers.delete(handler);
|
|
2490
|
+
return this;
|
|
2491
|
+
}
|
|
2492
|
+
// ===========================================================================
|
|
2493
|
+
// State Management
|
|
2494
|
+
// ===========================================================================
|
|
2495
|
+
/**
|
|
2496
|
+
* Update this agent's state
|
|
2497
|
+
*/
|
|
2498
|
+
async updateState(state) {
|
|
2499
|
+
if (!this.#agentId) {
|
|
2500
|
+
throw new Error("Agent not registered");
|
|
2501
|
+
}
|
|
2502
|
+
const result = await this.#connection.sendRequest(STATE_METHODS.AGENTS_UPDATE, {
|
|
2503
|
+
agentId: this.#agentId,
|
|
2504
|
+
state
|
|
2505
|
+
});
|
|
2506
|
+
this.#currentState = result.agent.state;
|
|
2507
|
+
return result.agent;
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Update this agent's metadata
|
|
2511
|
+
*/
|
|
2512
|
+
async updateMetadata(metadata) {
|
|
2513
|
+
if (!this.#agentId) {
|
|
2514
|
+
throw new Error("Agent not registered");
|
|
2515
|
+
}
|
|
2516
|
+
const result = await this.#connection.sendRequest(STATE_METHODS.AGENTS_UPDATE, {
|
|
2517
|
+
agentId: this.#agentId,
|
|
2518
|
+
metadata
|
|
2519
|
+
});
|
|
2520
|
+
return result.agent;
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Mark this agent as busy
|
|
2524
|
+
*/
|
|
2525
|
+
async busy() {
|
|
2526
|
+
return this.updateState("busy");
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Mark this agent as idle
|
|
2530
|
+
*/
|
|
2531
|
+
async idle() {
|
|
2532
|
+
return this.updateState("idle");
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Mark this agent as done/stopped
|
|
2536
|
+
*/
|
|
2537
|
+
async done(result) {
|
|
2538
|
+
if (!this.#agentId) {
|
|
2539
|
+
throw new Error("Agent not registered");
|
|
2540
|
+
}
|
|
2541
|
+
await this.updateState("stopped");
|
|
2542
|
+
if (result) {
|
|
2543
|
+
await this.updateMetadata({
|
|
2544
|
+
exitCode: result.exitCode,
|
|
2545
|
+
exitReason: result.exitReason
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
// ===========================================================================
|
|
2550
|
+
// Child Agent Management
|
|
2551
|
+
// ===========================================================================
|
|
2552
|
+
/**
|
|
2553
|
+
* Spawn a child agent
|
|
2554
|
+
*/
|
|
2555
|
+
async spawn(options) {
|
|
2556
|
+
if (!this.#agentId) {
|
|
2557
|
+
throw new Error("Agent not registered");
|
|
2558
|
+
}
|
|
2559
|
+
const params = {
|
|
2560
|
+
...options,
|
|
2561
|
+
parent: this.#agentId
|
|
2562
|
+
};
|
|
2563
|
+
return this.#connection.sendRequest(LIFECYCLE_METHODS.AGENTS_SPAWN, params);
|
|
2564
|
+
}
|
|
2565
|
+
// ===========================================================================
|
|
2566
|
+
// Messaging
|
|
2567
|
+
// ===========================================================================
|
|
2568
|
+
/**
|
|
2569
|
+
* Send a message to an address
|
|
2570
|
+
*/
|
|
2571
|
+
async send(to, payload, meta) {
|
|
2572
|
+
const params = { to };
|
|
2573
|
+
if (payload !== void 0) params.payload = payload;
|
|
2574
|
+
if (meta) params.meta = meta;
|
|
2575
|
+
return this.#connection.sendRequest(CORE_METHODS.SEND, params);
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Send a message to the parent agent
|
|
2579
|
+
*/
|
|
2580
|
+
async sendToParent(payload, meta) {
|
|
2581
|
+
return this.send({ parent: true }, payload, {
|
|
2582
|
+
...meta,
|
|
2583
|
+
relationship: "child-to-parent"
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Send a message to child agents
|
|
2588
|
+
*/
|
|
2589
|
+
async sendToChildren(payload, meta) {
|
|
2590
|
+
return this.send({ children: true }, payload, {
|
|
2591
|
+
...meta,
|
|
2592
|
+
relationship: "parent-to-child"
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Send a message to a specific agent
|
|
2597
|
+
*/
|
|
2598
|
+
async sendToAgent(agentId, payload, meta) {
|
|
2599
|
+
return this.send({ agent: agentId }, payload, meta);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Send a message to all agents in a scope
|
|
2603
|
+
*/
|
|
2604
|
+
async sendToScope(scopeId, payload, meta) {
|
|
2605
|
+
return this.send({ scope: scopeId }, payload, meta);
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Send a message to sibling agents
|
|
2609
|
+
*/
|
|
2610
|
+
async sendToSiblings(payload, meta) {
|
|
2611
|
+
return this.send({ siblings: true }, payload, {
|
|
2612
|
+
...meta,
|
|
2613
|
+
relationship: "peer"
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Reply to a message (uses correlationId from original)
|
|
2618
|
+
*/
|
|
2619
|
+
async reply(originalMessage, payload, meta) {
|
|
2620
|
+
return this.send({ agent: originalMessage.from }, payload, {
|
|
2621
|
+
...meta,
|
|
2622
|
+
correlationId: originalMessage.meta?.correlationId ?? originalMessage.id,
|
|
2623
|
+
isResult: true
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
// ===========================================================================
|
|
2627
|
+
// Scope Management
|
|
2628
|
+
// ===========================================================================
|
|
2629
|
+
/**
|
|
2630
|
+
* Create a new scope
|
|
2631
|
+
*/
|
|
2632
|
+
async createScope(options) {
|
|
2633
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_CREATE, options);
|
|
2634
|
+
return result.scope;
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Join a scope
|
|
2638
|
+
*/
|
|
2639
|
+
async joinScope(scopeId) {
|
|
2640
|
+
if (!this.#agentId) {
|
|
2641
|
+
throw new Error("Agent not registered");
|
|
2642
|
+
}
|
|
2643
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_JOIN, {
|
|
2644
|
+
scopeId,
|
|
2645
|
+
agentId: this.#agentId
|
|
2646
|
+
});
|
|
2647
|
+
this.#scopeMemberships.add(scopeId);
|
|
2648
|
+
return result;
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Leave a scope
|
|
2652
|
+
*/
|
|
2653
|
+
async leaveScope(scopeId) {
|
|
2654
|
+
if (!this.#agentId) {
|
|
2655
|
+
throw new Error("Agent not registered");
|
|
2656
|
+
}
|
|
2657
|
+
const result = await this.#connection.sendRequest(SCOPE_METHODS.SCOPES_LEAVE, {
|
|
2658
|
+
scopeId,
|
|
2659
|
+
agentId: this.#agentId
|
|
2660
|
+
});
|
|
2661
|
+
this.#scopeMemberships.delete(scopeId);
|
|
2662
|
+
return result;
|
|
2663
|
+
}
|
|
2664
|
+
// ===========================================================================
|
|
2665
|
+
// Subscriptions
|
|
2666
|
+
// ===========================================================================
|
|
2667
|
+
/**
|
|
2668
|
+
* Subscribe to events
|
|
2669
|
+
*/
|
|
2670
|
+
async subscribe(filter) {
|
|
2671
|
+
const params = {};
|
|
2672
|
+
if (filter) params.filter = filter;
|
|
2673
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.SUBSCRIBE, params);
|
|
2674
|
+
const subscription = createSubscription(
|
|
2675
|
+
result.subscriptionId,
|
|
2676
|
+
() => this.unsubscribe(result.subscriptionId),
|
|
2677
|
+
{ filter }
|
|
2678
|
+
);
|
|
2679
|
+
this.#subscriptions.set(result.subscriptionId, subscription);
|
|
2680
|
+
return subscription;
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Unsubscribe from events
|
|
2684
|
+
*/
|
|
2685
|
+
async unsubscribe(subscriptionId) {
|
|
2686
|
+
const subscription = this.#subscriptions.get(subscriptionId);
|
|
2687
|
+
if (subscription) {
|
|
2688
|
+
subscription._close();
|
|
2689
|
+
this.#subscriptions.delete(subscriptionId);
|
|
2690
|
+
}
|
|
2691
|
+
await this.#connection.sendRequest(CORE_METHODS.UNSUBSCRIBE, { subscriptionId });
|
|
2692
|
+
}
|
|
2693
|
+
// ===========================================================================
|
|
2694
|
+
// Reconnection
|
|
2695
|
+
// ===========================================================================
|
|
2696
|
+
/**
|
|
2697
|
+
* Current connection state
|
|
2698
|
+
*/
|
|
2699
|
+
get connectionState() {
|
|
2700
|
+
return this.#connection.state;
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Whether the connection is currently reconnecting
|
|
2704
|
+
*/
|
|
2705
|
+
get isReconnecting() {
|
|
2706
|
+
return this.#isReconnecting;
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Register a handler for reconnection events.
|
|
2710
|
+
*
|
|
2711
|
+
* @param handler - Function called when reconnection events occur
|
|
2712
|
+
* @returns Unsubscribe function to remove the handler
|
|
2713
|
+
*/
|
|
2714
|
+
onReconnection(handler) {
|
|
2715
|
+
this.#reconnectionHandlers.add(handler);
|
|
2716
|
+
return () => this.#reconnectionHandlers.delete(handler);
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Register a handler for connection state changes.
|
|
2720
|
+
*
|
|
2721
|
+
* @param handler - Function called when state changes
|
|
2722
|
+
* @returns Unsubscribe function to remove the handler
|
|
2723
|
+
*/
|
|
2724
|
+
onStateChange(handler) {
|
|
2725
|
+
return this.#connection.onStateChange(handler);
|
|
2726
|
+
}
|
|
2727
|
+
// ===========================================================================
|
|
2728
|
+
// Internal
|
|
2729
|
+
// ===========================================================================
|
|
2730
|
+
/**
|
|
2731
|
+
* Handle incoming notifications
|
|
2732
|
+
*/
|
|
2733
|
+
async #handleNotification(method, params) {
|
|
2734
|
+
switch (method) {
|
|
2735
|
+
case NOTIFICATION_METHODS.EVENT: {
|
|
2736
|
+
const eventParams = params;
|
|
2737
|
+
const subscription = this.#subscriptions.get(eventParams.subscriptionId);
|
|
2738
|
+
if (subscription) {
|
|
2739
|
+
subscription._pushEvent(eventParams);
|
|
2740
|
+
}
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2743
|
+
case NOTIFICATION_METHODS.MESSAGE: {
|
|
2744
|
+
const messageParams = params;
|
|
2745
|
+
for (const handler of this.#messageHandlers) {
|
|
2746
|
+
try {
|
|
2747
|
+
await handler(messageParams.message);
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.error("MAP: Message handler error:", error);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
break;
|
|
2753
|
+
}
|
|
2754
|
+
default:
|
|
2755
|
+
console.warn("MAP: Unknown notification:", method);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Emit a reconnection event to all registered handlers
|
|
2760
|
+
*/
|
|
2761
|
+
#emitReconnectionEvent(event) {
|
|
2762
|
+
for (const handler of this.#reconnectionHandlers) {
|
|
2763
|
+
try {
|
|
2764
|
+
handler(event);
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
console.error("MAP: Reconnection event handler error:", error);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Handle disconnect when auto-reconnect is enabled
|
|
2772
|
+
*/
|
|
2773
|
+
async #handleDisconnect() {
|
|
2774
|
+
this.#isReconnecting = true;
|
|
2775
|
+
this.#connected = false;
|
|
2776
|
+
this.#emitReconnectionEvent({ type: "disconnected" });
|
|
2777
|
+
try {
|
|
2778
|
+
await this.#attemptReconnect();
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
this.#isReconnecting = false;
|
|
2781
|
+
this.#emitReconnectionEvent({
|
|
2782
|
+
type: "reconnectFailed",
|
|
2783
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Attempt to reconnect with retry logic
|
|
2789
|
+
*/
|
|
2790
|
+
async #attemptReconnect() {
|
|
2791
|
+
const options = this.#options.reconnection;
|
|
2792
|
+
const createStream = this.#options.createStream;
|
|
2793
|
+
const retryPolicy = {
|
|
2794
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
2795
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
2796
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
2797
|
+
jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
|
|
2798
|
+
};
|
|
2799
|
+
const scopesToRestore = Array.from(this.#scopeMemberships);
|
|
2800
|
+
await withRetry(
|
|
2801
|
+
async () => {
|
|
2802
|
+
const newStream = await createStream();
|
|
2803
|
+
await this.#connection.reconnect(newStream);
|
|
2804
|
+
const result = await this.connect({
|
|
2805
|
+
agentId: this.#agentId ?? this.#lastConnectOptions?.agentId,
|
|
2806
|
+
auth: this.#lastConnectOptions?.auth
|
|
2807
|
+
});
|
|
2808
|
+
this.#agentId = result.agent.id;
|
|
2809
|
+
this.#sessionId = result.connection.sessionId;
|
|
2810
|
+
this.#serverCapabilities = result.connection.capabilities;
|
|
2811
|
+
this.#currentState = result.agent.state;
|
|
2812
|
+
},
|
|
2813
|
+
retryPolicy,
|
|
2814
|
+
{
|
|
2815
|
+
onRetry: (state) => {
|
|
2816
|
+
this.#emitReconnectionEvent({
|
|
2817
|
+
type: "reconnecting",
|
|
2818
|
+
attempt: state.attempt,
|
|
2819
|
+
delay: state.nextDelayMs,
|
|
2820
|
+
error: state.lastError
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
);
|
|
2825
|
+
this.#isReconnecting = false;
|
|
2826
|
+
this.#emitReconnectionEvent({ type: "reconnected" });
|
|
2827
|
+
if (options.restoreScopeMemberships !== false) {
|
|
2828
|
+
await this.#restoreScopeMemberships(scopesToRestore);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Restore scope memberships after reconnection
|
|
2833
|
+
*/
|
|
2834
|
+
async #restoreScopeMemberships(scopes) {
|
|
2835
|
+
this.#scopeMemberships.clear();
|
|
2836
|
+
for (const scopeId of scopes) {
|
|
2837
|
+
try {
|
|
2838
|
+
await this.joinScope(scopeId);
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
console.warn("MAP: Failed to restore scope membership:", scopeId, error);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
|
|
2846
|
+
// src/federation/envelope.ts
|
|
2847
|
+
function createFederationEnvelope(payload, sourceSystem, targetSystem, options) {
|
|
2848
|
+
return {
|
|
2849
|
+
payload,
|
|
2850
|
+
federation: {
|
|
2851
|
+
sourceSystem,
|
|
2852
|
+
targetSystem,
|
|
2853
|
+
hopCount: 0,
|
|
2854
|
+
maxHops: options?.maxHops,
|
|
2855
|
+
path: options?.trackPath ? [sourceSystem] : void 0,
|
|
2856
|
+
originTimestamp: Date.now(),
|
|
2857
|
+
correlationId: options?.correlationId
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
function processFederationEnvelope(envelope, config) {
|
|
2862
|
+
const { federation } = envelope;
|
|
2863
|
+
const maxHops = federation.maxHops ?? config.maxHops ?? 10;
|
|
2864
|
+
if (federation.hopCount >= maxHops) {
|
|
2865
|
+
return {
|
|
2866
|
+
success: false,
|
|
2867
|
+
errorCode: ERROR_CODES.FEDERATION_MAX_HOPS_EXCEEDED,
|
|
2868
|
+
errorMessage: `Message exceeded maximum hop count of ${maxHops}`
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
if (federation.path?.includes(config.systemId)) {
|
|
2872
|
+
return {
|
|
2873
|
+
success: false,
|
|
2874
|
+
errorCode: ERROR_CODES.FEDERATION_LOOP_DETECTED,
|
|
2875
|
+
errorMessage: `Loop detected: message already visited ${config.systemId}`
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
if (config.allowedSources && !config.allowedSources.includes(federation.sourceSystem)) {
|
|
2879
|
+
return {
|
|
2880
|
+
success: false,
|
|
2881
|
+
errorCode: ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
2882
|
+
errorMessage: `Source system ${federation.sourceSystem} not in allowed sources`
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
if (config.allowedTargets && !config.allowedTargets.includes(federation.targetSystem)) {
|
|
2886
|
+
return {
|
|
2887
|
+
success: false,
|
|
2888
|
+
errorCode: ERROR_CODES.FEDERATION_ROUTE_REJECTED,
|
|
2889
|
+
errorMessage: `Target system ${federation.targetSystem} not in allowed targets`
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
return {
|
|
2893
|
+
success: true,
|
|
2894
|
+
envelope: {
|
|
2895
|
+
payload: envelope.payload,
|
|
2896
|
+
federation: {
|
|
2897
|
+
...federation,
|
|
2898
|
+
hopCount: federation.hopCount + 1,
|
|
2899
|
+
path: config.trackPath ? [...federation.path ?? [], config.systemId] : federation.path
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function isEnvelopeAtDestination(envelope, currentSystemId) {
|
|
2905
|
+
return envelope.federation.targetSystem === currentSystemId;
|
|
2906
|
+
}
|
|
2907
|
+
function unwrapEnvelope(envelope) {
|
|
2908
|
+
return envelope.payload;
|
|
2909
|
+
}
|
|
2910
|
+
function getEnvelopeRoutingInfo(envelope) {
|
|
2911
|
+
const { federation } = envelope;
|
|
2912
|
+
return {
|
|
2913
|
+
source: federation.sourceSystem,
|
|
2914
|
+
target: federation.targetSystem,
|
|
2915
|
+
hops: federation.hopCount,
|
|
2916
|
+
path: federation.path,
|
|
2917
|
+
age: Date.now() - federation.originTimestamp,
|
|
2918
|
+
correlationId: federation.correlationId
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
function isValidEnvelope(obj) {
|
|
2922
|
+
if (!obj || typeof obj !== "object") return false;
|
|
2923
|
+
const envelope = obj;
|
|
2924
|
+
if (!("payload" in envelope) || !("federation" in envelope)) return false;
|
|
2925
|
+
const federation = envelope.federation;
|
|
2926
|
+
if (!federation || typeof federation !== "object") return false;
|
|
2927
|
+
return typeof federation.sourceSystem === "string" && typeof federation.targetSystem === "string" && typeof federation.hopCount === "number" && typeof federation.originTimestamp === "number";
|
|
2928
|
+
}
|
|
2929
|
+
function withPayload(envelope, newPayload) {
|
|
2930
|
+
return {
|
|
2931
|
+
payload: newPayload,
|
|
2932
|
+
federation: envelope.federation
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/federation/buffer.ts
|
|
2937
|
+
var DEFAULT_CONFIG = {
|
|
2938
|
+
enabled: true,
|
|
2939
|
+
maxMessages: 1e3,
|
|
2940
|
+
maxBytes: 10 * 1024 * 1024,
|
|
2941
|
+
// 10MB
|
|
2942
|
+
retentionMs: 60 * 60 * 1e3,
|
|
2943
|
+
// 1 hour
|
|
2944
|
+
overflowStrategy: "drop-oldest"
|
|
2945
|
+
};
|
|
2946
|
+
var FederationOutageBuffer = class {
|
|
2947
|
+
#config;
|
|
2948
|
+
#buffers = /* @__PURE__ */ new Map();
|
|
2949
|
+
constructor(config) {
|
|
2950
|
+
this.#config = { ...DEFAULT_CONFIG, ...config };
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Whether buffering is enabled.
|
|
2954
|
+
*/
|
|
2955
|
+
get enabled() {
|
|
2956
|
+
return this.#config.enabled;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Get the configuration.
|
|
2960
|
+
*/
|
|
2961
|
+
get config() {
|
|
2962
|
+
return this.#config;
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Enqueue a message for a peer.
|
|
2966
|
+
*
|
|
2967
|
+
* @param peerId - Target peer system ID
|
|
2968
|
+
* @param envelope - Message envelope to buffer
|
|
2969
|
+
* @returns true if message was buffered, false if rejected
|
|
2970
|
+
*/
|
|
2971
|
+
enqueue(peerId, envelope) {
|
|
2972
|
+
if (!this.#config.enabled) return false;
|
|
2973
|
+
let buffer = this.#buffers.get(peerId);
|
|
2974
|
+
if (!buffer) {
|
|
2975
|
+
buffer = { messages: [], totalEnqueued: 0, totalDropped: 0, totalBytes: 0 };
|
|
2976
|
+
this.#buffers.set(peerId, buffer);
|
|
2977
|
+
}
|
|
2978
|
+
this.#evictExpired(buffer);
|
|
2979
|
+
const messageSize = this.#estimateSize(envelope);
|
|
2980
|
+
while (buffer.totalBytes + messageSize > this.#config.maxBytes && buffer.messages.length > 0) {
|
|
2981
|
+
const removed = buffer.messages.shift();
|
|
2982
|
+
buffer.totalBytes -= removed.size;
|
|
2983
|
+
buffer.totalDropped++;
|
|
2984
|
+
}
|
|
2985
|
+
if (buffer.messages.length >= this.#config.maxMessages) {
|
|
2986
|
+
switch (this.#config.overflowStrategy) {
|
|
2987
|
+
case "drop-oldest": {
|
|
2988
|
+
const removed = buffer.messages.shift();
|
|
2989
|
+
buffer.totalBytes -= removed.size;
|
|
2990
|
+
buffer.totalDropped++;
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
case "drop-newest":
|
|
2994
|
+
buffer.totalDropped++;
|
|
2995
|
+
return false;
|
|
2996
|
+
case "reject":
|
|
2997
|
+
return false;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
buffer.messages.push({
|
|
3001
|
+
envelope,
|
|
3002
|
+
enqueuedAt: Date.now(),
|
|
3003
|
+
size: messageSize
|
|
3004
|
+
});
|
|
3005
|
+
buffer.totalEnqueued++;
|
|
3006
|
+
buffer.totalBytes += messageSize;
|
|
3007
|
+
return true;
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Drain all buffered messages for a peer.
|
|
3011
|
+
*
|
|
3012
|
+
* Returns messages in FIFO order and clears the buffer.
|
|
3013
|
+
*
|
|
3014
|
+
* @param peerId - Target peer system ID
|
|
3015
|
+
* @returns Array of buffered envelopes
|
|
3016
|
+
*/
|
|
3017
|
+
drain(peerId) {
|
|
3018
|
+
const buffer = this.#buffers.get(peerId);
|
|
3019
|
+
if (!buffer) return [];
|
|
3020
|
+
this.#evictExpired(buffer);
|
|
3021
|
+
const messages = buffer.messages.map((m) => m.envelope);
|
|
3022
|
+
buffer.messages = [];
|
|
3023
|
+
buffer.totalBytes = 0;
|
|
3024
|
+
return messages;
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Peek at buffered messages without removing them.
|
|
3028
|
+
*
|
|
3029
|
+
* @param peerId - Target peer system ID
|
|
3030
|
+
* @returns Array of buffered envelopes (still in buffer)
|
|
3031
|
+
*/
|
|
3032
|
+
peek(peerId) {
|
|
3033
|
+
const buffer = this.#buffers.get(peerId);
|
|
3034
|
+
if (!buffer) return [];
|
|
3035
|
+
this.#evictExpired(buffer);
|
|
3036
|
+
return buffer.messages.map((m) => m.envelope);
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Get statistics for all peer buffers.
|
|
3040
|
+
*
|
|
3041
|
+
* @returns Map of peer ID to buffer stats
|
|
3042
|
+
*/
|
|
3043
|
+
stats() {
|
|
3044
|
+
const result = /* @__PURE__ */ new Map();
|
|
3045
|
+
const now = Date.now();
|
|
3046
|
+
for (const [peerId, buffer] of this.#buffers) {
|
|
3047
|
+
this.#evictExpired(buffer);
|
|
3048
|
+
const oldestAge = buffer.messages.length > 0 ? now - buffer.messages[0].enqueuedAt : 0;
|
|
3049
|
+
result.set(peerId, {
|
|
3050
|
+
count: buffer.messages.length,
|
|
3051
|
+
oldestAge,
|
|
3052
|
+
totalEnqueued: buffer.totalEnqueued,
|
|
3053
|
+
totalDropped: buffer.totalDropped,
|
|
3054
|
+
totalBytes: buffer.totalBytes
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
return result;
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Get count for a specific peer.
|
|
3061
|
+
*
|
|
3062
|
+
* @param peerId - Target peer system ID
|
|
3063
|
+
* @returns Number of buffered messages
|
|
3064
|
+
*/
|
|
3065
|
+
count(peerId) {
|
|
3066
|
+
const buffer = this.#buffers.get(peerId);
|
|
3067
|
+
if (!buffer) return 0;
|
|
3068
|
+
this.#evictExpired(buffer);
|
|
3069
|
+
return buffer.messages.length;
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Check if buffer has messages for a peer.
|
|
3073
|
+
*
|
|
3074
|
+
* @param peerId - Target peer system ID
|
|
3075
|
+
* @returns true if there are buffered messages
|
|
3076
|
+
*/
|
|
3077
|
+
has(peerId) {
|
|
3078
|
+
return this.count(peerId) > 0;
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Clear buffer for a specific peer.
|
|
3082
|
+
*
|
|
3083
|
+
* @param peerId - Target peer system ID
|
|
3084
|
+
*/
|
|
3085
|
+
clear(peerId) {
|
|
3086
|
+
this.#buffers.delete(peerId);
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Clear all buffers.
|
|
3090
|
+
*/
|
|
3091
|
+
clearAll() {
|
|
3092
|
+
this.#buffers.clear();
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Get list of peers with buffered messages.
|
|
3096
|
+
*
|
|
3097
|
+
* @returns Array of peer IDs
|
|
3098
|
+
*/
|
|
3099
|
+
peers() {
|
|
3100
|
+
const result = [];
|
|
3101
|
+
for (const [peerId, buffer] of this.#buffers) {
|
|
3102
|
+
this.#evictExpired(buffer);
|
|
3103
|
+
if (buffer.messages.length > 0) {
|
|
3104
|
+
result.push(peerId);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
return result;
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Evict expired messages from a buffer.
|
|
3111
|
+
*/
|
|
3112
|
+
#evictExpired(buffer) {
|
|
3113
|
+
const cutoff = Date.now() - this.#config.retentionMs;
|
|
3114
|
+
let removed = 0;
|
|
3115
|
+
let bytesRemoved = 0;
|
|
3116
|
+
while (buffer.messages.length > 0 && buffer.messages[0].enqueuedAt < cutoff) {
|
|
3117
|
+
const msg = buffer.messages.shift();
|
|
3118
|
+
bytesRemoved += msg.size;
|
|
3119
|
+
removed++;
|
|
3120
|
+
}
|
|
3121
|
+
buffer.totalDropped += removed;
|
|
3122
|
+
buffer.totalBytes -= bytesRemoved;
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Estimate the size of an envelope in bytes.
|
|
3126
|
+
*/
|
|
3127
|
+
#estimateSize(envelope) {
|
|
3128
|
+
try {
|
|
3129
|
+
return JSON.stringify(envelope).length * 2;
|
|
3130
|
+
} catch {
|
|
3131
|
+
return 1024;
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
3135
|
+
|
|
3136
|
+
// src/connection/gateway.ts
|
|
3137
|
+
var GatewayConnection = class {
|
|
3138
|
+
#connection;
|
|
3139
|
+
#options;
|
|
3140
|
+
#connectedSystems = /* @__PURE__ */ new Map();
|
|
3141
|
+
#reconnectionHandlers = /* @__PURE__ */ new Set();
|
|
3142
|
+
#outageBuffer;
|
|
3143
|
+
#lastSyncTimestamps = /* @__PURE__ */ new Map();
|
|
3144
|
+
#sessionId = null;
|
|
3145
|
+
#serverCapabilities = null;
|
|
3146
|
+
#connected = false;
|
|
3147
|
+
#isReconnecting = false;
|
|
3148
|
+
#lastConnectOptions;
|
|
3149
|
+
constructor(stream, options = {}) {
|
|
3150
|
+
this.#connection = new BaseConnection(stream, options);
|
|
3151
|
+
this.#options = options;
|
|
3152
|
+
this.#outageBuffer = options.buffer?.enabled ? new FederationOutageBuffer(options.buffer) : null;
|
|
3153
|
+
if (options.reconnection?.enabled && options.createStream) {
|
|
3154
|
+
this.#connection.onStateChange((newState) => {
|
|
3155
|
+
if (newState === "closed" && this.#connected && !this.#isReconnecting) {
|
|
3156
|
+
void this.#handleDisconnect();
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
// ===========================================================================
|
|
3162
|
+
// Connection Lifecycle
|
|
3163
|
+
// ===========================================================================
|
|
3164
|
+
/**
|
|
3165
|
+
* Connect to the local MAP system
|
|
3166
|
+
*/
|
|
3167
|
+
async connect(options) {
|
|
3168
|
+
const params = {
|
|
3169
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3170
|
+
participantType: "gateway",
|
|
3171
|
+
name: this.#options.name,
|
|
3172
|
+
capabilities: this.#options.capabilities,
|
|
3173
|
+
auth: options?.auth
|
|
3174
|
+
};
|
|
3175
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.CONNECT, params);
|
|
3176
|
+
this.#sessionId = result.sessionId;
|
|
3177
|
+
this.#serverCapabilities = result.capabilities;
|
|
3178
|
+
this.#connected = true;
|
|
3179
|
+
this.#connection._transitionTo("connected");
|
|
3180
|
+
this.#lastConnectOptions = options;
|
|
3181
|
+
return result;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Disconnect from the local MAP system
|
|
3185
|
+
*/
|
|
3186
|
+
async disconnect(reason) {
|
|
3187
|
+
if (!this.#connected) return;
|
|
3188
|
+
try {
|
|
3189
|
+
await this.#connection.sendRequest(
|
|
3190
|
+
CORE_METHODS.DISCONNECT,
|
|
3191
|
+
reason ? { reason } : void 0
|
|
3192
|
+
);
|
|
3193
|
+
} finally {
|
|
3194
|
+
await this.#connection.close();
|
|
3195
|
+
this.#connected = false;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Whether the gateway is connected to the local system
|
|
3200
|
+
*/
|
|
3201
|
+
get isConnected() {
|
|
3202
|
+
return this.#connected && !this.#connection.isClosed;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Current session ID
|
|
3206
|
+
*/
|
|
3207
|
+
get sessionId() {
|
|
3208
|
+
return this.#sessionId;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Server capabilities
|
|
3212
|
+
*/
|
|
3213
|
+
get serverCapabilities() {
|
|
3214
|
+
return this.#serverCapabilities;
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* List of connected remote systems
|
|
3218
|
+
*/
|
|
3219
|
+
get connectedSystems() {
|
|
3220
|
+
return new Map(this.#connectedSystems);
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* AbortSignal that triggers when the connection closes
|
|
3224
|
+
*/
|
|
3225
|
+
get signal() {
|
|
3226
|
+
return this.#connection.signal;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Promise that resolves when the connection closes
|
|
3230
|
+
*/
|
|
3231
|
+
get closed() {
|
|
3232
|
+
return this.#connection.closed;
|
|
3233
|
+
}
|
|
3234
|
+
// ===========================================================================
|
|
3235
|
+
// Federation
|
|
3236
|
+
// ===========================================================================
|
|
3237
|
+
/**
|
|
3238
|
+
* Connect to a remote MAP system
|
|
3239
|
+
*/
|
|
3240
|
+
async connectToSystem(systemId, endpoint, auth) {
|
|
3241
|
+
const params = {
|
|
3242
|
+
systemId,
|
|
3243
|
+
endpoint,
|
|
3244
|
+
auth
|
|
3245
|
+
};
|
|
3246
|
+
const result = await this.#connection.sendRequest(FEDERATION_METHODS.FEDERATION_CONNECT, params);
|
|
3247
|
+
if (result.connected && result.systemInfo) {
|
|
3248
|
+
this.#connectedSystems.set(systemId, {
|
|
3249
|
+
name: result.systemInfo.name,
|
|
3250
|
+
version: result.systemInfo.version
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
return result;
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Route a message to a remote system.
|
|
3257
|
+
*
|
|
3258
|
+
* If routing config is provided, wraps the message in a federation envelope
|
|
3259
|
+
* with proper metadata for multi-hop routing. Otherwise, sends raw message
|
|
3260
|
+
* for backwards compatibility.
|
|
3261
|
+
*
|
|
3262
|
+
* During reconnection, messages are buffered if buffer is configured.
|
|
3263
|
+
*/
|
|
3264
|
+
async routeToSystem(systemId, message) {
|
|
3265
|
+
let envelope;
|
|
3266
|
+
if (this.#options.routing) {
|
|
3267
|
+
envelope = createFederationEnvelope(
|
|
3268
|
+
message,
|
|
3269
|
+
this.#options.routing.systemId,
|
|
3270
|
+
systemId,
|
|
3271
|
+
{
|
|
3272
|
+
maxHops: this.#options.routing.maxHops,
|
|
3273
|
+
trackPath: this.#options.routing.trackPath
|
|
3274
|
+
}
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
if (this.#isReconnecting && this.#outageBuffer && envelope) {
|
|
3278
|
+
const buffered = this.#outageBuffer.enqueue(systemId, envelope);
|
|
3279
|
+
if (!buffered) {
|
|
3280
|
+
this.#emitReconnectionEvent({
|
|
3281
|
+
type: "bufferOverflow",
|
|
3282
|
+
peerId: systemId,
|
|
3283
|
+
messagesBuffered: this.#outageBuffer.count(systemId)
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
return { routed: false };
|
|
3287
|
+
}
|
|
3288
|
+
const params = { systemId };
|
|
3289
|
+
if (envelope) {
|
|
3290
|
+
params.envelope = envelope;
|
|
3291
|
+
} else {
|
|
3292
|
+
params.message = message;
|
|
3293
|
+
}
|
|
3294
|
+
const result = await this.#connection.sendRequest(FEDERATION_METHODS.FEDERATION_ROUTE, params);
|
|
3295
|
+
if (result.routed) {
|
|
3296
|
+
this.#lastSyncTimestamps.set(systemId, Date.now());
|
|
3297
|
+
}
|
|
3298
|
+
return result;
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Check if a remote system is connected
|
|
3302
|
+
*/
|
|
3303
|
+
isSystemConnected(systemId) {
|
|
3304
|
+
return this.#connectedSystems.has(systemId);
|
|
3305
|
+
}
|
|
3306
|
+
// ===========================================================================
|
|
3307
|
+
// Reconnection
|
|
3308
|
+
// ===========================================================================
|
|
3309
|
+
/**
|
|
3310
|
+
* Current connection state
|
|
3311
|
+
*/
|
|
3312
|
+
get state() {
|
|
3313
|
+
return this.#connection.state;
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Whether the connection is currently reconnecting
|
|
3317
|
+
*/
|
|
3318
|
+
get isReconnecting() {
|
|
3319
|
+
return this.#isReconnecting;
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Get the outage buffer (for advanced use)
|
|
3323
|
+
*/
|
|
3324
|
+
get outageBuffer() {
|
|
3325
|
+
return this.#outageBuffer;
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Get last sync timestamp for a peer
|
|
3329
|
+
*/
|
|
3330
|
+
getLastSyncTimestamp(peerId) {
|
|
3331
|
+
return this.#lastSyncTimestamps.get(peerId);
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Register a handler for reconnection events.
|
|
3335
|
+
*
|
|
3336
|
+
* @param handler - Function called when reconnection events occur
|
|
3337
|
+
* @returns Unsubscribe function to remove the handler
|
|
3338
|
+
*/
|
|
3339
|
+
onReconnection(handler) {
|
|
3340
|
+
this.#reconnectionHandlers.add(handler);
|
|
3341
|
+
return () => this.#reconnectionHandlers.delete(handler);
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Register a handler for connection state changes.
|
|
3345
|
+
*
|
|
3346
|
+
* @param handler - Function called when state changes
|
|
3347
|
+
* @returns Unsubscribe function to remove the handler
|
|
3348
|
+
*/
|
|
3349
|
+
onStateChange(handler) {
|
|
3350
|
+
return this.#connection.onStateChange(handler);
|
|
3351
|
+
}
|
|
3352
|
+
// ===========================================================================
|
|
3353
|
+
// Internal
|
|
3354
|
+
// ===========================================================================
|
|
3355
|
+
/**
|
|
3356
|
+
* Emit a reconnection event to all registered handlers
|
|
3357
|
+
*/
|
|
3358
|
+
#emitReconnectionEvent(event) {
|
|
3359
|
+
for (const handler of this.#reconnectionHandlers) {
|
|
3360
|
+
try {
|
|
3361
|
+
handler(event);
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
console.error("MAP: Gateway reconnection event handler error:", error);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Handle disconnect when auto-reconnect is enabled
|
|
3369
|
+
*/
|
|
3370
|
+
async #handleDisconnect() {
|
|
3371
|
+
this.#isReconnecting = true;
|
|
3372
|
+
this.#connected = false;
|
|
3373
|
+
this.#emitReconnectionEvent({ type: "disconnected" });
|
|
3374
|
+
try {
|
|
3375
|
+
await this.#attemptReconnect();
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
this.#isReconnecting = false;
|
|
3378
|
+
this.#emitReconnectionEvent({
|
|
3379
|
+
type: "reconnectFailed",
|
|
3380
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Attempt to reconnect with retry logic
|
|
3386
|
+
*/
|
|
3387
|
+
async #attemptReconnect() {
|
|
3388
|
+
const options = this.#options.reconnection;
|
|
3389
|
+
const createStream = this.#options.createStream;
|
|
3390
|
+
const retryPolicy = {
|
|
3391
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
3392
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_POLICY.baseDelayMs,
|
|
3393
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
3394
|
+
jitter: options.jitter ?? DEFAULT_RETRY_POLICY.jitter
|
|
3395
|
+
};
|
|
3396
|
+
await withRetry(
|
|
3397
|
+
async () => {
|
|
3398
|
+
const newStream = await createStream();
|
|
3399
|
+
await this.#connection.reconnect(newStream);
|
|
3400
|
+
const connectResult = await this.connect(this.#lastConnectOptions);
|
|
3401
|
+
this.#sessionId = connectResult.sessionId;
|
|
3402
|
+
this.#serverCapabilities = connectResult.capabilities;
|
|
3403
|
+
},
|
|
3404
|
+
retryPolicy,
|
|
3405
|
+
{
|
|
3406
|
+
onRetry: (state) => {
|
|
3407
|
+
this.#emitReconnectionEvent({
|
|
3408
|
+
type: "reconnecting",
|
|
3409
|
+
attempt: state.attempt,
|
|
3410
|
+
delay: state.nextDelayMs,
|
|
3411
|
+
error: state.lastError
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
);
|
|
3416
|
+
this.#isReconnecting = false;
|
|
3417
|
+
this.#emitReconnectionEvent({ type: "reconnected" });
|
|
3418
|
+
await this.#drainBufferedMessages();
|
|
3419
|
+
await this.#replayFromPeers();
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Drain buffered messages after reconnection
|
|
3423
|
+
*/
|
|
3424
|
+
async #drainBufferedMessages() {
|
|
3425
|
+
if (!this.#outageBuffer) return;
|
|
3426
|
+
const peers = this.#outageBuffer.peers();
|
|
3427
|
+
for (const peerId of peers) {
|
|
3428
|
+
const messages = this.#outageBuffer.drain(peerId);
|
|
3429
|
+
if (messages.length === 0) continue;
|
|
3430
|
+
for (const envelope of messages) {
|
|
3431
|
+
try {
|
|
3432
|
+
const params = {
|
|
3433
|
+
systemId: peerId,
|
|
3434
|
+
envelope
|
|
3435
|
+
};
|
|
3436
|
+
await this.#connection.sendRequest(FEDERATION_METHODS.FEDERATION_ROUTE, params);
|
|
3437
|
+
} catch (error) {
|
|
3438
|
+
console.warn("MAP: Failed to send buffered message to", peerId, error);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
this.#emitReconnectionEvent({
|
|
3442
|
+
type: "bufferDrained",
|
|
3443
|
+
peerId,
|
|
3444
|
+
messagesDrained: messages.length
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Replay missed events from peers after reconnection
|
|
3450
|
+
*/
|
|
3451
|
+
async #replayFromPeers() {
|
|
3452
|
+
const replayOptions = this.#options.replay;
|
|
3453
|
+
if (!replayOptions?.enabled) return;
|
|
3454
|
+
const peersToReplay = Array.from(this.#lastSyncTimestamps.entries());
|
|
3455
|
+
if (peersToReplay.length === 0) return;
|
|
3456
|
+
for (const [peerId, lastSync] of peersToReplay) {
|
|
3457
|
+
try {
|
|
3458
|
+
await this.#replayFromPeer(peerId, lastSync);
|
|
3459
|
+
} catch (error) {
|
|
3460
|
+
console.warn("MAP: Failed to replay events from peer", peerId, error);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Replay missed events from a single peer
|
|
3466
|
+
*/
|
|
3467
|
+
async #replayFromPeer(peerId, lastSyncTimestamp) {
|
|
3468
|
+
const replayOptions = this.#options.replay;
|
|
3469
|
+
const maxAge = replayOptions.maxAgeMs ?? 60 * 60 * 1e3;
|
|
3470
|
+
const cutoff = Date.now() - maxAge;
|
|
3471
|
+
if (lastSyncTimestamp < cutoff) {
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
this.#emitReconnectionEvent({ type: "replayStarted", peerId });
|
|
3475
|
+
let totalReplayed = 0;
|
|
3476
|
+
const maxEvents = replayOptions.maxEvents ?? 1e3;
|
|
3477
|
+
let afterEventId;
|
|
3478
|
+
let hasMore = true;
|
|
3479
|
+
while (hasMore && totalReplayed < maxEvents) {
|
|
3480
|
+
const params = {
|
|
3481
|
+
limit: Math.min(100, maxEvents - totalReplayed)
|
|
3482
|
+
};
|
|
3483
|
+
if (afterEventId) {
|
|
3484
|
+
params.afterEventId = afterEventId;
|
|
3485
|
+
} else {
|
|
3486
|
+
params.fromTimestamp = lastSyncTimestamp;
|
|
3487
|
+
}
|
|
3488
|
+
if (replayOptions.eventTypes) {
|
|
3489
|
+
params.filter = { eventTypes: replayOptions.eventTypes };
|
|
3490
|
+
}
|
|
3491
|
+
const result = await this.#connection.sendRequest(CORE_METHODS.REPLAY, params);
|
|
3492
|
+
totalReplayed += result.events.length;
|
|
3493
|
+
hasMore = result.hasMore;
|
|
3494
|
+
afterEventId = result.events.at(-1)?.eventId;
|
|
3495
|
+
if (result.events.length === 0) {
|
|
3496
|
+
break;
|
|
3497
|
+
}
|
|
3498
|
+
const lastEvent = result.events.at(-1);
|
|
3499
|
+
if (lastEvent) {
|
|
3500
|
+
this.#lastSyncTimestamps.set(peerId, lastEvent.timestamp);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
this.#emitReconnectionEvent({
|
|
3504
|
+
type: "replayCompleted",
|
|
3505
|
+
peerId,
|
|
3506
|
+
eventsReplayed: totalReplayed
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
var JsonRpcVersionSchema = z.literal("2.0");
|
|
3511
|
+
var RequestIdSchema = z.union([z.string(), z.number().int()]);
|
|
3512
|
+
var ProtocolVersionSchema = z.literal(1);
|
|
3513
|
+
var TimestampSchema = z.number().int();
|
|
3514
|
+
var MetaSchema = z.record(z.unknown()).optional();
|
|
3515
|
+
var ParticipantIdSchema = z.string();
|
|
3516
|
+
var AgentIdSchema = z.string();
|
|
3517
|
+
var ScopeIdSchema = z.string();
|
|
3518
|
+
var SessionIdSchema = z.string();
|
|
3519
|
+
var MessageIdSchema = z.string();
|
|
3520
|
+
var SubscriptionIdSchema = z.string();
|
|
3521
|
+
var CorrelationIdSchema = z.string();
|
|
3522
|
+
var ParticipantTypeSchema = z.enum(["agent", "client", "system", "gateway"]);
|
|
3523
|
+
var TransportTypeSchema = z.enum(["websocket", "stdio", "inprocess", "http-sse"]);
|
|
3524
|
+
var ErrorCategorySchema = z.enum([
|
|
3525
|
+
"protocol",
|
|
3526
|
+
"auth",
|
|
3527
|
+
"routing",
|
|
3528
|
+
"agent",
|
|
3529
|
+
"resource",
|
|
3530
|
+
"federation",
|
|
3531
|
+
"internal"
|
|
3532
|
+
]);
|
|
3533
|
+
var AgentVisibilitySchema = z.enum(["public", "parent-only", "scope", "system"]);
|
|
3534
|
+
var AgentStateSchema = z.union([
|
|
3535
|
+
z.enum(["registered", "idle", "busy", "waiting", "stopping", "stopped", "error"]),
|
|
3536
|
+
z.string().regex(/^x-/)
|
|
3537
|
+
]);
|
|
3538
|
+
var MessagePrioritySchema = z.enum(["low", "normal", "high", "urgent"]);
|
|
3539
|
+
var DeliverySemanticsSchema = z.enum(["at-most-once", "at-least-once", "exactly-once"]);
|
|
3540
|
+
var MessageRelationshipSchema = z.enum([
|
|
3541
|
+
"peer",
|
|
3542
|
+
"parent-to-child",
|
|
3543
|
+
"child-to-parent",
|
|
3544
|
+
"supervisor-to-supervised",
|
|
3545
|
+
"broadcast"
|
|
3546
|
+
]);
|
|
3547
|
+
var EventTypeSchema = z.enum([
|
|
3548
|
+
"agent.registered",
|
|
3549
|
+
"agent.unregistered",
|
|
3550
|
+
"agent.state-changed",
|
|
3551
|
+
"agent.spawned",
|
|
3552
|
+
"scope.created",
|
|
3553
|
+
"scope.deleted",
|
|
3554
|
+
"scope.joined",
|
|
3555
|
+
"scope.left",
|
|
3556
|
+
"message.sent",
|
|
3557
|
+
"message.delivered",
|
|
3558
|
+
"session.started",
|
|
3559
|
+
"session.ended",
|
|
3560
|
+
"system.error",
|
|
3561
|
+
"system.shutdown"
|
|
3562
|
+
]);
|
|
3563
|
+
var ScopeJoinPolicySchema = z.enum(["open", "approval", "invite"]);
|
|
3564
|
+
var ScopeVisibilitySchema = z.enum(["public", "private", "unlisted"]);
|
|
3565
|
+
var MessageVisibilitySchema = z.enum(["members", "public"]);
|
|
3566
|
+
var ScopeSendPolicySchema = z.enum(["anyone", "members"]);
|
|
3567
|
+
var ParticipantCapabilitiesSchema = z.object({
|
|
3568
|
+
observation: z.object({
|
|
3569
|
+
canObserve: z.boolean().optional(),
|
|
3570
|
+
canQuery: z.boolean().optional()
|
|
3571
|
+
}).strict().optional(),
|
|
3572
|
+
messaging: z.object({
|
|
3573
|
+
canSend: z.boolean().optional(),
|
|
3574
|
+
canReceive: z.boolean().optional(),
|
|
3575
|
+
canBroadcast: z.boolean().optional()
|
|
3576
|
+
}).strict().optional(),
|
|
3577
|
+
lifecycle: z.object({
|
|
3578
|
+
canSpawn: z.boolean().optional(),
|
|
3579
|
+
canRegister: z.boolean().optional(),
|
|
3580
|
+
canUnregister: z.boolean().optional(),
|
|
3581
|
+
canSteer: z.boolean().optional(),
|
|
3582
|
+
canStop: z.boolean().optional()
|
|
3583
|
+
}).strict().optional(),
|
|
3584
|
+
scopes: z.object({
|
|
3585
|
+
canCreateScopes: z.boolean().optional(),
|
|
3586
|
+
canManageScopes: z.boolean().optional()
|
|
3587
|
+
}).strict().optional(),
|
|
3588
|
+
_meta: MetaSchema
|
|
3589
|
+
}).strict();
|
|
3590
|
+
var DirectAddressSchema = z.object({ agent: AgentIdSchema }).strict();
|
|
3591
|
+
var MultiAddressSchema = z.object({ agents: z.array(AgentIdSchema).min(1) }).strict();
|
|
3592
|
+
var ScopeAddressSchema = z.object({ scope: ScopeIdSchema }).strict();
|
|
3593
|
+
var RoleAddressSchema = z.object({
|
|
3594
|
+
role: z.string(),
|
|
3595
|
+
scope: ScopeIdSchema.optional()
|
|
3596
|
+
}).strict();
|
|
3597
|
+
var HierarchicalAddressSchema = z.object({
|
|
3598
|
+
parent: z.literal(true).optional(),
|
|
3599
|
+
children: z.literal(true).optional(),
|
|
3600
|
+
siblings: z.literal(true).optional(),
|
|
3601
|
+
ancestors: z.literal(true).optional(),
|
|
3602
|
+
descendants: z.literal(true).optional()
|
|
3603
|
+
}).strict();
|
|
3604
|
+
var BroadcastAddressSchema = z.object({ broadcast: z.literal(true) }).strict();
|
|
3605
|
+
var SystemAddressSchema = z.object({ system: z.literal(true) }).strict();
|
|
3606
|
+
var ParticipantAddressSchema = z.object({ participant: ParticipantIdSchema }).strict();
|
|
3607
|
+
var FederatedAddressSchema = z.object({
|
|
3608
|
+
system: z.string(),
|
|
3609
|
+
address: z.lazy(() => AddressSchema)
|
|
3610
|
+
}).strict();
|
|
3611
|
+
var AddressSchema = z.union([
|
|
3612
|
+
DirectAddressSchema,
|
|
3613
|
+
MultiAddressSchema,
|
|
3614
|
+
ScopeAddressSchema,
|
|
3615
|
+
RoleAddressSchema,
|
|
3616
|
+
HierarchicalAddressSchema,
|
|
3617
|
+
BroadcastAddressSchema,
|
|
3618
|
+
SystemAddressSchema,
|
|
3619
|
+
ParticipantAddressSchema,
|
|
3620
|
+
FederatedAddressSchema
|
|
3621
|
+
]);
|
|
3622
|
+
var AgentRelationshipSchema = z.object({
|
|
3623
|
+
type: z.enum(["peer", "supervisor", "supervised", "collaborator"]),
|
|
3624
|
+
agentId: AgentIdSchema,
|
|
3625
|
+
metadata: z.record(z.unknown()).optional(),
|
|
3626
|
+
_meta: MetaSchema
|
|
3627
|
+
}).strict();
|
|
3628
|
+
var AgentLifecycleSchema = z.object({
|
|
3629
|
+
createdAt: TimestampSchema.optional(),
|
|
3630
|
+
startedAt: TimestampSchema.optional(),
|
|
3631
|
+
stoppedAt: TimestampSchema.optional(),
|
|
3632
|
+
lastActiveAt: TimestampSchema.optional(),
|
|
3633
|
+
exitCode: z.number().int().optional(),
|
|
3634
|
+
exitReason: z.string().optional(),
|
|
3635
|
+
_meta: MetaSchema
|
|
3636
|
+
}).strict();
|
|
3637
|
+
var AgentSchema = z.object({
|
|
3638
|
+
id: AgentIdSchema,
|
|
3639
|
+
name: z.string().optional(),
|
|
3640
|
+
description: z.string().optional(),
|
|
3641
|
+
parent: AgentIdSchema.optional(),
|
|
3642
|
+
relationships: z.array(AgentRelationshipSchema).optional(),
|
|
3643
|
+
state: AgentStateSchema,
|
|
3644
|
+
role: z.string().optional(),
|
|
3645
|
+
scopes: z.array(ScopeIdSchema).optional(),
|
|
3646
|
+
visibility: AgentVisibilitySchema.optional(),
|
|
3647
|
+
lifecycle: AgentLifecycleSchema.optional(),
|
|
3648
|
+
capabilities: ParticipantCapabilitiesSchema.optional(),
|
|
3649
|
+
metadata: z.record(z.unknown()).optional(),
|
|
3650
|
+
_meta: MetaSchema
|
|
3651
|
+
}).strict();
|
|
3652
|
+
var ScopeSchema = z.object({
|
|
3653
|
+
id: ScopeIdSchema,
|
|
3654
|
+
name: z.string().optional(),
|
|
3655
|
+
parent: ScopeIdSchema.optional(),
|
|
3656
|
+
children: z.array(ScopeIdSchema).optional(),
|
|
3657
|
+
joinPolicy: ScopeJoinPolicySchema.optional(),
|
|
3658
|
+
visibility: ScopeVisibilitySchema.optional(),
|
|
3659
|
+
messageVisibility: MessageVisibilitySchema.optional(),
|
|
3660
|
+
sendPolicy: ScopeSendPolicySchema.optional(),
|
|
3661
|
+
metadata: z.record(z.unknown()).optional(),
|
|
3662
|
+
_meta: MetaSchema
|
|
3663
|
+
}).strict();
|
|
3664
|
+
var MessageMetaSchema = z.object({
|
|
3665
|
+
correlationId: CorrelationIdSchema.optional(),
|
|
3666
|
+
causationId: MessageIdSchema.optional(),
|
|
3667
|
+
traceId: z.string().optional(),
|
|
3668
|
+
spanId: z.string().optional(),
|
|
3669
|
+
priority: MessagePrioritySchema.optional(),
|
|
3670
|
+
delivery: DeliverySemanticsSchema.optional(),
|
|
3671
|
+
relationship: MessageRelationshipSchema.optional(),
|
|
3672
|
+
expiresAt: TimestampSchema.optional(),
|
|
3673
|
+
isResult: z.boolean().optional(),
|
|
3674
|
+
_meta: MetaSchema
|
|
3675
|
+
}).strict();
|
|
3676
|
+
var MessageSchema = z.object({
|
|
3677
|
+
id: MessageIdSchema,
|
|
3678
|
+
from: ParticipantIdSchema,
|
|
3679
|
+
to: AddressSchema,
|
|
3680
|
+
timestamp: TimestampSchema,
|
|
3681
|
+
payload: z.unknown().optional(),
|
|
3682
|
+
meta: MessageMetaSchema.optional(),
|
|
3683
|
+
_meta: MetaSchema
|
|
3684
|
+
}).strict();
|
|
3685
|
+
var EventSchema = z.object({
|
|
3686
|
+
type: EventTypeSchema,
|
|
3687
|
+
timestamp: TimestampSchema,
|
|
3688
|
+
data: z.record(z.unknown()).optional(),
|
|
3689
|
+
_meta: MetaSchema
|
|
3690
|
+
}).strict();
|
|
3691
|
+
var SubscriptionFilterSchema = z.object({
|
|
3692
|
+
eventTypes: z.array(EventTypeSchema).optional(),
|
|
3693
|
+
scopes: z.array(ScopeIdSchema).optional(),
|
|
3694
|
+
agents: z.array(AgentIdSchema).optional(),
|
|
3695
|
+
includeChildren: z.boolean().optional(),
|
|
3696
|
+
_meta: MetaSchema
|
|
3697
|
+
}).strict();
|
|
3698
|
+
var MAPErrorDataSchema = z.object({
|
|
3699
|
+
category: ErrorCategorySchema.optional(),
|
|
3700
|
+
retryable: z.boolean().optional(),
|
|
3701
|
+
retryAfterMs: z.number().int().optional(),
|
|
3702
|
+
details: z.record(z.unknown()).optional(),
|
|
3703
|
+
_meta: MetaSchema
|
|
3704
|
+
}).passthrough();
|
|
3705
|
+
var MAPErrorSchema = z.object({
|
|
3706
|
+
code: z.number().int(),
|
|
3707
|
+
message: z.string(),
|
|
3708
|
+
data: MAPErrorDataSchema.optional()
|
|
3709
|
+
}).strict();
|
|
3710
|
+
var MAPRequestSchema = z.object({
|
|
3711
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
3712
|
+
id: RequestIdSchema,
|
|
3713
|
+
method: z.string(),
|
|
3714
|
+
params: z.record(z.unknown()).optional()
|
|
3715
|
+
}).strict();
|
|
3716
|
+
var MAPResponseSuccessSchema = z.object({
|
|
3717
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
3718
|
+
id: RequestIdSchema,
|
|
3719
|
+
result: z.unknown()
|
|
3720
|
+
}).strict();
|
|
3721
|
+
var MAPResponseErrorSchema = z.object({
|
|
3722
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
3723
|
+
id: RequestIdSchema,
|
|
3724
|
+
error: MAPErrorSchema
|
|
3725
|
+
}).strict();
|
|
3726
|
+
var MAPResponseSchema = z.union([MAPResponseSuccessSchema, MAPResponseErrorSchema]);
|
|
3727
|
+
var MAPNotificationSchema = z.object({
|
|
3728
|
+
jsonrpc: JsonRpcVersionSchema,
|
|
3729
|
+
method: z.string(),
|
|
3730
|
+
params: z.record(z.unknown()).optional()
|
|
3731
|
+
}).strict();
|
|
3732
|
+
|
|
3733
|
+
// src/protocol/index.ts
|
|
3734
|
+
var METHOD_REGISTRY = {
|
|
3735
|
+
// Core methods
|
|
3736
|
+
"connect": {
|
|
3737
|
+
method: "map/connect",
|
|
3738
|
+
category: "core",
|
|
3739
|
+
capabilities: [],
|
|
3740
|
+
description: "Establish connection to MAP system"
|
|
3741
|
+
},
|
|
3742
|
+
"disconnect": {
|
|
3743
|
+
method: "map/disconnect",
|
|
3744
|
+
category: "core",
|
|
3745
|
+
capabilities: [],
|
|
3746
|
+
description: "Disconnect from MAP system"
|
|
3747
|
+
},
|
|
3748
|
+
"send": {
|
|
3749
|
+
method: "map/send",
|
|
3750
|
+
category: "core",
|
|
3751
|
+
capabilities: ["messaging.canSend"],
|
|
3752
|
+
description: "Send a message to an address"
|
|
3753
|
+
},
|
|
3754
|
+
"subscribe": {
|
|
3755
|
+
method: "map/subscribe",
|
|
3756
|
+
category: "core",
|
|
3757
|
+
capabilities: ["observation.canObserve"],
|
|
3758
|
+
description: "Subscribe to event stream"
|
|
3759
|
+
},
|
|
3760
|
+
"unsubscribe": {
|
|
3761
|
+
method: "map/unsubscribe",
|
|
3762
|
+
category: "core",
|
|
3763
|
+
capabilities: ["observation.canObserve"],
|
|
3764
|
+
description: "Unsubscribe from event stream"
|
|
3765
|
+
},
|
|
3766
|
+
"replay": {
|
|
3767
|
+
method: "map/replay",
|
|
3768
|
+
category: "core",
|
|
3769
|
+
capabilities: ["observation.canObserve"],
|
|
3770
|
+
description: "Replay historical events with filtering and pagination"
|
|
3771
|
+
},
|
|
3772
|
+
// Observation methods
|
|
3773
|
+
"agents/list": {
|
|
3774
|
+
method: "map/agents/list",
|
|
3775
|
+
category: "observation",
|
|
3776
|
+
capabilities: ["observation.canQuery"],
|
|
3777
|
+
description: "List agents with optional filters"
|
|
3778
|
+
},
|
|
3779
|
+
"agents/get": {
|
|
3780
|
+
method: "map/agents/get",
|
|
3781
|
+
category: "observation",
|
|
3782
|
+
capabilities: ["observation.canQuery"],
|
|
3783
|
+
description: "Get agent by ID with optional hierarchy"
|
|
3784
|
+
},
|
|
3785
|
+
"scopes/list": {
|
|
3786
|
+
method: "map/scopes/list",
|
|
3787
|
+
category: "observation",
|
|
3788
|
+
capabilities: ["observation.canQuery"],
|
|
3789
|
+
description: "List all scopes"
|
|
3790
|
+
},
|
|
3791
|
+
"scopes/get": {
|
|
3792
|
+
method: "map/scopes/get",
|
|
3793
|
+
category: "observation",
|
|
3794
|
+
capabilities: ["observation.canQuery"],
|
|
3795
|
+
description: "Get scope by ID"
|
|
3796
|
+
},
|
|
3797
|
+
"scopes/members": {
|
|
3798
|
+
method: "map/scopes/members",
|
|
3799
|
+
category: "observation",
|
|
3800
|
+
capabilities: ["observation.canQuery"],
|
|
3801
|
+
description: "List scope members"
|
|
3802
|
+
},
|
|
3803
|
+
"structure/graph": {
|
|
3804
|
+
method: "map/structure/graph",
|
|
3805
|
+
category: "observation",
|
|
3806
|
+
capabilities: ["observation.canQuery"],
|
|
3807
|
+
description: "Get agent hierarchy graph"
|
|
3808
|
+
},
|
|
3809
|
+
// Lifecycle methods
|
|
3810
|
+
"agents/register": {
|
|
3811
|
+
method: "map/agents/register",
|
|
3812
|
+
category: "lifecycle",
|
|
3813
|
+
capabilities: ["lifecycle.canRegister"],
|
|
3814
|
+
description: "Register a new agent"
|
|
3815
|
+
},
|
|
3816
|
+
"agents/unregister": {
|
|
3817
|
+
method: "map/agents/unregister",
|
|
3818
|
+
category: "lifecycle",
|
|
3819
|
+
capabilities: ["lifecycle.canUnregister"],
|
|
3820
|
+
description: "Unregister an agent"
|
|
3821
|
+
},
|
|
3822
|
+
"agents/spawn": {
|
|
3823
|
+
method: "map/agents/spawn",
|
|
3824
|
+
category: "lifecycle",
|
|
3825
|
+
capabilities: ["lifecycle.canSpawn"],
|
|
3826
|
+
description: "Spawn a child agent"
|
|
3827
|
+
},
|
|
3828
|
+
// State methods
|
|
3829
|
+
"agents/update": {
|
|
3830
|
+
method: "map/agents/update",
|
|
3831
|
+
category: "state",
|
|
3832
|
+
capabilities: ["lifecycle.canRegister"],
|
|
3833
|
+
description: "Update agent state or metadata"
|
|
3834
|
+
},
|
|
3835
|
+
"agents/suspend": {
|
|
3836
|
+
method: "map/agents/suspend",
|
|
3837
|
+
category: "state",
|
|
3838
|
+
capabilities: ["lifecycle.canStop"],
|
|
3839
|
+
description: "Suspend an agent"
|
|
3840
|
+
},
|
|
3841
|
+
"agents/resume": {
|
|
3842
|
+
method: "map/agents/resume",
|
|
3843
|
+
category: "state",
|
|
3844
|
+
capabilities: ["lifecycle.canStop"],
|
|
3845
|
+
description: "Resume a suspended agent"
|
|
3846
|
+
},
|
|
3847
|
+
"agents/stop": {
|
|
3848
|
+
method: "map/agents/stop",
|
|
3849
|
+
category: "state",
|
|
3850
|
+
capabilities: ["lifecycle.canStop"],
|
|
3851
|
+
description: "Stop an agent"
|
|
3852
|
+
},
|
|
3853
|
+
// Steering methods
|
|
3854
|
+
"inject": {
|
|
3855
|
+
method: "map/inject",
|
|
3856
|
+
category: "steering",
|
|
3857
|
+
capabilities: ["lifecycle.canSteer"],
|
|
3858
|
+
description: "Inject context into an agent"
|
|
3859
|
+
},
|
|
3860
|
+
// Scope methods
|
|
3861
|
+
"scopes/create": {
|
|
3862
|
+
method: "map/scopes/create",
|
|
3863
|
+
category: "scope",
|
|
3864
|
+
capabilities: ["scopes.canCreateScopes"],
|
|
3865
|
+
description: "Create a new scope"
|
|
3866
|
+
},
|
|
3867
|
+
"scopes/delete": {
|
|
3868
|
+
method: "map/scopes/delete",
|
|
3869
|
+
category: "scope",
|
|
3870
|
+
capabilities: ["scopes.canManageScopes"],
|
|
3871
|
+
description: "Delete a scope"
|
|
3872
|
+
},
|
|
3873
|
+
"scopes/join": {
|
|
3874
|
+
method: "map/scopes/join",
|
|
3875
|
+
category: "scope",
|
|
3876
|
+
capabilities: [],
|
|
3877
|
+
description: "Join a scope"
|
|
3878
|
+
},
|
|
3879
|
+
"scopes/leave": {
|
|
3880
|
+
method: "map/scopes/leave",
|
|
3881
|
+
category: "scope",
|
|
3882
|
+
capabilities: [],
|
|
3883
|
+
description: "Leave a scope"
|
|
3884
|
+
},
|
|
3885
|
+
// Session methods
|
|
3886
|
+
"session/list": {
|
|
3887
|
+
method: "map/session/list",
|
|
3888
|
+
category: "session",
|
|
3889
|
+
capabilities: [],
|
|
3890
|
+
description: "List sessions"
|
|
3891
|
+
},
|
|
3892
|
+
"session/load": {
|
|
3893
|
+
method: "map/session/load",
|
|
3894
|
+
category: "session",
|
|
3895
|
+
capabilities: [],
|
|
3896
|
+
description: "Load a session"
|
|
3897
|
+
},
|
|
3898
|
+
"session/close": {
|
|
3899
|
+
method: "map/session/close",
|
|
3900
|
+
category: "session",
|
|
3901
|
+
capabilities: [],
|
|
3902
|
+
description: "Close a session"
|
|
3903
|
+
},
|
|
3904
|
+
// Auth methods
|
|
3905
|
+
"auth/refresh": {
|
|
3906
|
+
method: "map/auth/refresh",
|
|
3907
|
+
category: "auth",
|
|
3908
|
+
capabilities: [],
|
|
3909
|
+
description: "Refresh authentication token"
|
|
3910
|
+
},
|
|
3911
|
+
// Federation methods
|
|
3912
|
+
"federation/connect": {
|
|
3913
|
+
method: "map/federation/connect",
|
|
3914
|
+
category: "federation",
|
|
3915
|
+
capabilities: ["federation.canFederate"],
|
|
3916
|
+
description: "Connect to federated system"
|
|
3917
|
+
},
|
|
3918
|
+
"federation/route": {
|
|
3919
|
+
method: "map/federation/route",
|
|
3920
|
+
category: "federation",
|
|
3921
|
+
capabilities: ["federation.canFederate"],
|
|
3922
|
+
description: "Route message to federated system"
|
|
3923
|
+
},
|
|
3924
|
+
// Notification methods (client → server)
|
|
3925
|
+
"subscription/ack": {
|
|
3926
|
+
method: "map/subscribe.ack",
|
|
3927
|
+
category: "notification",
|
|
3928
|
+
capabilities: [],
|
|
3929
|
+
description: "Acknowledge received events for backpressure flow control"
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3932
|
+
function getMethodsByCategory(category) {
|
|
3933
|
+
return Object.values(METHOD_REGISTRY).filter((m) => m.category === category);
|
|
3934
|
+
}
|
|
3935
|
+
function getRequiredCapabilities(methodName) {
|
|
3936
|
+
const info = METHOD_REGISTRY[methodName];
|
|
3937
|
+
if (info) return info.capabilities;
|
|
3938
|
+
const byWire = Object.values(METHOD_REGISTRY).find((m) => m.method === methodName);
|
|
3939
|
+
return byWire?.capabilities ?? [];
|
|
3940
|
+
}
|
|
3941
|
+
function hasRequiredCapabilities(methodName, capabilities) {
|
|
3942
|
+
const required = getRequiredCapabilities(methodName);
|
|
3943
|
+
if (required.length === 0) return true;
|
|
3944
|
+
for (const path of required) {
|
|
3945
|
+
const [category, capability] = path.split(".");
|
|
3946
|
+
const categoryCapabilities = capabilities[category];
|
|
3947
|
+
if (!categoryCapabilities?.[capability]) {
|
|
3948
|
+
return false;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return true;
|
|
3952
|
+
}
|
|
3953
|
+
function getMethodInfo(wireMethod) {
|
|
3954
|
+
return Object.values(METHOD_REGISTRY).find((m) => m.method === wireMethod);
|
|
3955
|
+
}
|
|
3956
|
+
function buildConnectResponse(params) {
|
|
3957
|
+
return {
|
|
3958
|
+
protocolVersion: params.protocolVersion,
|
|
3959
|
+
sessionId: params.sessionId,
|
|
3960
|
+
participantId: params.participantId,
|
|
3961
|
+
capabilities: params.capabilities,
|
|
3962
|
+
systemInfo: params.systemInfo,
|
|
3963
|
+
reconnected: params.reconnected,
|
|
3964
|
+
reclaimedAgents: params.reclaimedAgents,
|
|
3965
|
+
ownedAgents: params.ownedAgents
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
function buildDisconnectResponse(session) {
|
|
3969
|
+
return { session };
|
|
3970
|
+
}
|
|
3971
|
+
function buildSendResponse(messageId, delivered) {
|
|
3972
|
+
return { messageId, delivered };
|
|
3973
|
+
}
|
|
3974
|
+
function buildAgentsRegisterResponse(agent) {
|
|
3975
|
+
return { agent };
|
|
3976
|
+
}
|
|
3977
|
+
function buildAgentsUnregisterResponse(agent) {
|
|
3978
|
+
return { agent };
|
|
3979
|
+
}
|
|
3980
|
+
function buildAgentsListResponse(agents) {
|
|
3981
|
+
return { agents };
|
|
3982
|
+
}
|
|
3983
|
+
function buildAgentsGetResponse(agent, children, descendants) {
|
|
3984
|
+
const result = { agent };
|
|
3985
|
+
if (children) result.children = children;
|
|
3986
|
+
if (descendants) result.descendants = descendants;
|
|
3987
|
+
return result;
|
|
3988
|
+
}
|
|
3989
|
+
function buildAgentsUpdateResponse(agent) {
|
|
3990
|
+
return { agent };
|
|
3991
|
+
}
|
|
3992
|
+
function buildAgentsSpawnResponse(agent) {
|
|
3993
|
+
return { agent };
|
|
3994
|
+
}
|
|
3995
|
+
function buildScopesCreateResponse(scope) {
|
|
3996
|
+
return { scope };
|
|
3997
|
+
}
|
|
3998
|
+
function buildScopesListResponse(scopes) {
|
|
3999
|
+
return { scopes };
|
|
4000
|
+
}
|
|
4001
|
+
function buildScopesJoinResponse(scope, agent) {
|
|
4002
|
+
return { scope, agent };
|
|
4003
|
+
}
|
|
4004
|
+
function buildScopesLeaveResponse(scope, agent) {
|
|
4005
|
+
return { scope, agent };
|
|
4006
|
+
}
|
|
4007
|
+
function buildSubscribeResponse(subscriptionId) {
|
|
4008
|
+
return { subscriptionId };
|
|
4009
|
+
}
|
|
4010
|
+
function buildUnsubscribeResponse(subscriptionId, closedAt = Date.now()) {
|
|
4011
|
+
return {
|
|
4012
|
+
subscription: {
|
|
4013
|
+
id: subscriptionId,
|
|
4014
|
+
closedAt
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
// src/permissions/index.ts
|
|
4020
|
+
function isAgentExposed(exposure, agentId) {
|
|
4021
|
+
if (!exposure?.agents) return true;
|
|
4022
|
+
const {
|
|
4023
|
+
publicByDefault = true,
|
|
4024
|
+
publicAgents = [],
|
|
4025
|
+
hiddenAgents = []
|
|
4026
|
+
} = exposure.agents;
|
|
4027
|
+
if (matchesPatterns(agentId, hiddenAgents)) return false;
|
|
4028
|
+
if (matchesPatterns(agentId, publicAgents)) return true;
|
|
4029
|
+
return publicByDefault;
|
|
4030
|
+
}
|
|
4031
|
+
function isEventTypeExposed(exposure, eventType) {
|
|
4032
|
+
if (!exposure?.events) return true;
|
|
4033
|
+
const { exposedTypes, hiddenTypes = [] } = exposure.events;
|
|
4034
|
+
if (hiddenTypes.includes(eventType)) return false;
|
|
4035
|
+
if (exposedTypes && !exposedTypes.includes(eventType)) return false;
|
|
4036
|
+
return true;
|
|
4037
|
+
}
|
|
4038
|
+
function isScopeExposed(exposure, scopeId) {
|
|
4039
|
+
if (!exposure?.scopes) return true;
|
|
4040
|
+
const {
|
|
4041
|
+
publicByDefault = true,
|
|
4042
|
+
publicScopes = [],
|
|
4043
|
+
hiddenScopes = []
|
|
4044
|
+
} = exposure.scopes;
|
|
4045
|
+
if (matchesPatterns(scopeId, hiddenScopes)) return false;
|
|
4046
|
+
if (matchesPatterns(scopeId, publicScopes)) return true;
|
|
4047
|
+
return publicByDefault;
|
|
4048
|
+
}
|
|
4049
|
+
function hasCapability(capabilities, path) {
|
|
4050
|
+
const [category, cap] = path.split(".");
|
|
4051
|
+
const categoryCapabilities = capabilities[category];
|
|
4052
|
+
return categoryCapabilities?.[cap] ?? false;
|
|
4053
|
+
}
|
|
4054
|
+
function canPerformMethod(method, capabilities) {
|
|
4055
|
+
return hasRequiredCapabilities(method, capabilities);
|
|
4056
|
+
}
|
|
4057
|
+
function canSeeScope(scope, participant, memberAgentIds = []) {
|
|
4058
|
+
const visibility = scope.visibility ?? "public";
|
|
4059
|
+
switch (visibility) {
|
|
4060
|
+
case "public":
|
|
4061
|
+
return true;
|
|
4062
|
+
case "members":
|
|
4063
|
+
return memberAgentIds.length > 0;
|
|
4064
|
+
case "system":
|
|
4065
|
+
return participant.type === "system";
|
|
4066
|
+
default:
|
|
4067
|
+
return false;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
function canSendToScope(scope, participant, memberAgentIds = []) {
|
|
4071
|
+
if (participant.type === "system") return true;
|
|
4072
|
+
const sendPolicy = scope.sendPolicy ?? "members";
|
|
4073
|
+
switch (sendPolicy) {
|
|
4074
|
+
case "any":
|
|
4075
|
+
return true;
|
|
4076
|
+
case "members":
|
|
4077
|
+
return memberAgentIds.length > 0;
|
|
4078
|
+
default:
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
function canJoinScope(scope, participantType, agentRole) {
|
|
4083
|
+
const joinPolicy = scope.joinPolicy ?? "open";
|
|
4084
|
+
switch (joinPolicy) {
|
|
4085
|
+
case "open":
|
|
4086
|
+
return true;
|
|
4087
|
+
case "invite":
|
|
4088
|
+
return false;
|
|
4089
|
+
case "role":
|
|
4090
|
+
if (!agentRole || !scope.autoJoinRoles) return false;
|
|
4091
|
+
return scope.autoJoinRoles.includes(agentRole);
|
|
4092
|
+
case "system":
|
|
4093
|
+
return participantType === "system";
|
|
4094
|
+
default:
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
function canSeeAgent(agent, participant, ownedAgentIds = []) {
|
|
4099
|
+
const visibility = agent.visibility ?? "public";
|
|
4100
|
+
switch (visibility) {
|
|
4101
|
+
case "public":
|
|
4102
|
+
return true;
|
|
4103
|
+
case "parent-only":
|
|
4104
|
+
if (ownedAgentIds.includes(agent.id)) return true;
|
|
4105
|
+
return agent.parent ? ownedAgentIds.includes(agent.parent) : false;
|
|
4106
|
+
case "scope":
|
|
4107
|
+
return true;
|
|
4108
|
+
case "system":
|
|
4109
|
+
return participant.type === "system";
|
|
4110
|
+
default:
|
|
4111
|
+
return false;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
function canMessageAgent(agent, participant, ownedAgentIds = []) {
|
|
4115
|
+
if (!canSeeAgent(agent, participant, ownedAgentIds)) {
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
return true;
|
|
4119
|
+
}
|
|
4120
|
+
function canControlAgent(agent, participant, ownedAgentIds = []) {
|
|
4121
|
+
if (participant.type === "system") return true;
|
|
4122
|
+
if (ownedAgentIds.includes(agent.id)) return true;
|
|
4123
|
+
if (agent.parent && ownedAgentIds.includes(agent.parent)) return true;
|
|
4124
|
+
return false;
|
|
4125
|
+
}
|
|
4126
|
+
function canPerformAction(context, action) {
|
|
4127
|
+
if (action.target?.agentId) {
|
|
4128
|
+
if (!isAgentExposed(context.system.exposure, action.target.agentId)) {
|
|
4129
|
+
return {
|
|
4130
|
+
allowed: false,
|
|
4131
|
+
reason: "Agent not exposed by system configuration",
|
|
4132
|
+
layer: 1
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
if (action.target?.scopeId) {
|
|
4137
|
+
if (!isScopeExposed(context.system.exposure, action.target.scopeId)) {
|
|
4138
|
+
return {
|
|
4139
|
+
allowed: false,
|
|
4140
|
+
reason: "Scope not exposed by system configuration",
|
|
4141
|
+
layer: 1
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (action.target?.eventTypes) {
|
|
4146
|
+
for (const eventType of action.target.eventTypes) {
|
|
4147
|
+
if (!isEventTypeExposed(context.system.exposure, eventType)) {
|
|
4148
|
+
return {
|
|
4149
|
+
allowed: false,
|
|
4150
|
+
reason: `Event type '${eventType}' not exposed by system configuration`,
|
|
4151
|
+
layer: 1
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
const requiredCaps = getRequiredCapabilities(action.method);
|
|
4157
|
+
for (const cap of requiredCaps) {
|
|
4158
|
+
if (!hasCapability(context.participant.capabilities, cap)) {
|
|
4159
|
+
return {
|
|
4160
|
+
allowed: false,
|
|
4161
|
+
reason: `Missing required capability: ${cap}`,
|
|
4162
|
+
layer: 2
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
return { allowed: true };
|
|
4167
|
+
}
|
|
4168
|
+
function filterVisibleAgents(agents, context) {
|
|
4169
|
+
const ownedAgentIds = context.ownedAgentIds ?? [];
|
|
4170
|
+
return agents.filter((agent) => {
|
|
4171
|
+
if (!isAgentExposed(context.system.exposure, agent.id)) {
|
|
4172
|
+
return false;
|
|
4173
|
+
}
|
|
4174
|
+
if (!canSeeAgent(agent, context.participant, ownedAgentIds)) {
|
|
4175
|
+
return false;
|
|
4176
|
+
}
|
|
4177
|
+
return true;
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
function filterVisibleScopes(scopes, context) {
|
|
4181
|
+
const scopeMembership = context.scopeMembership ?? /* @__PURE__ */ new Map();
|
|
4182
|
+
return scopes.filter((scope) => {
|
|
4183
|
+
if (!isScopeExposed(context.system.exposure, scope.id)) {
|
|
4184
|
+
return false;
|
|
4185
|
+
}
|
|
4186
|
+
const memberAgentIds = scopeMembership.get(scope.id) ?? [];
|
|
4187
|
+
if (!canSeeScope(scope, context.participant, memberAgentIds)) {
|
|
4188
|
+
return false;
|
|
4189
|
+
}
|
|
4190
|
+
return true;
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
function filterVisibleEvents(events, context) {
|
|
4194
|
+
return events.filter((event) => {
|
|
4195
|
+
if (!isEventTypeExposed(context.system.exposure, event.type)) {
|
|
4196
|
+
return false;
|
|
4197
|
+
}
|
|
4198
|
+
return true;
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
function matchesPatterns(value, patterns) {
|
|
4202
|
+
return patterns.some((pattern) => matchGlob(value, pattern));
|
|
4203
|
+
}
|
|
4204
|
+
function matchGlob(value, pattern) {
|
|
4205
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
4206
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
4207
|
+
return regex.test(value);
|
|
4208
|
+
}
|
|
4209
|
+
function deepClone(obj) {
|
|
4210
|
+
return JSON.parse(JSON.stringify(obj));
|
|
4211
|
+
}
|
|
4212
|
+
function deepMergePermissions(base, override) {
|
|
4213
|
+
const result = { ...base };
|
|
4214
|
+
if (override.canSee) {
|
|
4215
|
+
result.canSee = { ...base.canSee, ...override.canSee };
|
|
4216
|
+
}
|
|
4217
|
+
if (override.canMessage) {
|
|
4218
|
+
result.canMessage = { ...base.canMessage, ...override.canMessage };
|
|
4219
|
+
}
|
|
4220
|
+
if (override.acceptsFrom) {
|
|
4221
|
+
result.acceptsFrom = { ...base.acceptsFrom, ...override.acceptsFrom };
|
|
4222
|
+
}
|
|
4223
|
+
return result;
|
|
4224
|
+
}
|
|
4225
|
+
function mapVisibilityToRule(visibility) {
|
|
4226
|
+
switch (visibility) {
|
|
4227
|
+
case "public":
|
|
4228
|
+
return "all";
|
|
4229
|
+
case "parent-only":
|
|
4230
|
+
return "hierarchy";
|
|
4231
|
+
case "scope":
|
|
4232
|
+
return "scoped";
|
|
4233
|
+
case "system":
|
|
4234
|
+
return "direct";
|
|
4235
|
+
default:
|
|
4236
|
+
return "all";
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
var DEFAULT_AGENT_PERMISSION_CONFIG = {
|
|
4240
|
+
defaultPermissions: {
|
|
4241
|
+
canSee: {
|
|
4242
|
+
agents: "all",
|
|
4243
|
+
scopes: "all",
|
|
4244
|
+
structure: "full"
|
|
4245
|
+
},
|
|
4246
|
+
canMessage: {
|
|
4247
|
+
agents: "all",
|
|
4248
|
+
scopes: "all"
|
|
4249
|
+
},
|
|
4250
|
+
acceptsFrom: {
|
|
4251
|
+
agents: "all",
|
|
4252
|
+
clients: "all",
|
|
4253
|
+
systems: "all"
|
|
4254
|
+
}
|
|
4255
|
+
},
|
|
4256
|
+
rolePermissions: {}
|
|
4257
|
+
};
|
|
4258
|
+
function resolveAgentPermissions(agent, config = DEFAULT_AGENT_PERMISSION_CONFIG) {
|
|
4259
|
+
let permissions = deepClone(config.defaultPermissions);
|
|
4260
|
+
if (agent.role && config.rolePermissions[agent.role]) {
|
|
4261
|
+
permissions = deepMergePermissions(permissions, config.rolePermissions[agent.role]);
|
|
4262
|
+
}
|
|
4263
|
+
if (agent.permissionOverrides) {
|
|
4264
|
+
permissions = deepMergePermissions(permissions, agent.permissionOverrides);
|
|
4265
|
+
}
|
|
4266
|
+
if (agent.visibility && !agent.permissionOverrides?.canSee?.agents) {
|
|
4267
|
+
permissions.canSee = permissions.canSee ?? {};
|
|
4268
|
+
permissions.canSee.agents = mapVisibilityToRule(agent.visibility);
|
|
4269
|
+
}
|
|
4270
|
+
return permissions;
|
|
4271
|
+
}
|
|
4272
|
+
function checkAgentAcceptance(rule, context) {
|
|
4273
|
+
if (!rule || rule === "all") return true;
|
|
4274
|
+
if (rule === "hierarchy") {
|
|
4275
|
+
return context.isParent === true || context.isChild === true || context.isAncestor === true || context.isDescendant === true;
|
|
4276
|
+
}
|
|
4277
|
+
if (rule === "scoped") {
|
|
4278
|
+
return (context.sharedScopes?.length ?? 0) > 0;
|
|
4279
|
+
}
|
|
4280
|
+
if (typeof rule === "object" && "include" in rule) {
|
|
4281
|
+
return context.senderAgentId !== void 0 && rule.include.includes(context.senderAgentId);
|
|
4282
|
+
}
|
|
4283
|
+
return false;
|
|
4284
|
+
}
|
|
4285
|
+
function checkClientAcceptance(rule, senderId) {
|
|
4286
|
+
if (!rule || rule === "all") return true;
|
|
4287
|
+
if (rule === "none") return false;
|
|
4288
|
+
if (typeof rule === "object" && "include" in rule) {
|
|
4289
|
+
return rule.include.includes(senderId);
|
|
4290
|
+
}
|
|
4291
|
+
return false;
|
|
4292
|
+
}
|
|
4293
|
+
function checkSystemAcceptance(rule, senderSystemId) {
|
|
4294
|
+
if (!rule || rule === "all") return true;
|
|
4295
|
+
if (rule === "none") return false;
|
|
4296
|
+
if (typeof rule === "object" && "include" in rule) {
|
|
4297
|
+
return senderSystemId !== void 0 && rule.include.includes(senderSystemId);
|
|
4298
|
+
}
|
|
4299
|
+
return false;
|
|
4300
|
+
}
|
|
4301
|
+
function canAgentAcceptMessage(targetAgent, context, config = DEFAULT_AGENT_PERMISSION_CONFIG) {
|
|
4302
|
+
const permissions = resolveAgentPermissions(targetAgent, config);
|
|
4303
|
+
const acceptsFrom = permissions.acceptsFrom;
|
|
4304
|
+
if (!acceptsFrom) return true;
|
|
4305
|
+
switch (context.senderType) {
|
|
4306
|
+
case "agent":
|
|
4307
|
+
return checkAgentAcceptance(acceptsFrom.agents, context);
|
|
4308
|
+
case "client":
|
|
4309
|
+
return checkClientAcceptance(acceptsFrom.clients, context.senderId);
|
|
4310
|
+
case "system":
|
|
4311
|
+
case "gateway":
|
|
4312
|
+
return checkSystemAcceptance(acceptsFrom.systems, context.senderSystemId);
|
|
4313
|
+
default:
|
|
4314
|
+
return false;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
function canAgentSeeAgent(viewerAgent, targetAgentId, context, config = DEFAULT_AGENT_PERMISSION_CONFIG) {
|
|
4318
|
+
const permissions = resolveAgentPermissions(viewerAgent, config);
|
|
4319
|
+
const canSee = permissions.canSee?.agents;
|
|
4320
|
+
if (!canSee || canSee === "all") return true;
|
|
4321
|
+
if (canSee === "hierarchy") {
|
|
4322
|
+
return context.isParent === true || context.isChild === true || context.isAncestor === true || context.isDescendant === true;
|
|
4323
|
+
}
|
|
4324
|
+
if (canSee === "scoped") {
|
|
4325
|
+
return (context.sharedScopes?.length ?? 0) > 0;
|
|
4326
|
+
}
|
|
4327
|
+
if (canSee === "direct") {
|
|
4328
|
+
return false;
|
|
4329
|
+
}
|
|
4330
|
+
if (typeof canSee === "object" && "include" in canSee) {
|
|
4331
|
+
return canSee.include.includes(targetAgentId);
|
|
4332
|
+
}
|
|
4333
|
+
return false;
|
|
4334
|
+
}
|
|
4335
|
+
function canAgentMessageAgent(senderAgent, targetAgentId, context, config = DEFAULT_AGENT_PERMISSION_CONFIG) {
|
|
4336
|
+
const permissions = resolveAgentPermissions(senderAgent, config);
|
|
4337
|
+
const canMessage = permissions.canMessage?.agents;
|
|
4338
|
+
if (!canMessage || canMessage === "all") return true;
|
|
4339
|
+
if (canMessage === "hierarchy") {
|
|
4340
|
+
return context.isParent === true || context.isChild === true || context.isAncestor === true || context.isDescendant === true;
|
|
4341
|
+
}
|
|
4342
|
+
if (canMessage === "scoped") {
|
|
4343
|
+
return (context.sharedScopes?.length ?? 0) > 0;
|
|
4344
|
+
}
|
|
4345
|
+
if (typeof canMessage === "object" && "include" in canMessage) {
|
|
4346
|
+
return canMessage.include.includes(targetAgentId);
|
|
4347
|
+
}
|
|
4348
|
+
return false;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
export { AGENT_ERROR_CODES, AUTH_ERROR_CODES, AUTH_METHODS, AddressSchema, AgentConnection, AgentIdSchema, AgentLifecycleSchema, AgentRelationshipSchema, AgentSchema, AgentStateSchema, AgentVisibilitySchema, BaseConnection, BroadcastAddressSchema, CAPABILITY_REQUIREMENTS, CORE_METHODS, CausalEventBuffer, ClientConnection, CorrelationIdSchema, DEFAULT_AGENT_PERMISSION_CONFIG, DEFAULT_RETRY_POLICY, DeliverySemanticsSchema, DirectAddressSchema, ERROR_CODES, EVENT_TYPES, EXTENSION_METHODS, ErrorCategorySchema, EventSchema, EventTypeSchema, FEDERATION_ERROR_CODES, FEDERATION_METHODS, FederatedAddressSchema, FederationOutageBuffer, GatewayConnection, HierarchicalAddressSchema, JSONRPC_VERSION, JsonRpcVersionSchema, LIFECYCLE_METHODS, MAPConnectionError, MAPErrorDataSchema, MAPErrorSchema, MAPNotificationSchema, MAPRequestError, MAPRequestSchema, MAPResponseErrorSchema, MAPResponseSchema, MAPResponseSuccessSchema, MAPTimeoutError, MAP_METHODS, METHOD_REGISTRY, MessageIdSchema, MessageMetaSchema, MessagePrioritySchema, MessageRelationshipSchema, MessageSchema, MessageVisibilitySchema, MetaSchema, MultiAddressSchema, NOTIFICATION_METHODS, OBSERVATION_METHODS, PERMISSION_METHODS, PROTOCOL_ERROR_CODES, PROTOCOL_VERSION, ParticipantAddressSchema, ParticipantCapabilitiesSchema, ParticipantIdSchema, ParticipantTypeSchema, ProtocolVersionSchema, RESOURCE_ERROR_CODES, ROUTING_ERROR_CODES, RequestIdSchema, RoleAddressSchema, SCOPE_METHODS, SESSION_METHODS, STATE_METHODS, STEERING_METHODS, STRUCTURE_METHODS, ScopeAddressSchema, ScopeIdSchema, ScopeJoinPolicySchema, ScopeSchema, ScopeSendPolicySchema, ScopeVisibilitySchema, SessionIdSchema, Subscription, SubscriptionFilterSchema, SubscriptionIdSchema, SystemAddressSchema, TimestampSchema, TransportTypeSchema, buildAgentsGetResponse, buildAgentsListResponse, buildAgentsRegisterResponse, buildAgentsSpawnResponse, buildAgentsUnregisterResponse, buildAgentsUpdateResponse, buildConnectResponse, buildDisconnectResponse, buildScopesCreateResponse, buildScopesJoinResponse, buildScopesLeaveResponse, buildScopesListResponse, buildSendResponse, buildSubscribeResponse, buildUnsubscribeResponse, calculateDelay, canAgentAcceptMessage, canAgentMessageAgent, canAgentSeeAgent, canControlAgent, canJoinScope, canMessageAgent, canPerformAction, canPerformMethod, canSeeAgent, canSeeScope, canSendToScope, compareUlid, createErrorResponse, createEvent, createFederationEnvelope, createNotification, createRequest, createRetryPolicy, createStreamPair, createSubscription, createSuccessResponse, deepMergePermissions, filterVisibleAgents, filterVisibleEvents, filterVisibleScopes, getEnvelopeRoutingInfo, getMethodInfo, getMethodsByCategory, getRequiredCapabilities, hasCapability, hasRequiredCapabilities, isAgentExposed, isBroadcastAddress, isDirectAddress, isEnvelopeAtDestination, isErrorResponse, isEventTypeExposed, isFederatedAddress, isHierarchicalAddress, isNotification, isOrphanedAgent, isRequest, isResponse, isScopeAddress, isScopeExposed, isSuccessResponse, isValidEnvelope, isValidUlid, mapVisibilityToRule, ndJsonStream, processFederationEnvelope, resolveAgentPermissions, retryable, sleep, sortCausalOrder, ulidTimestamp, unwrapEnvelope, validateCausalOrder, websocketStream, withPayload, withRetry };
|
|
4352
|
+
//# sourceMappingURL=index.js.map
|
|
4353
|
+
//# sourceMappingURL=index.js.map
|