@fatagnus/dink-sdk 2.23.1 → 2.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,578 @@
1
- export { CenterClient, type CenterChannel } from './client.js';
2
- //# sourceMappingURL=index.d.ts.map
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ /**
4
+ * Service definition metadata
5
+ */
6
+ export interface ServiceDefinition {
7
+ name: string;
8
+ version: string;
9
+ methods: string[];
10
+ }
11
+ /**
12
+ * Interface for implementing a service handler on the edge
13
+ */
14
+ /** Bidirectional raw byte channel on the edge side. */
15
+ export interface EdgeChannel {
16
+ /** Unique channel identifier (e.g., "ch_abc123") */
17
+ readonly id: string;
18
+ /** Send raw bytes to the browser. Returns false if backpressure is active. */
19
+ write(data: Uint8Array): boolean;
20
+ /** Register handler for incoming raw bytes from browser */
21
+ onData(handler: (data: Uint8Array) => void): void;
22
+ /** Close the channel, optionally with a reason */
23
+ close(reason?: string): void;
24
+ /** Called when the channel closes (by either side). Supports multiple handlers. */
25
+ onClose(handler: (reason?: string) => void): void;
26
+ /** Send a JSON-serializable control message out-of-band */
27
+ sendControl(msg: unknown): void;
28
+ /** Register handler for incoming control messages */
29
+ onControl(handler: (msg: unknown) => void): void;
30
+ /** True if channel is open */
31
+ readonly isOpen: boolean;
32
+ /** Promise that resolves when the channel closes */
33
+ readonly closed: Promise<void>;
34
+ /** Number of bytes queued for sending */
35
+ readonly bufferedAmount: number;
36
+ /** Register handler called when buffered data has been flushed */
37
+ onDrain(handler: () => void): void;
38
+ }
39
+ /**
40
+ */
41
+ export interface ServiceHandler {
42
+ definition(): ServiceDefinition;
43
+ handleRequest(method: string, data: Uint8Array): Promise<Uint8Array>;
44
+ handleStream?(method: string, data: Uint8Array, emit: (data: Uint8Array) => Promise<void>, signal?: AbortSignal): Promise<void>;
45
+ handleChannel?(method: string, channel: EdgeChannel, request: unknown): Promise<void>;
46
+ }
47
+ /**
48
+ * Interface for making RPC calls to edge services
49
+ */
50
+ export interface ServiceCaller {
51
+ call<Req, Resp>(edgeId: string, service: string, method: string, req: Req): Promise<Resp>;
52
+ subscribe<Req, Resp>(edgeId: string, service: string, method: string, req: Req, handler: (resp: Resp) => void): Promise<Subscription>;
53
+ }
54
+ /**
55
+ * Interface for streaming subscriptions
56
+ */
57
+ export interface Subscription {
58
+ unsubscribe(): void;
59
+ }
60
+ /**
61
+ * Connection quality level
62
+ */
63
+ export type ConnectionQualityLevel = "excellent" | "good" | "fair" | "poor" | "unknown";
64
+ /**
65
+ * Connection state
66
+ */
67
+ export type ConnectionState = "connecting" | "connected" | "reconnecting" | "disconnected";
68
+ /**
69
+ * Connection quality metrics
70
+ */
71
+ export interface ConnectionQuality {
72
+ state: ConnectionState;
73
+ latencyMs: number | null;
74
+ avgLatencyMs: number | null;
75
+ messagesSentPerSecond: number;
76
+ messagesReceivedPerSecond: number;
77
+ totalMessagesSent: number;
78
+ totalMessagesReceived: number;
79
+ lastPingAt: number | null;
80
+ qualityLevel: ConnectionQualityLevel;
81
+ }
82
+ /**
83
+ * Configuration for connection quality monitoring
84
+ */
85
+ export interface ConnectionQualityConfig {
86
+ /** Latency threshold for "excellent" quality in ms (default: 50) */
87
+ excellentLatencyMs?: number;
88
+ /** Latency threshold for "good" quality in ms (default: 150) */
89
+ goodLatencyMs?: number;
90
+ /** Latency threshold for "fair" quality in ms (default: 300) */
91
+ fairLatencyMs?: number;
92
+ /** Ping interval in milliseconds (default: 30000) */
93
+ pingIntervalMs?: number;
94
+ }
95
+ /**
96
+ * Configuration for CenterClient
97
+ */
98
+ export interface CenterConfig {
99
+ /** dinkd server URL (e.g., "nats://localhost:4222") */
100
+ serverUrl: string;
101
+ /** App API key for authentication */
102
+ apiKey?: string;
103
+ /** App ID (optional, defaults to 'platform') */
104
+ appId?: string;
105
+ /** Request timeout in ms (default: 30000) */
106
+ timeout?: number;
107
+ /** Reconnect wait time in ms (default: 2000) */
108
+ reconnectWait?: number;
109
+ /** Max reconnect attempts, -1 for infinite (default: -1) */
110
+ maxReconnects?: number;
111
+ /** Connection quality configuration */
112
+ qualityConfig?: ConnectionQualityConfig;
113
+ }
114
+ /**
115
+ * Information about a discovered edge
116
+ */
117
+ export interface EdgeInfo {
118
+ /** Unique edge identifier */
119
+ id: string;
120
+ /** Display name of the edge */
121
+ name: string;
122
+ /** Online status */
123
+ status: "online" | "offline";
124
+ /** Labels attached to the edge */
125
+ labels: Record<string, string>;
126
+ /** Services exposed by this edge */
127
+ services: string[];
128
+ /** Groups this edge belongs to */
129
+ groups?: string[];
130
+ }
131
+ /**
132
+ * Options for discovering edges
133
+ */
134
+ export interface DiscoverOptions {
135
+ /** Filter by service name */
136
+ serviceName?: string;
137
+ /** Filter by labels */
138
+ labels?: Record<string, string>;
139
+ /** Only return online edges (default: true) */
140
+ onlineOnly?: boolean;
141
+ /** Filter by method name (requires serviceName) */
142
+ methodName?: string;
143
+ }
144
+ /**
145
+ * Options for RPC calls
146
+ */
147
+ export interface CallOptions {
148
+ /** Request timeout in ms */
149
+ timeout?: number;
150
+ /** Number of retries on failure (default: 0) */
151
+ retries?: number;
152
+ /** Delay between retries in ms (default: 1000) */
153
+ retryDelay?: number;
154
+ }
155
+ /**
156
+ * Type of API key
157
+ */
158
+ export type KeyType = "edge" | "center" | "client" | "admin" | "app_master" | "sync_key";
159
+ /**
160
+ * Status of an API key
161
+ */
162
+ export type KeyStatus = "active" | "expired" | "revoked" | "disabled";
163
+ /**
164
+ * Metadata about an API key
165
+ */
166
+ export interface KeyMetadata {
167
+ /** Unique key identifier */
168
+ id: string;
169
+ /** App ID this key belongs to */
170
+ appId: string;
171
+ /** Display name of the key */
172
+ name: string;
173
+ /** Type of the key */
174
+ type: KeyType;
175
+ /** Edge ID (for edge keys) */
176
+ edgeId?: string;
177
+ /** Scopes granted to this key */
178
+ scopes: string[];
179
+ /** Resource restrictions */
180
+ resources?: {
181
+ services?: string[];
182
+ streams?: string[];
183
+ kvBuckets?: string[];
184
+ events?: string[];
185
+ };
186
+ /** Labels attached to the key */
187
+ labels?: Record<string, string>;
188
+ /** Current status of the key */
189
+ status: KeyStatus;
190
+ /** When the key was created */
191
+ createdAt: Date;
192
+ /** When the key expires (if set) */
193
+ expiresAt?: Date;
194
+ /** When the key was last used */
195
+ lastUsedAt?: Date;
196
+ /** When the key was revoked (if revoked) */
197
+ revokedAt?: Date;
198
+ /** Reason for revocation (if revoked) */
199
+ revokeReason?: string;
200
+ /** Groups for peer communication */
201
+ groups?: string[];
202
+ }
203
+ /**
204
+ * Options for creating an API key
205
+ */
206
+ export interface CreateKeyOptions {
207
+ /** Display name for the key */
208
+ name: string;
209
+ /** Type of key to create */
210
+ type: KeyType;
211
+ /** Edge ID (optional for edge keys, auto-generated if not provided) */
212
+ edgeId?: string;
213
+ /** Scopes to grant (optional, defaults based on type) */
214
+ scopes?: string[];
215
+ /** Scope bundle to use (e.g., 'bundle:edge') */
216
+ scopeBundle?: string;
217
+ /** Expiration time (e.g., '24h', '7d', '30d') */
218
+ expiresIn?: string;
219
+ /** Labels to attach to the key */
220
+ labels?: Record<string, string>;
221
+ /** Groups for peer-to-peer communication */
222
+ groups?: string[];
223
+ }
224
+ /**
225
+ * Result from creating an API key
226
+ */
227
+ export interface CreateKeyResult {
228
+ /** Key metadata */
229
+ key: KeyMetadata;
230
+ /** The actual API key (only shown once!) */
231
+ apiKey: string;
232
+ }
233
+ /**
234
+ * Options for listing keys
235
+ */
236
+ export interface ListKeysOptions {
237
+ /** Filter by key type */
238
+ type?: KeyType;
239
+ /** Filter by labels */
240
+ labels?: Record<string, string>;
241
+ }
242
+ /**
243
+ * Service introspection types for AI agent support.
244
+ * These types match the Go struct definitions in pkg/types/introspect.go
245
+ * Property names use snake_case to match Go JSON tags for wire compatibility.
246
+ */
247
+ /**
248
+ * MethodAIContext provides AI-specific guidance for understanding and using a method.
249
+ */
250
+ export interface MethodAIContext {
251
+ /** When this method should be called */
252
+ when_to_use?: string;
253
+ /** Why to use this method over alternatives */
254
+ why_to_use?: string;
255
+ /** Instructions on how to use this method correctly */
256
+ how_to_use?: string;
257
+ /** Conditions that must be true before calling this method */
258
+ preconditions?: string[];
259
+ /** Side effects that occur when calling this method */
260
+ side_effects?: string[];
261
+ /** Names of related methods that may be useful */
262
+ related_methods?: string[];
263
+ /** Scenarios describing when to use this method */
264
+ scenarios?: string[];
265
+ }
266
+ /**
267
+ * ServiceAIContext provides AI-specific guidance for understanding a service.
268
+ */
269
+ export interface ServiceAIContext {
270
+ /** High-level description of what this service does */
271
+ overview?: string;
272
+ /** List of capabilities this service provides */
273
+ capabilities?: string[];
274
+ /** Known limitations of this service */
275
+ limitations?: string[];
276
+ }
277
+ /**
278
+ * MethodDescriptor provides complete metadata about a service method for introspection
279
+ * and AI agent consumption.
280
+ */
281
+ export interface MethodDescriptor {
282
+ /** Method name */
283
+ name: string;
284
+ /** Human-readable description of what this method does */
285
+ description?: string;
286
+ /** JSON Schema describing the input parameters */
287
+ input_schema?: Record<string, unknown>;
288
+ /** JSON Schema describing the output */
289
+ output_schema?: Record<string, unknown>;
290
+ /** Tags for categorization and filtering */
291
+ tags?: string[];
292
+ /** AI-specific context for this method */
293
+ ai_context?: MethodAIContext;
294
+ }
295
+ /**
296
+ * ServiceDescriptor provides complete metadata about a service for introspection
297
+ * and AI agent consumption.
298
+ */
299
+ export interface ServiceDescriptor {
300
+ /** Service name */
301
+ name: string;
302
+ /** Service version (semver recommended) */
303
+ version: string;
304
+ /** Human-readable description of this service */
305
+ description?: string;
306
+ /** Methods provided by this service */
307
+ methods?: MethodDescriptor[];
308
+ /** AI-specific context for this service */
309
+ ai_context?: ServiceAIContext;
310
+ /** Names of related services */
311
+ related_services?: string[];
312
+ }
313
+ /** Bidirectional raw byte channel (center-to-edge). */
314
+ export interface CenterChannel {
315
+ /** Unique channel identifier */
316
+ readonly id: string;
317
+ /** Send raw bytes to the edge. Returns false if backpressure is active. */
318
+ write(data: Uint8Array): boolean;
319
+ /** Register handler for incoming raw bytes from edge */
320
+ onData(handler: (data: Uint8Array) => void): void;
321
+ /** Close the channel */
322
+ close(): void;
323
+ /** Called when the channel closes. Supports multiple handlers. */
324
+ onClose(handler: (reason?: string) => void): void;
325
+ /** Send a JSON-serializable control message out-of-band */
326
+ sendControl(msg: unknown): void;
327
+ /** Register handler for incoming control messages */
328
+ onControl(handler: (msg: unknown) => void): void;
329
+ /** True if channel is open */
330
+ readonly isOpen: boolean;
331
+ /** Promise that resolves when the channel closes */
332
+ readonly closed: Promise<void>;
333
+ /** Number of bytes queued for sending */
334
+ readonly bufferedAmount: number;
335
+ /** Register handler called when buffered data has been flushed */
336
+ onDrain(handler: () => void): void;
337
+ }
338
+ export declare class CenterClient implements ServiceCaller {
339
+ private config;
340
+ private nc;
341
+ private appId;
342
+ private connectionState;
343
+ private lastLatencyMs;
344
+ private lastPingAt;
345
+ private latencyHistory;
346
+ private messagesSent;
347
+ private messagesReceived;
348
+ private statsStartedAt;
349
+ private qualityCallbacks;
350
+ private qualityConfig;
351
+ private pingTimer;
352
+ constructor(config: CenterConfig);
353
+ connect(): Promise<void>;
354
+ close(): Promise<void>;
355
+ isConnected(): boolean;
356
+ /**
357
+ * Get current connection quality metrics.
358
+ */
359
+ getConnectionQuality(): ConnectionQuality;
360
+ /**
361
+ * Subscribe to connection quality changes.
362
+ * @param callback - Function called with updated quality metrics
363
+ * @returns Unsubscribe function
364
+ */
365
+ onConnectionQualityChange(callback: (quality: ConnectionQuality) => void): () => void;
366
+ /**
367
+ * Ping the server and return round-trip latency in milliseconds.
368
+ */
369
+ ping(): Promise<number>;
370
+ private setConnectionState;
371
+ private calculateQualityLevel;
372
+ private calculateAvgLatency;
373
+ private buildQualitySnapshot;
374
+ private emitQualityChange;
375
+ private setupConnectionHandlers;
376
+ private startPingTimer;
377
+ private stopPingTimer;
378
+ discoverEdges(opts?: DiscoverOptions): Promise<EdgeInfo[]>;
379
+ listEdges(): Promise<EdgeInfo[]>;
380
+ call<Req, Resp>(edgeId: string, service: string, method: string, req: Req, opts?: CallOptions): Promise<Resp>;
381
+ subscribe<Req, Resp>(edgeId: string, service: string, method: string, req: Req, handler: (resp: Resp) => void): Promise<Subscription>;
382
+ /**
383
+ * Open a bidirectional raw byte channel to an edge service.
384
+ */
385
+ openChannel(edgeId: string, service: string, method: string, request?: unknown): Promise<CenterChannel>;
386
+ /**
387
+ * Call an edge RPC method and receive a streaming response.
388
+ * Returns an async iterable that yields response chunks.
389
+ */
390
+ callEdgeRpcStream<Req>(edgeId: string, service: string, method: string, req: Req, opts?: CallOptions): AsyncGenerator<Uint8Array, void, undefined>;
391
+ private delay;
392
+ private getAuthHeaders;
393
+ /**
394
+ * Register a webhook endpoint for an edge service.
395
+ */
396
+ registerWebhook(opts: {
397
+ edgeId: string;
398
+ service: string;
399
+ method: string;
400
+ secret?: string;
401
+ secretMode?: string;
402
+ description?: string;
403
+ }): Promise<Record<string, unknown>>;
404
+ /**
405
+ * List webhook registrations.
406
+ * @param filter - Optional filter by edgeId
407
+ */
408
+ listWebhooks(filter?: {
409
+ edgeId?: string;
410
+ }): Promise<Array<Record<string, unknown>>>;
411
+ /**
412
+ * Delete a webhook registration.
413
+ * @param webhookId - The webhook ID to delete
414
+ */
415
+ deleteWebhook(webhookId: string): Promise<void>;
416
+ /**
417
+ * Get a specific webhook by ID.
418
+ * @param webhookId - The webhook ID
419
+ */
420
+ getWebhook(webhookId: string): Promise<Record<string, unknown>>;
421
+ /**
422
+ * Describe a service by name, returning its ServiceDescriptor.
423
+ * @param serviceName - The name or ID of the service to describe
424
+ * @returns Promise resolving to the ServiceDescriptor
425
+ */
426
+ describeService(serviceName: string): Promise<ServiceDescriptor>;
427
+ /**
428
+ * Describe a specific method of a service, returning its MethodDescriptor.
429
+ * @param serviceName - The name or ID of the service
430
+ * @param methodName - The name of the method to describe
431
+ * @returns Promise resolving to the MethodDescriptor
432
+ */
433
+ describeMethod(serviceName: string, methodName: string): Promise<MethodDescriptor>;
434
+ /**
435
+ * Get LLM-friendly context (llm.txt format) for specified services.
436
+ * @param serviceNames - A single service name or array of service names
437
+ * @returns Promise resolving to llm.txt formatted string
438
+ */
439
+ getLLMContext(serviceNames: string | string[]): Promise<string>;
440
+ /**
441
+ * Get LLM-friendly context (llm.txt format) for all registered services.
442
+ * @returns Promise resolving to llm.txt formatted string
443
+ */
444
+ getAllLLMContext(): Promise<string>;
445
+ /**
446
+ * List all services with their full ServiceDescriptors.
447
+ * @returns Promise resolving to array of ServiceDescriptors
448
+ */
449
+ listServicesWithDescriptions(): Promise<ServiceDescriptor[]>;
450
+ /**
451
+ * Internal helper to list services from the API.
452
+ */
453
+ private listServicesInternal;
454
+ /**
455
+ * Internal helper to get descriptors for a list of services.
456
+ */
457
+ private getDescriptorsForServices;
458
+ /**
459
+ * Create a new API key.
460
+ * @param options - Key creation options
461
+ * @returns Promise resolving to the created key metadata and API key string
462
+ * @throws Error if key creation fails
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const result = await client.createKey({
467
+ * name: 'my-edge-agent',
468
+ * type: 'edge',
469
+ * });
470
+ * console.log('API Key (save this!):', result.apiKey);
471
+ * console.log('Edge ID:', result.key.edgeId);
472
+ * ```
473
+ */
474
+ createKey(options: CreateKeyOptions): Promise<CreateKeyResult>;
475
+ /**
476
+ * List API keys with optional filtering.
477
+ * @param options - Optional filter options
478
+ * @returns Promise resolving to array of key metadata
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * // List all edge keys
483
+ * const keys = await client.listKeys({ type: 'edge' });
484
+ * for (const key of keys) {
485
+ * console.log(`${key.name}: ${key.status}`);
486
+ * }
487
+ * ```
488
+ */
489
+ listKeys(options?: ListKeysOptions): Promise<KeyMetadata[]>;
490
+ /**
491
+ * Get details of a specific key.
492
+ * @param keyId - The key ID to retrieve
493
+ * @returns Promise resolving to key metadata
494
+ *
495
+ * @example
496
+ * ```typescript
497
+ * const key = await client.getKey('key-123');
498
+ * console.log(`Key status: ${key.status}`);
499
+ * ```
500
+ */
501
+ getKey(keyId: string): Promise<KeyMetadata>;
502
+ /**
503
+ * Revoke an API key.
504
+ * @param keyId - The key ID to revoke
505
+ * @param reason - Optional reason for revocation
506
+ *
507
+ * @example
508
+ * ```typescript
509
+ * await client.revokeKey('key-123', 'No longer needed');
510
+ * ```
511
+ */
512
+ revokeKey(keyId: string, reason?: string): Promise<void>;
513
+ /**
514
+ * Delete a revoked API key permanently.
515
+ * Only revoked keys can be deleted - active keys must be revoked first.
516
+ * @param keyId - The key ID to delete
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * await client.deleteKey('key-123');
521
+ * ```
522
+ */
523
+ deleteKey(keyId: string): Promise<void>;
524
+ /**
525
+ * Rotate an API key (revokes old key and creates new one).
526
+ * @param keyId - The key ID to rotate
527
+ * @returns Promise resolving to the new key metadata and API key string
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * const result = await client.rotateKey('key-123');
532
+ * console.log('New API Key:', result.apiKey);
533
+ * ```
534
+ */
535
+ rotateKey(keyId: string): Promise<CreateKeyResult>;
536
+ /**
537
+ * Expose a center service into a group namespace.
538
+ * Edges in the group can call this service.
539
+ * Subscribes on: center.{appId}.group.{groupId}.services.{name}.>
540
+ */
541
+ exposeToGroup(groupId: string, handler: ServiceHandler): Promise<Subscription>;
542
+ /**
543
+ * Call a specific edge's service within a group.
544
+ * Publishes to: edge.{appId}.group.{groupId}.{edgeId}.services.{service}.{method}
545
+ */
546
+ callGroup<Req, Resp>(groupId: string, edgeId: string, service: string, method: string, req: Req, opts?: CallOptions): Promise<Resp>;
547
+ /**
548
+ * Scatter call to all edges in a group.
549
+ * Discovers edges in the group, then calls each via directed subject in parallel.
550
+ * Returns a map of edgeID → response data.
551
+ */
552
+ scatterGroup<Req, Resp>(groupId: string, service: string, method: string, req: Req, opts?: CallOptions): Promise<Record<string, Resp>>;
553
+ /**
554
+ * Helper to parse key metadata from API response.
555
+ */
556
+ private parseKeyMetadata;
557
+ /**
558
+ * Internal helper to format ServiceDescriptors as llm.txt.
559
+ */
560
+ private formatAsLLMTxt;
561
+ /**
562
+ * Call an RPC method on any edge exposing the given service.
563
+ * Discovers an available edge first, then routes the call.
564
+ */
565
+ callServiceRpc<Req, Resp>(service: string, method: string, req: Req, opts?: CallOptions & {
566
+ labels?: Record<string, string>;
567
+ }): Promise<Resp>;
568
+ /**
569
+ * List all services available in the mesh.
570
+ */
571
+ listServices(): Promise<Array<{
572
+ name: string;
573
+ version?: string;
574
+ edgeCount: number;
575
+ }>>;
576
+ }
577
+
578
+ export {};
@@ -162,4 +162,3 @@ export declare class EdgeClient {
162
162
  private registerPeerService;
163
163
  private getAuthHeaders;
164
164
  }
