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.
Files changed (180) hide show
  1. package/index.ts +21 -12
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/dist/index.d.ts +11 -0
  21. package/packages/connection-runtime/dist/index.js +11 -0
  22. package/packages/connection-runtime/dist/registry.d.ts +69 -0
  23. package/packages/connection-runtime/dist/registry.js +117 -0
  24. package/packages/connection-runtime/dist/supervisor.d.ts +89 -0
  25. package/packages/connection-runtime/dist/supervisor.js +169 -0
  26. package/packages/connection-runtime/dist/types.d.ts +70 -0
  27. package/packages/connection-runtime/dist/types.js +16 -0
  28. package/packages/connection-runtime/package.json +24 -0
  29. package/packages/connection-runtime/src/index.ts +12 -0
  30. package/packages/connection-runtime/src/registry.ts +142 -0
  31. package/packages/connection-runtime/src/supervisor.ts +224 -0
  32. package/packages/connection-runtime/src/types.ts +88 -0
  33. package/packages/context-compiler/package.json +1 -1
  34. package/packages/context-discovery/package.json +1 -1
  35. package/packages/context-manager/package.json +1 -1
  36. package/packages/contracts/dist/approvals.d.ts +273 -0
  37. package/packages/contracts/dist/approvals.js +93 -0
  38. package/packages/contracts/dist/base.d.ts +78 -0
  39. package/packages/contracts/dist/base.js +82 -0
  40. package/packages/contracts/dist/events.d.ts +99 -0
  41. package/packages/contracts/dist/events.js +65 -0
  42. package/packages/contracts/dist/index.d.ts +20 -0
  43. package/packages/contracts/dist/index.js +20 -0
  44. package/packages/contracts/dist/jobs.d.ts +234 -0
  45. package/packages/contracts/dist/jobs.js +79 -0
  46. package/packages/contracts/dist/notifications.d.ts +317 -0
  47. package/packages/contracts/dist/notifications.js +85 -0
  48. package/packages/contracts/dist/projects.d.ts +171 -0
  49. package/packages/contracts/dist/projects.js +62 -0
  50. package/packages/contracts/dist/sessions.d.ts +226 -0
  51. package/packages/contracts/dist/sessions.js +82 -0
  52. package/packages/contracts/package.json +24 -0
  53. package/packages/contracts/src/approvals.ts +115 -0
  54. package/packages/contracts/src/base.ts +126 -0
  55. package/packages/contracts/src/events.ts +78 -0
  56. package/packages/contracts/src/index.ts +21 -0
  57. package/packages/contracts/src/jobs.ts +101 -0
  58. package/packages/contracts/src/notifications.ts +104 -0
  59. package/packages/contracts/src/projects.ts +81 -0
  60. package/packages/contracts/src/sessions.ts +104 -0
  61. package/packages/cookie-sanitizer/package.json +1 -1
  62. package/packages/cost-optimizer/package.json +1 -1
  63. package/packages/dependency-analyzer/package.json +1 -1
  64. package/packages/django-plugin/package.json +1 -1
  65. package/packages/doc-generator/package.json +1 -1
  66. package/packages/durable-review/package.json +1 -1
  67. package/packages/e2e-tester/package.json +1 -1
  68. package/packages/evaluation-engine/package.json +1 -1
  69. package/packages/evaluation-runner/package.json +1 -1
  70. package/packages/event-bus/package.json +1 -1
  71. package/packages/event-sink/dist/index.d.ts +12 -0
  72. package/packages/event-sink/dist/index.js +12 -0
  73. package/packages/event-sink/dist/outbox.d.ts +51 -0
  74. package/packages/event-sink/dist/outbox.js +88 -0
  75. package/packages/event-sink/dist/projection.d.ts +41 -0
  76. package/packages/event-sink/dist/projection.js +62 -0
  77. package/packages/event-sink/dist/sink.d.ts +81 -0
  78. package/packages/event-sink/dist/sink.js +106 -0
  79. package/packages/event-sink/package.json +28 -0
  80. package/packages/event-sink/src/index.ts +15 -0
  81. package/packages/event-sink/src/outbox.ts +119 -0
  82. package/packages/event-sink/src/projection.ts +84 -0
  83. package/packages/event-sink/src/sink.ts +167 -0
  84. package/packages/event-store/package.json +1 -1
  85. package/packages/experience-replay/package.json +1 -1
  86. package/packages/feedback-collector/package.json +1 -1
  87. package/packages/file-copy-helper/package.json +1 -1
  88. package/packages/framework-detector/package.json +1 -1
  89. package/packages/framework-plugin-sdk/package.json +1 -1
  90. package/packages/frappe-plugin/package.json +1 -1
  91. package/packages/generic-web-plugin/package.json +1 -1
  92. package/packages/health-monitor/package.json +1 -1
  93. package/packages/intent-analyzer/package.json +1 -1
  94. package/packages/jev-judge/package.json +1 -1
  95. package/packages/knowledge-graph/package.json +1 -1
  96. package/packages/knowledge-retrieval/package.json +1 -1
  97. package/packages/laravel-plugin/package.json +1 -1
  98. package/packages/learning-engine/package.json +1 -1
  99. package/packages/mcp-adapter/package.json +1 -1
  100. package/packages/memory-engine/package.json +1 -1
  101. package/packages/milestone-manager/package.json +1 -1
  102. package/packages/model-registry/package.json +1 -1
  103. package/packages/nextjs-plugin/package.json +1 -1
  104. package/packages/notification/package.json +1 -1
  105. package/packages/observability/package.json +1 -1
  106. package/packages/okf-indexer/package.json +1 -1
  107. package/packages/paperclip-integration/package.json +1 -1
  108. package/packages/performance-optimizer/package.json +1 -1
  109. package/packages/permission-mode/dist/checker.d.ts +66 -0
  110. package/packages/permission-mode/dist/checker.js +64 -0
  111. package/packages/permission-mode/dist/index.d.ts +12 -0
  112. package/packages/permission-mode/dist/index.js +11 -0
  113. package/packages/permission-mode/dist/manager.d.ts +65 -0
  114. package/packages/permission-mode/dist/manager.js +91 -0
  115. package/packages/permission-mode/dist/mode.d.ts +31 -0
  116. package/packages/permission-mode/dist/mode.js +47 -0
  117. package/packages/permission-mode/package.json +24 -0
  118. package/packages/permission-mode/src/checker.ts +103 -0
  119. package/packages/permission-mode/src/index.ts +13 -0
  120. package/packages/permission-mode/src/manager.ts +154 -0
  121. package/packages/permission-mode/src/mode.ts +73 -0
  122. package/packages/privilege-broker/package.json +1 -1
  123. package/packages/project-analyzer/package.json +1 -1
  124. package/packages/project-bootstrap/package.json +1 -1
  125. package/packages/projection-engine/package.json +1 -1
  126. package/packages/prompt-compiler/package.json +1 -1
  127. package/packages/prompt-versioning/package.json +1 -1
  128. package/packages/provider-adapter-sdk/package.json +1 -1
  129. package/packages/provider-router/package.json +1 -1
  130. package/packages/provider-selector/package.json +1 -1
  131. package/packages/providers/package.json +1 -1
  132. package/packages/qdrant-skills/package.json +1 -1
  133. package/packages/quota-manager/package.json +1 -1
  134. package/packages/rate-limiter/package.json +1 -1
  135. package/packages/react-vite-plugin/package.json +1 -1
  136. package/packages/relay/dist/client.d.ts +53 -0
  137. package/packages/relay/dist/client.js +95 -0
  138. package/packages/relay/dist/index.d.ts +9 -0
  139. package/packages/relay/dist/index.js +8 -0
  140. package/packages/relay/dist/server.d.ts +58 -0
  141. package/packages/relay/dist/server.js +93 -0
  142. package/packages/relay/dist/types.d.ts +35 -0
  143. package/packages/relay/dist/types.js +4 -0
  144. package/packages/relay/package.json +24 -0
  145. package/packages/relay/src/client.ts +119 -0
  146. package/packages/relay/src/index.ts +10 -0
  147. package/packages/relay/src/server.ts +120 -0
  148. package/packages/relay/src/types.ts +38 -0
  149. package/packages/release-manager/package.json +1 -1
  150. package/packages/requirement-compiler/package.json +1 -1
  151. package/packages/runtime/package.json +1 -1
  152. package/packages/scheduler/package.json +1 -1
  153. package/packages/scheduler-adapter/package.json +1 -1
  154. package/packages/session/package.json +1 -1
  155. package/packages/session-api/package.json +1 -1
  156. package/packages/session-export/package.json +1 -1
  157. package/packages/shared-context/package.json +1 -1
  158. package/packages/skill-mcp-client/package.json +1 -1
  159. package/packages/skill-registry/package.json +1 -1
  160. package/packages/skills/package.json +1 -1
  161. package/packages/sprint-planner/package.json +1 -1
  162. package/packages/subscription-engine/package.json +1 -1
  163. package/packages/task-compiler/package.json +1 -1
  164. package/packages/telegram/package.json +1 -1
  165. package/packages/tencentdb-memory/package.json +1 -1
  166. package/packages/tencentdb-sync/package.json +1 -1
  167. package/packages/test-data-generator/package.json +1 -1
  168. package/packages/test-generator/package.json +1 -1
  169. package/packages/todo-bd-sync/package.json +1 -1
  170. package/packages/token-estimation/package.json +1 -1
  171. package/packages/token-optimizer/package.json +1 -1
  172. package/packages/trajectory/package.json +1 -1
  173. package/packages/tui/package.json +1 -1
  174. package/packages/types/package.json +1 -1
  175. package/packages/webmcp-tester/package.json +1 -1
  176. package/packages/wiki/package.json +1 -1
  177. package/packages/workflow-events/package.json +1 -1
  178. package/packages/workspace-scanner/package.json +1 -1
  179. package/packages/worktree/package.json +1 -1
  180. 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
+ }