pi-harness-runtime 1.1.336 → 1.1.337
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/index.ts +21 -12
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/dist/index.d.ts +11 -0
- package/packages/connection-runtime/dist/index.js +11 -0
- package/packages/connection-runtime/dist/registry.d.ts +69 -0
- package/packages/connection-runtime/dist/registry.js +117 -0
- package/packages/connection-runtime/dist/supervisor.d.ts +89 -0
- package/packages/connection-runtime/dist/supervisor.js +169 -0
- package/packages/connection-runtime/dist/types.d.ts +70 -0
- package/packages/connection-runtime/dist/types.js +16 -0
- package/packages/connection-runtime/package.json +24 -0
- package/packages/connection-runtime/src/index.ts +12 -0
- package/packages/connection-runtime/src/registry.ts +142 -0
- package/packages/connection-runtime/src/supervisor.ts +224 -0
- package/packages/connection-runtime/src/types.ts +88 -0
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/approvals.d.ts +273 -0
- package/packages/contracts/dist/approvals.js +93 -0
- package/packages/contracts/dist/base.d.ts +78 -0
- package/packages/contracts/dist/base.js +82 -0
- package/packages/contracts/dist/events.d.ts +99 -0
- package/packages/contracts/dist/events.js +65 -0
- package/packages/contracts/dist/index.d.ts +20 -0
- package/packages/contracts/dist/index.js +20 -0
- package/packages/contracts/dist/jobs.d.ts +234 -0
- package/packages/contracts/dist/jobs.js +79 -0
- package/packages/contracts/dist/notifications.d.ts +317 -0
- package/packages/contracts/dist/notifications.js +85 -0
- package/packages/contracts/dist/projects.d.ts +171 -0
- package/packages/contracts/dist/projects.js +62 -0
- package/packages/contracts/dist/sessions.d.ts +226 -0
- package/packages/contracts/dist/sessions.js +82 -0
- package/packages/contracts/package.json +24 -0
- package/packages/contracts/src/approvals.ts +115 -0
- package/packages/contracts/src/base.ts +126 -0
- package/packages/contracts/src/events.ts +78 -0
- package/packages/contracts/src/index.ts +21 -0
- package/packages/contracts/src/jobs.ts +101 -0
- package/packages/contracts/src/notifications.ts +104 -0
- package/packages/contracts/src/projects.ts +81 -0
- package/packages/contracts/src/sessions.ts +104 -0
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/dist/index.d.ts +12 -0
- package/packages/event-sink/dist/index.js +12 -0
- package/packages/event-sink/dist/outbox.d.ts +51 -0
- package/packages/event-sink/dist/outbox.js +88 -0
- package/packages/event-sink/dist/projection.d.ts +41 -0
- package/packages/event-sink/dist/projection.js +62 -0
- package/packages/event-sink/dist/sink.d.ts +81 -0
- package/packages/event-sink/dist/sink.js +106 -0
- package/packages/event-sink/package.json +28 -0
- package/packages/event-sink/src/index.ts +15 -0
- package/packages/event-sink/src/outbox.ts +119 -0
- package/packages/event-sink/src/projection.ts +84 -0
- package/packages/event-sink/src/sink.ts +167 -0
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/dist/checker.d.ts +66 -0
- package/packages/permission-mode/dist/checker.js +64 -0
- package/packages/permission-mode/dist/index.d.ts +12 -0
- package/packages/permission-mode/dist/index.js +11 -0
- package/packages/permission-mode/dist/manager.d.ts +65 -0
- package/packages/permission-mode/dist/manager.js +91 -0
- package/packages/permission-mode/dist/mode.d.ts +31 -0
- package/packages/permission-mode/dist/mode.js +47 -0
- package/packages/permission-mode/package.json +24 -0
- package/packages/permission-mode/src/checker.ts +103 -0
- package/packages/permission-mode/src/index.ts +13 -0
- package/packages/permission-mode/src/manager.ts +154 -0
- package/packages/permission-mode/src/mode.ts +73 -0
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/client.d.ts +53 -0
- package/packages/relay/dist/client.js +95 -0
- package/packages/relay/dist/index.d.ts +9 -0
- package/packages/relay/dist/index.js +8 -0
- package/packages/relay/dist/server.d.ts +58 -0
- package/packages/relay/dist/server.js +93 -0
- package/packages/relay/dist/types.d.ts +35 -0
- package/packages/relay/dist/types.js +4 -0
- package/packages/relay/package.json +24 -0
- package/packages/relay/src/client.ts +119 -0
- package/packages/relay/src/index.ts +10 -0
- package/packages/relay/src/server.ts +120 -0
- package/packages/relay/src/types.ts +38 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Supervisor
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's connection supervisor:
|
|
5
|
+
* - Jittered exponential backoff for reconnection
|
|
6
|
+
* - Resets delay after successful connection
|
|
7
|
+
* - Cap at maxDelay to prevent thundering herd
|
|
8
|
+
*/
|
|
9
|
+
import { DEFAULT_CONNECTION_OPTIONS } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Sleep utility - cross-platform
|
|
12
|
+
*/
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
// SAFETY: globalThis is always available in JS environments
|
|
15
|
+
// We assert it has setTimeout which is true for browsers and Node.js
|
|
16
|
+
const global = globalThis;
|
|
17
|
+
return new Promise(resolve => {
|
|
18
|
+
const tid = global.setTimeout(resolve, ms);
|
|
19
|
+
// Prevent unref warning in Node.js
|
|
20
|
+
if (typeof tid.unref === 'function') {
|
|
21
|
+
tid.unref?.();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Connection Supervisor
|
|
27
|
+
*
|
|
28
|
+
* Manages transport connection with automatic reconnection using
|
|
29
|
+
* jittered exponential backoff.
|
|
30
|
+
*/
|
|
31
|
+
export class ConnectionSupervisor {
|
|
32
|
+
transport;
|
|
33
|
+
delay;
|
|
34
|
+
maxDelay;
|
|
35
|
+
maxRetries;
|
|
36
|
+
jitter;
|
|
37
|
+
retries = 0;
|
|
38
|
+
state = 'idle';
|
|
39
|
+
stopping = false;
|
|
40
|
+
onConnected;
|
|
41
|
+
onDisconnected;
|
|
42
|
+
onError;
|
|
43
|
+
onRetry;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
const opts = { ...DEFAULT_CONNECTION_OPTIONS, ...options };
|
|
46
|
+
this.transport = options.transport;
|
|
47
|
+
this.delay = opts.initialDelay ?? 1000;
|
|
48
|
+
this.maxDelay = opts.maxDelay ?? 5 * 60 * 1000;
|
|
49
|
+
this.maxRetries = opts.maxRetries ?? 0;
|
|
50
|
+
this.jitter = opts.jitter ?? true;
|
|
51
|
+
this.onConnected = options.onConnected;
|
|
52
|
+
this.onDisconnected = options.onDisconnected;
|
|
53
|
+
this.onError = options.onError;
|
|
54
|
+
this.onRetry = options.onRetry;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Current connection state
|
|
58
|
+
*/
|
|
59
|
+
getState() {
|
|
60
|
+
return this.state;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Number of retries attempted
|
|
64
|
+
*/
|
|
65
|
+
getRetries() {
|
|
66
|
+
return this.retries;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Start the connection
|
|
70
|
+
*/
|
|
71
|
+
async connect() {
|
|
72
|
+
if (this.state === 'connected' || this.state === 'connecting') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.stopping = false;
|
|
76
|
+
this.state = 'connecting';
|
|
77
|
+
try {
|
|
78
|
+
await this.transport.open();
|
|
79
|
+
this.onConnected?.();
|
|
80
|
+
this.state = 'connected';
|
|
81
|
+
this.delay = this.getInitialDelay(); // Reset on success
|
|
82
|
+
this.retries = 0;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.handleConnectionError(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Stop the connection
|
|
90
|
+
*/
|
|
91
|
+
async disconnect() {
|
|
92
|
+
this.stopping = true;
|
|
93
|
+
this.state = 'stopped';
|
|
94
|
+
try {
|
|
95
|
+
await this.transport.close();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore close errors
|
|
99
|
+
}
|
|
100
|
+
this.onDisconnected?.();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Force reconnection
|
|
104
|
+
*/
|
|
105
|
+
async reconnect() {
|
|
106
|
+
if (this.state === 'reconnecting') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.state = 'reconnecting';
|
|
110
|
+
await this.attemptConnection();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Handle connection error with backoff
|
|
114
|
+
*/
|
|
115
|
+
async handleConnectionError(error) {
|
|
116
|
+
this.onError?.(error);
|
|
117
|
+
if (this.stopping) {
|
|
118
|
+
this.state = 'stopped';
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.state = 'reconnecting';
|
|
122
|
+
await this.attemptConnection();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Attempt connection with backoff
|
|
126
|
+
*/
|
|
127
|
+
async attemptConnection() {
|
|
128
|
+
while (!this.stopping) {
|
|
129
|
+
this.retries++;
|
|
130
|
+
if (this.maxRetries > 0 && this.retries > this.maxRetries) {
|
|
131
|
+
this.state = 'error';
|
|
132
|
+
this.onError?.(new Error(`Max retries (${this.maxRetries}) exceeded`));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Calculate delay with optional jitter
|
|
136
|
+
const actualDelay = this.jitter
|
|
137
|
+
? this.delay + Math.random() * this.delay * 0.3 // Up to 30% jitter
|
|
138
|
+
: this.delay;
|
|
139
|
+
this.onRetry?.(this.retries, actualDelay);
|
|
140
|
+
await sleep(actualDelay);
|
|
141
|
+
if (this.stopping) {
|
|
142
|
+
this.state = 'stopped';
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await this.transport.open();
|
|
147
|
+
this.onConnected?.();
|
|
148
|
+
this.state = 'connected';
|
|
149
|
+
this.delay = this.getInitialDelay(); // Reset on success
|
|
150
|
+
this.retries = 0;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
this.onError?.(error);
|
|
155
|
+
// Exponential backoff
|
|
156
|
+
this.delay = Math.min(this.delay * 2, this.maxDelay);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.state = 'stopped';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get initial delay (allows override)
|
|
163
|
+
*/
|
|
164
|
+
getInitialDelay() {
|
|
165
|
+
return this.transport.isConnected()
|
|
166
|
+
? this.delay
|
|
167
|
+
: 1000;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection types
|
|
3
|
+
*
|
|
4
|
+
* Note: Uses minimal EventEmitter-like interface to avoid Node.js dependency.
|
|
5
|
+
* Implementations can swap in Node's EventEmitter or use any compatible event system.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Minimal event emitter interface
|
|
9
|
+
*/
|
|
10
|
+
export interface EventEmitter {
|
|
11
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
12
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
13
|
+
emit(event: string, ...args: unknown[]): void;
|
|
14
|
+
once(event: string, listener: (...args: unknown[]) => void): void;
|
|
15
|
+
removeAllListeners(event?: string): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Connection state
|
|
19
|
+
*/
|
|
20
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
|
21
|
+
/**
|
|
22
|
+
* Connection error
|
|
23
|
+
*/
|
|
24
|
+
export interface ConnectionError extends Error {
|
|
25
|
+
code?: string;
|
|
26
|
+
recoverable?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Transport interface
|
|
30
|
+
*/
|
|
31
|
+
export interface Transport {
|
|
32
|
+
/** Open the connection */
|
|
33
|
+
open(): Promise<void>;
|
|
34
|
+
/** Close the connection */
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
/** Send a message */
|
|
37
|
+
send(data: unknown): Promise<void>;
|
|
38
|
+
/** Check if connected */
|
|
39
|
+
isConnected(): boolean;
|
|
40
|
+
/** Event emitter for connection events */
|
|
41
|
+
events: EventEmitter;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Transport events
|
|
45
|
+
*/
|
|
46
|
+
export interface TransportEvents {
|
|
47
|
+
'open': () => void;
|
|
48
|
+
'close': () => void;
|
|
49
|
+
'error': (error: Error) => void;
|
|
50
|
+
'message': (data: unknown) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Connection options
|
|
54
|
+
*/
|
|
55
|
+
export interface ConnectionOptions {
|
|
56
|
+
/** Initial retry delay in ms */
|
|
57
|
+
initialDelay?: number;
|
|
58
|
+
/** Maximum retry delay in ms */
|
|
59
|
+
maxDelay?: number;
|
|
60
|
+
/** Maximum number of retries (0 = infinite) */
|
|
61
|
+
maxRetries?: number;
|
|
62
|
+
/** Enable jitter (recommended) */
|
|
63
|
+
jitter?: boolean;
|
|
64
|
+
/** Enable verbose logging */
|
|
65
|
+
verbose?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Default connection options
|
|
69
|
+
*/
|
|
70
|
+
export declare const DEFAULT_CONNECTION_OPTIONS: Required<ConnectionOptions>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection types
|
|
3
|
+
*
|
|
4
|
+
* Note: Uses minimal EventEmitter-like interface to avoid Node.js dependency.
|
|
5
|
+
* Implementations can swap in Node's EventEmitter or use any compatible event system.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default connection options
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_CONNECTION_OPTIONS = {
|
|
11
|
+
initialDelay: 1000,
|
|
12
|
+
maxDelay: 5 * 60 * 1000, // 5 minutes
|
|
13
|
+
maxRetries: 0, // Infinite
|
|
14
|
+
jitter: true,
|
|
15
|
+
verbose: false,
|
|
16
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-harness/connection-runtime",
|
|
3
|
+
"version": "1.1.337",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Connection management with jittered exponential backoff for pi-harness",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "bun test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@pi-harness/contracts": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.7.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Runtime for pi-harness
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's client-runtime:
|
|
5
|
+
* - Jittered exponential backoff for reconnection
|
|
6
|
+
* - One transport retry owner per environment
|
|
7
|
+
* - Shared subscription logic across clients
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './supervisor.js';
|
|
11
|
+
export * from './registry.js';
|
|
12
|
+
export * from './types.js';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Registry
|
|
3
|
+
*
|
|
4
|
+
* Scopes connections by environment/context.
|
|
5
|
+
* Tracks active connections and their state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EventEmitter } from './types.js';
|
|
9
|
+
import { ConnectionSupervisor } from './supervisor.js';
|
|
10
|
+
|
|
11
|
+
export interface RegistryEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
supervisor: ConnectionSupervisor;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
lastConnectedAt?: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Connection Registry
|
|
21
|
+
*
|
|
22
|
+
* Manages multiple connections by ID.
|
|
23
|
+
* Connections persist even after disconnect (for reconnection).
|
|
24
|
+
*/
|
|
25
|
+
export class ConnectionRegistry {
|
|
26
|
+
private connections = new Map<string, RegistryEntry>();
|
|
27
|
+
private onChange?: (entries: RegistryEntry[]) => void;
|
|
28
|
+
|
|
29
|
+
constructor(onChange?: (entries: RegistryEntry[]) => void) {
|
|
30
|
+
this.onChange = onChange;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a new connection
|
|
35
|
+
*/
|
|
36
|
+
register(id: string, supervisor: ConnectionSupervisor, metadata?: Record<string, unknown>): RegistryEntry {
|
|
37
|
+
const entry: RegistryEntry = {
|
|
38
|
+
id,
|
|
39
|
+
supervisor,
|
|
40
|
+
metadata,
|
|
41
|
+
createdAt: new Date(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.connections.set(id, entry);
|
|
45
|
+
this.notifyChange();
|
|
46
|
+
return entry;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Unregister a connection
|
|
51
|
+
*/
|
|
52
|
+
unregister(id: string): boolean {
|
|
53
|
+
const entry = this.connections.get(id);
|
|
54
|
+
if (entry) {
|
|
55
|
+
entry.supervisor.disconnect().catch(() => {});
|
|
56
|
+
this.connections.delete(id);
|
|
57
|
+
this.notifyChange();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get a connection by ID
|
|
65
|
+
*/
|
|
66
|
+
get(id: string): RegistryEntry | undefined {
|
|
67
|
+
return this.connections.get(id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all connections
|
|
72
|
+
*/
|
|
73
|
+
list(): RegistryEntry[] {
|
|
74
|
+
return Array.from(this.connections.values());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get connections by state
|
|
79
|
+
*/
|
|
80
|
+
listByState(state: string): RegistryEntry[] {
|
|
81
|
+
return this.list().filter(e => e.supervisor.getState() === state);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update connection metadata
|
|
86
|
+
*/
|
|
87
|
+
updateMetadata(id: string, metadata: Record<string, unknown>): boolean {
|
|
88
|
+
const entry = this.connections.get(id);
|
|
89
|
+
if (entry) {
|
|
90
|
+
entry.metadata = { ...entry.metadata, ...metadata };
|
|
91
|
+
this.notifyChange();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update last connected timestamp
|
|
99
|
+
*/
|
|
100
|
+
touch(id: string): boolean {
|
|
101
|
+
const entry = this.connections.get(id);
|
|
102
|
+
if (entry) {
|
|
103
|
+
entry.lastConnectedAt = new Date();
|
|
104
|
+
this.notifyChange();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear all connections
|
|
112
|
+
*/
|
|
113
|
+
async clear(): Promise<void> {
|
|
114
|
+
const promises = this.list().map(e => e.supervisor.disconnect());
|
|
115
|
+
await Promise.allSettled(promises);
|
|
116
|
+
this.connections.clear();
|
|
117
|
+
this.notifyChange();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get count of connections
|
|
122
|
+
*/
|
|
123
|
+
size(): number {
|
|
124
|
+
return this.connections.size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if connection exists
|
|
129
|
+
*/
|
|
130
|
+
has(id: string): boolean {
|
|
131
|
+
return this.connections.has(id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Notify change listeners
|
|
136
|
+
*/
|
|
137
|
+
private notifyChange(): void {
|
|
138
|
+
if (this.onChange) {
|
|
139
|
+
this.onChange(this.list());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Supervisor
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's connection supervisor:
|
|
5
|
+
* - Jittered exponential backoff for reconnection
|
|
6
|
+
* - Resets delay after successful connection
|
|
7
|
+
* - Cap at maxDelay to prevent thundering herd
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { EventEmitter } from './types.js';
|
|
11
|
+
import { DEFAULT_CONNECTION_OPTIONS } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sleep utility - cross-platform
|
|
15
|
+
*/
|
|
16
|
+
function sleep(ms: number): Promise<void> {
|
|
17
|
+
// SAFETY: globalThis is always available in JS environments
|
|
18
|
+
// We assert it has setTimeout which is true for browsers and Node.js
|
|
19
|
+
const global = globalThis as unknown as {
|
|
20
|
+
setTimeout: (fn: () => void, ms: number) => { unref?: () => void }
|
|
21
|
+
};
|
|
22
|
+
return new Promise(resolve => {
|
|
23
|
+
const tid = global.setTimeout(resolve, ms);
|
|
24
|
+
// Prevent unref warning in Node.js
|
|
25
|
+
if (typeof tid.unref === 'function') {
|
|
26
|
+
tid.unref?.();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Connection Supervisor options
|
|
33
|
+
*/
|
|
34
|
+
export interface SupervisorOptions {
|
|
35
|
+
/** Transport to manage */
|
|
36
|
+
transport: EventEmitter & {
|
|
37
|
+
open(): Promise<void>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
isConnected(): boolean;
|
|
40
|
+
};
|
|
41
|
+
/** Initial retry delay in ms */
|
|
42
|
+
initialDelay?: number;
|
|
43
|
+
/** Maximum retry delay in ms */
|
|
44
|
+
maxDelay?: number;
|
|
45
|
+
/** Maximum retries (0 = infinite) */
|
|
46
|
+
maxRetries?: number;
|
|
47
|
+
/** Enable jitter */
|
|
48
|
+
jitter?: boolean;
|
|
49
|
+
/** Callback when connected */
|
|
50
|
+
onConnected?: () => void;
|
|
51
|
+
/** Callback when disconnected */
|
|
52
|
+
onDisconnected?: () => void;
|
|
53
|
+
/** Callback on error */
|
|
54
|
+
onError?: (error: Error) => void;
|
|
55
|
+
/** Callback on retry attempt */
|
|
56
|
+
onRetry?: (attempt: number, delay: number) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Connection Supervisor
|
|
61
|
+
*
|
|
62
|
+
* Manages transport connection with automatic reconnection using
|
|
63
|
+
* jittered exponential backoff.
|
|
64
|
+
*/
|
|
65
|
+
export class ConnectionSupervisor {
|
|
66
|
+
private transport: SupervisorOptions['transport'];
|
|
67
|
+
private delay: number;
|
|
68
|
+
private maxDelay: number;
|
|
69
|
+
private maxRetries: number;
|
|
70
|
+
private jitter: boolean;
|
|
71
|
+
private retries = 0;
|
|
72
|
+
private state: 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'stopped' | 'error' = 'idle';
|
|
73
|
+
private stopping = false;
|
|
74
|
+
private onConnected?: () => void;
|
|
75
|
+
private onDisconnected?: () => void;
|
|
76
|
+
private onError?: (error: Error) => void;
|
|
77
|
+
private onRetry?: (attempt: number, delay: number) => void;
|
|
78
|
+
|
|
79
|
+
constructor(options: SupervisorOptions) {
|
|
80
|
+
const opts = { ...DEFAULT_CONNECTION_OPTIONS, ...options };
|
|
81
|
+
|
|
82
|
+
this.transport = options.transport;
|
|
83
|
+
this.delay = opts.initialDelay ?? 1000;
|
|
84
|
+
this.maxDelay = opts.maxDelay ?? 5 * 60 * 1000;
|
|
85
|
+
this.maxRetries = opts.maxRetries ?? 0;
|
|
86
|
+
this.jitter = opts.jitter ?? true;
|
|
87
|
+
this.onConnected = options.onConnected;
|
|
88
|
+
this.onDisconnected = options.onDisconnected;
|
|
89
|
+
this.onError = options.onError;
|
|
90
|
+
this.onRetry = options.onRetry;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Current connection state
|
|
95
|
+
*/
|
|
96
|
+
getState() {
|
|
97
|
+
return this.state;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Number of retries attempted
|
|
102
|
+
*/
|
|
103
|
+
getRetries() {
|
|
104
|
+
return this.retries;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the connection
|
|
109
|
+
*/
|
|
110
|
+
async connect(): Promise<void> {
|
|
111
|
+
if (this.state === 'connected' || this.state === 'connecting') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.stopping = false;
|
|
116
|
+
this.state = 'connecting';
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await this.transport.open();
|
|
120
|
+
this.onConnected?.();
|
|
121
|
+
this.state = 'connected';
|
|
122
|
+
this.delay = this.getInitialDelay(); // Reset on success
|
|
123
|
+
this.retries = 0;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.handleConnectionError(error as Error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stop the connection
|
|
131
|
+
*/
|
|
132
|
+
async disconnect(): Promise<void> {
|
|
133
|
+
this.stopping = true;
|
|
134
|
+
this.state = 'stopped';
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await this.transport.close();
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore close errors
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.onDisconnected?.();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Force reconnection
|
|
147
|
+
*/
|
|
148
|
+
async reconnect(): Promise<void> {
|
|
149
|
+
if (this.state === 'reconnecting') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.state = 'reconnecting';
|
|
154
|
+
await this.attemptConnection();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handle connection error with backoff
|
|
159
|
+
*/
|
|
160
|
+
private async handleConnectionError(error: Error): Promise<void> {
|
|
161
|
+
this.onError?.(error);
|
|
162
|
+
|
|
163
|
+
if (this.stopping) {
|
|
164
|
+
this.state = 'stopped';
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.state = 'reconnecting';
|
|
169
|
+
await this.attemptConnection();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Attempt connection with backoff
|
|
174
|
+
*/
|
|
175
|
+
private async attemptConnection(): Promise<void> {
|
|
176
|
+
while (!this.stopping) {
|
|
177
|
+
this.retries++;
|
|
178
|
+
|
|
179
|
+
if (this.maxRetries > 0 && this.retries > this.maxRetries) {
|
|
180
|
+
this.state = 'error';
|
|
181
|
+
this.onError?.(new Error(`Max retries (${this.maxRetries}) exceeded`));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Calculate delay with optional jitter
|
|
186
|
+
const actualDelay = this.jitter
|
|
187
|
+
? this.delay + Math.random() * this.delay * 0.3 // Up to 30% jitter
|
|
188
|
+
: this.delay;
|
|
189
|
+
|
|
190
|
+
this.onRetry?.(this.retries, actualDelay);
|
|
191
|
+
await sleep(actualDelay);
|
|
192
|
+
|
|
193
|
+
if (this.stopping) {
|
|
194
|
+
this.state = 'stopped';
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await this.transport.open();
|
|
200
|
+
this.onConnected?.();
|
|
201
|
+
this.state = 'connected';
|
|
202
|
+
this.delay = this.getInitialDelay(); // Reset on success
|
|
203
|
+
this.retries = 0;
|
|
204
|
+
return;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.onError?.(error as Error);
|
|
207
|
+
|
|
208
|
+
// Exponential backoff
|
|
209
|
+
this.delay = Math.min(this.delay * 2, this.maxDelay);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.state = 'stopped';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get initial delay (allows override)
|
|
218
|
+
*/
|
|
219
|
+
protected getInitialDelay(): number {
|
|
220
|
+
return this.transport.isConnected()
|
|
221
|
+
? this.delay
|
|
222
|
+
: 1000;
|
|
223
|
+
}
|
|
224
|
+
}
|