165
- //# sourceMappingURL=client.d.ts.map
@@ -413,6 +413,7 @@ export class EdgeClient {
413
413
  const nc = this.nc;
414
414
  let inputSub = null;
415
415
  let cancelSub = null;
416
+ let ctrlSub = null;
416
417
  let open = true;
417
418
  try {
418
419
  // Parse the channel initiation message
@@ -424,6 +425,16 @@ export class EdgeClient {
424
425
  if (channelPayload && typeof channelPayload === 'object' && 'payload' in channelPayload) {
425
426
  channelPayload = channelPayload.payload || {};
426
427
  }
428
+ // Generate channel ID from output subject
429
+ const channelId = 'ch_' + (outputSubject.split('.').pop() || outputSubject);
430
+ // Control message handlers
431
+ const controlHandlers = [];
432
+ // Backpressure tracking
433
+ let _bufferedAmount = 0;
434
+ const drainHandlers = [];
435
+ // Closed promise
436
+ let resolveClosed;
437
+ const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
427
438
  const dataHandlers = [];
428
439
  const closeHandlers = [];
429
440
  const cleanup = (reason) => {
@@ -432,6 +443,8 @@ export class EdgeClient {
432
443
  open = false;
433
444
  inputSub?.unsubscribe();
434
445
  cancelSub?.unsubscribe();
446
+ ctrlSub?.unsubscribe();
447
+ resolveClosed();
435
448
  for (const h of closeHandlers) {
436
449
  try {
437
450
  h(reason);
@@ -460,16 +473,50 @@ export class EdgeClient {
460
473
  cleanup('cancelled by client');
461
474
  },
462
475
  });
476
+ // Subscribe to control messages from browser
477
+ try {
478
+ ctrlSub = nc.subscribe(inputSubject + '.ctrl', {
479
+ callback: (_err, msg) => {
480
+ if (!open)
481
+ return;
482
+ try {
483
+ const parsed = JSON.parse(sc.decode(msg.data));
484
+ for (const h of controlHandlers) {
485
+ try {
486
+ h(parsed);
487
+ }
488
+ catch { /* ignore */ }
489
+ }
490
+ }
491
+ catch { /* ignore parse errors */ }
492
+ },
493
+ });
494
+ }
495
+ catch { /* ctrl subscription is optional */ }
463
496
  // ACK the channel request
464
497
  if (msg.reply) {
465
- msg.respond(sc.encode(JSON.stringify({ status: 'channel', cancelSubject })));
498
+ msg.respond(sc.encode(JSON.stringify({ status: 'channel', cancelSubject, channelId })));
466
499
  }
467
500
  // Build the EdgeChannel object
468
501
  const channel = {
502
+ get id() { return channelId; },
469
503
  write(data) {
470
504
  if (!open)
471
- return;
505
+ return false;
472
506
  nc.publish(outputSubject, data);
507
+ _bufferedAmount += data.byteLength;
508
+ queueMicrotask(() => {
509
+ _bufferedAmount = Math.max(0, _bufferedAmount - data.byteLength);
510
+ if (_bufferedAmount === 0) {
511
+ for (const h of drainHandlers) {
512
+ try {
513
+ h();
514
+ }
515
+ catch { /* ignore */ }
516
+ }
517
+ }
518
+ });
519
+ return true;
473
520
  },
474
521
  onData(handler) {
475
522
  dataHandlers.push(handler);
@@ -483,9 +530,18 @@ export class EdgeClient {
483
530
  onClose(handler) {
484
531
  closeHandlers.push(handler);
485
532
  },
486
- get isOpen() {
487
- return open;
533
+ sendControl(msg) {
534
+ if (!open)
535
+ return;
536
+ nc.publish(outputSubject + '.ctrl', sc.encode(JSON.stringify(msg)));
537
+ },
538
+ onControl(handler) {
539
+ controlHandlers.push(handler);
488
540
  },
541
+ get isOpen() { return open; },
542
+ get closed() { return closedPromise; },
543
+ get bufferedAmount() { return _bufferedAmount; },
544
+ onDrain(handler) { drainHandlers.push(handler); },
489
545
  };
490
546
  // Call the handler — it may hold the channel open as long as it needs
491
547
  await handler.handleChannel(method, channel, channelPayload);
@@ -503,6 +559,7 @@ export class EdgeClient {
503
559
  finally {
504
560
  inputSub?.unsubscribe();
505
561
  cancelSub?.unsubscribe();
562
+ ctrlSub?.unsubscribe();
506
563
  }
507
564
  }
508
565
  async unexposeService(name) {