@mindexec/cli 0.2.135 → 0.2.137
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/package.json +1 -1
- package/scripts/remote-frame-ws-smoke.mjs +14 -0
- package/scripts/remote-registry-follower-smoke.mjs +123 -6
- package/server.js +604 -8
package/package.json
CHANGED
|
@@ -184,6 +184,20 @@ async function main() {
|
|
|
184
184
|
assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
|
|
185
185
|
assert.equal(frame.metadata.mimeType, 'image/png');
|
|
186
186
|
assert.ok(frame.payload.length > 0, 'payload should be non-empty');
|
|
187
|
+
|
|
188
|
+
const frameStatus = await fetchJson(`${bridge.baseUrl}/api/status?remoteFrames=ws`);
|
|
189
|
+
assert.equal(frameStatus.ok, true);
|
|
190
|
+
assert.ok(frameStatus.payload?.remoteFrameWs?.clientCount >= 1, JSON.stringify(frameStatus.payload?.remoteFrameWs));
|
|
191
|
+
assert.ok(frameStatus.payload?.remoteFrameWs?.framesSent >= 1, JSON.stringify(frameStatus.payload?.remoteFrameWs));
|
|
192
|
+
assert.equal(frameStatus.payload?.remoteFrameWs?.lastFrameDeviceId, device.deviceId);
|
|
193
|
+
assert.equal(frameStatus.payload?.remoteFrameWs?.lastFrameSeq, frame.metadata.frameSeq);
|
|
194
|
+
assert.ok(
|
|
195
|
+
frameStatus.payload?.remoteFrameWs?.clients?.some(client =>
|
|
196
|
+
Array.isArray(client.deviceIds)
|
|
197
|
+
&& client.deviceIds.includes(device.deviceId)
|
|
198
|
+
&& client.autoStartLive === true
|
|
199
|
+
&& client.sent >= 1),
|
|
200
|
+
JSON.stringify(frameStatus.payload?.remoteFrameWs));
|
|
187
201
|
ws.close();
|
|
188
202
|
|
|
189
203
|
console.log('Remote frame WebSocket smoke OK');
|
|
@@ -8,6 +8,7 @@ import net from 'node:net';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { WebSocketServer } from 'ws';
|
|
11
12
|
|
|
12
13
|
const BRIDGE_TOKEN = 'remote-registry-follower-smoke-token';
|
|
13
14
|
const PAIR_TOKEN = 'remote-registry-follower-pair-token';
|
|
@@ -84,6 +85,7 @@ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active =
|
|
|
84
85
|
|
|
85
86
|
function startFakeSupabase(getTarget) {
|
|
86
87
|
const requests = [];
|
|
88
|
+
const realtimeClients = new Set();
|
|
87
89
|
const server = createServer((req, res) => {
|
|
88
90
|
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
89
91
|
requests.push({
|
|
@@ -108,6 +110,77 @@ function startFakeSupabase(getTarget) {
|
|
|
108
110
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
109
111
|
res.end(JSON.stringify({ error: 'not-found' }));
|
|
110
112
|
});
|
|
113
|
+
const realtime = new WebSocketServer({ noServer: true });
|
|
114
|
+
|
|
115
|
+
realtime.on('connection', (ws, req) => {
|
|
116
|
+
realtimeClients.add(ws);
|
|
117
|
+
ws.on('message', data => {
|
|
118
|
+
const text = data.toString();
|
|
119
|
+
let message = null;
|
|
120
|
+
try {
|
|
121
|
+
message = JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (message.event === 'phx_join') {
|
|
127
|
+
assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
|
|
128
|
+
assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
|
|
129
|
+
assert.equal(message.payload?.access_token, ACCESS_TOKEN);
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
message.payload?.config?.postgres_changes?.[0],
|
|
132
|
+
{
|
|
133
|
+
event: '*',
|
|
134
|
+
schema: 'public',
|
|
135
|
+
table: 'remote_host_targets',
|
|
136
|
+
filter: `user_id=eq.${USER_ID}`
|
|
137
|
+
});
|
|
138
|
+
ws.remoteRegistryTopic = message.topic;
|
|
139
|
+
ws.send(JSON.stringify({
|
|
140
|
+
topic: message.topic,
|
|
141
|
+
event: 'phx_reply',
|
|
142
|
+
payload: {
|
|
143
|
+
status: 'ok',
|
|
144
|
+
response: {
|
|
145
|
+
postgres_changes: [
|
|
146
|
+
{
|
|
147
|
+
id: 1,
|
|
148
|
+
event: '*',
|
|
149
|
+
schema: 'public',
|
|
150
|
+
table: 'remote_host_targets'
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
ref: message.ref,
|
|
156
|
+
join_ref: message.join_ref
|
|
157
|
+
}));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (message.event === 'heartbeat') {
|
|
162
|
+
ws.send(JSON.stringify({
|
|
163
|
+
topic: message.topic,
|
|
164
|
+
event: 'phx_reply',
|
|
165
|
+
payload: { status: 'ok', response: {} },
|
|
166
|
+
ref: message.ref
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
ws.on('close', () => realtimeClients.delete(ws));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
server.on('upgrade', (req, socket, head) => {
|
|
174
|
+
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
175
|
+
if (parsed.pathname !== '/realtime/v1/websocket') {
|
|
176
|
+
socket.destroy();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
realtime.handleUpgrade(req, socket, head, ws => {
|
|
181
|
+
realtime.emit('connection', ws, req);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
111
184
|
|
|
112
185
|
return new Promise((resolve, reject) => {
|
|
113
186
|
server.once('error', reject);
|
|
@@ -117,7 +190,41 @@ function startFakeSupabase(getTarget) {
|
|
|
117
190
|
resolve({
|
|
118
191
|
url: `http://127.0.0.1:${port}`,
|
|
119
192
|
requests,
|
|
120
|
-
|
|
193
|
+
realtimeClients,
|
|
194
|
+
broadcastChange: (type = 'UPDATE') => {
|
|
195
|
+
for (const client of realtimeClients) {
|
|
196
|
+
if (client.readyState !== 1 || !client.remoteRegistryTopic) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
client.send(JSON.stringify({
|
|
201
|
+
topic: client.remoteRegistryTopic,
|
|
202
|
+
event: 'postgres_changes',
|
|
203
|
+
payload: {
|
|
204
|
+
ids: [1],
|
|
205
|
+
data: {
|
|
206
|
+
schema: 'public',
|
|
207
|
+
table: 'remote_host_targets',
|
|
208
|
+
commit_timestamp: new Date().toISOString(),
|
|
209
|
+
type,
|
|
210
|
+
record: getTarget(),
|
|
211
|
+
old_record: null
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
ref: null
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
stop: () => new Promise(done => {
|
|
219
|
+
for (const client of realtimeClients) {
|
|
220
|
+
try {
|
|
221
|
+
client.close();
|
|
222
|
+
} catch {
|
|
223
|
+
// best effort only
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
realtime.close(() => server.close(done));
|
|
227
|
+
})
|
|
121
228
|
});
|
|
122
229
|
});
|
|
123
230
|
});
|
|
@@ -140,8 +247,10 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
|
|
|
140
247
|
WORKSPACE_PATH: workspacePath,
|
|
141
248
|
MINDEXEC_AUTH_DATA_ROOT: authRoot,
|
|
142
249
|
MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
|
|
143
|
-
MINDEXEC_REMOTE_REGISTRY_POLL_MS: '
|
|
250
|
+
MINDEXEC_REMOTE_REGISTRY_POLL_MS: '10000',
|
|
144
251
|
MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
|
|
252
|
+
MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
|
|
253
|
+
MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
|
|
145
254
|
SUPABASE_URL: supabaseUrl,
|
|
146
255
|
SUPABASE_KEY,
|
|
147
256
|
NO_COLOR: '1'
|
|
@@ -196,7 +305,7 @@ async function writeSession(authRoot) {
|
|
|
196
305
|
}), 'utf8');
|
|
197
306
|
}
|
|
198
307
|
|
|
199
|
-
async function waitForManagedAgent(bridge, managerEndpoint, label) {
|
|
308
|
+
async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 20000) {
|
|
200
309
|
return await waitFor(async () => {
|
|
201
310
|
const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
|
|
202
311
|
const agent = result.payload;
|
|
@@ -205,7 +314,7 @@ async function waitForManagedAgent(bridge, managerEndpoint, label) {
|
|
|
205
314
|
&& String(agent.manager || '') === managerEndpoint
|
|
206
315
|
? agent
|
|
207
316
|
: null;
|
|
208
|
-
},
|
|
317
|
+
}, timeoutMs, `${label} managed agent\n${bridge.details()}`);
|
|
209
318
|
}
|
|
210
319
|
|
|
211
320
|
async function waitForConnectedDevice(bridge, label) {
|
|
@@ -280,14 +389,18 @@ async function main() {
|
|
|
280
389
|
assert.equal(firstAgent.manager, hostAEndpoint);
|
|
281
390
|
assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
|
|
282
391
|
await waitForConnectedDevice(hostA, 'host-a');
|
|
392
|
+
const firstStatus = await fetchJson(`${client.baseUrl}/api/status`);
|
|
393
|
+
assert.equal(firstStatus.payload?.remoteRegistryRealtime?.subscribed, true, JSON.stringify(firstStatus.payload?.remoteRegistryRealtime));
|
|
394
|
+
assert.ok(fakeSupabase.realtimeClients.size >= 1, 'expected LocalBridge realtime watcher to connect');
|
|
283
395
|
|
|
284
396
|
currentTarget = createRegistryTarget({
|
|
285
397
|
endpoint: hostBEndpoint,
|
|
286
398
|
endpointCandidates: [hostBEndpoint],
|
|
287
399
|
leaseId: 'lease-b'
|
|
288
400
|
});
|
|
401
|
+
fakeSupabase.broadcastChange('UPDATE');
|
|
289
402
|
|
|
290
|
-
const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b');
|
|
403
|
+
const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b', 9000);
|
|
291
404
|
assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
|
|
292
405
|
assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
|
|
293
406
|
assert.equal(secondAgent.manager, hostBEndpoint);
|
|
@@ -299,6 +412,7 @@ async function main() {
|
|
|
299
412
|
active: false,
|
|
300
413
|
expires_at: new Date(Date.now() - 1000).toISOString()
|
|
301
414
|
};
|
|
415
|
+
fakeSupabase.broadcastChange('UPDATE');
|
|
302
416
|
|
|
303
417
|
await waitFor(async () => {
|
|
304
418
|
const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
|
|
@@ -306,7 +420,10 @@ async function main() {
|
|
|
306
420
|
&& String(result.payload?.lastError || '') === 'registry-inactive'
|
|
307
421
|
? result.payload
|
|
308
422
|
: null;
|
|
309
|
-
},
|
|
423
|
+
}, 9000, `registry inactive stop\n${client.details()}`);
|
|
424
|
+
const finalStatus = await fetchJson(`${client.baseUrl}/api/status`);
|
|
425
|
+
assert.ok(finalStatus.payload?.remoteRegistryRealtime?.changes >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
|
|
426
|
+
assert.ok(finalStatus.payload?.remoteRegistryRealtime?.wakeups >= 2, JSON.stringify(finalStatus.payload?.remoteRegistryRealtime));
|
|
310
427
|
|
|
311
428
|
assert.ok(fakeSupabase.requests.length >= 2, JSON.stringify(fakeSupabase.requests));
|
|
312
429
|
console.log('Remote registry follower smoke OK');
|
package/server.js
CHANGED
|
@@ -17,7 +17,7 @@ import multer from 'multer';
|
|
|
17
17
|
import crypto from 'crypto';
|
|
18
18
|
import sharp from 'sharp';
|
|
19
19
|
import { createServer } from 'http';
|
|
20
|
-
import { WebSocketServer } from 'ws';
|
|
20
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
21
21
|
import chokidar from 'chokidar';
|
|
22
22
|
import Parser from 'web-tree-sitter';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
@@ -2128,6 +2128,35 @@ const remoteFrameWss = new WebSocketServer({ noServer: true });
|
|
|
2128
2128
|
const wsClients = new Set();
|
|
2129
2129
|
const remoteFrameClients = new Set();
|
|
2130
2130
|
const shellJobs = new Map();
|
|
2131
|
+
let remoteFrameClientSeq = 0;
|
|
2132
|
+
const remoteFrameWsDiagnostics = {
|
|
2133
|
+
opened: 0,
|
|
2134
|
+
closed: 0,
|
|
2135
|
+
errors: 0,
|
|
2136
|
+
framesReceived: 0,
|
|
2137
|
+
framesSent: 0,
|
|
2138
|
+
framesDropped: 0,
|
|
2139
|
+
framesInvalid: 0,
|
|
2140
|
+
framesNoClients: 0,
|
|
2141
|
+
framesNoSubscribers: 0,
|
|
2142
|
+
autoStartRequested: 0,
|
|
2143
|
+
autoStartStarted: 0,
|
|
2144
|
+
autoStartSkipped: 0,
|
|
2145
|
+
lastOpenAt: '',
|
|
2146
|
+
lastCloseAt: '',
|
|
2147
|
+
lastErrorAt: '',
|
|
2148
|
+
lastFrameAt: '',
|
|
2149
|
+
lastFrameDeviceId: '',
|
|
2150
|
+
lastFrameSeq: 0,
|
|
2151
|
+
lastFrameBytes: 0,
|
|
2152
|
+
lastFrameMatchedClients: 0,
|
|
2153
|
+
lastFrameSentClients: 0,
|
|
2154
|
+
lastFrameDroppedClients: 0,
|
|
2155
|
+
lastAutoStartAt: '',
|
|
2156
|
+
lastAutoStartRequested: 0,
|
|
2157
|
+
lastAutoStartStarted: 0,
|
|
2158
|
+
lastAutoStartSkipped: 0
|
|
2159
|
+
};
|
|
2131
2160
|
const REMOTE_FRAME_WS_AUTO_START_LIMIT = 120;
|
|
2132
2161
|
const REMOTE_FRAME_WS_DEFAULT_FPS = 12;
|
|
2133
2162
|
const REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH = 960;
|
|
@@ -2308,9 +2337,20 @@ function maybeAutoStartRemoteFrameLiveStreams(ws, deviceIds = []) {
|
|
|
2308
2337
|
}
|
|
2309
2338
|
|
|
2310
2339
|
if (started.length > 0 || skipped.length > 0) {
|
|
2340
|
+
const now = new Date().toISOString();
|
|
2341
|
+
ws.remoteFrameLastAutoStartAt = now;
|
|
2342
|
+
ws.remoteFrameLastAutoStartStarted = started.length;
|
|
2343
|
+
ws.remoteFrameLastAutoStartSkipped = skipped.length;
|
|
2344
|
+
remoteFrameWsDiagnostics.autoStartRequested += Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT);
|
|
2345
|
+
remoteFrameWsDiagnostics.autoStartStarted += started.length;
|
|
2346
|
+
remoteFrameWsDiagnostics.autoStartSkipped += skipped.length;
|
|
2347
|
+
remoteFrameWsDiagnostics.lastAutoStartAt = now;
|
|
2348
|
+
remoteFrameWsDiagnostics.lastAutoStartRequested = Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT);
|
|
2349
|
+
remoteFrameWsDiagnostics.lastAutoStartStarted = started.length;
|
|
2350
|
+
remoteFrameWsDiagnostics.lastAutoStartSkipped = skipped.length;
|
|
2311
2351
|
sendRemoteFrameClientJson(ws, {
|
|
2312
2352
|
type: 'RemoteFrameLiveAutoStart',
|
|
2313
|
-
timestamp:
|
|
2353
|
+
timestamp: now,
|
|
2314
2354
|
requested: Math.min(deviceIds.length, REMOTE_FRAME_WS_AUTO_START_LIMIT),
|
|
2315
2355
|
started,
|
|
2316
2356
|
skipped: skipped.slice(0, 24),
|
|
@@ -2352,20 +2392,36 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
|
|
|
2352
2392
|
}
|
|
2353
2393
|
|
|
2354
2394
|
function broadcastRemoteBinaryFrame(frameEvent) {
|
|
2395
|
+
const frame = frameEvent?.frame || {};
|
|
2396
|
+
remoteFrameWsDiagnostics.framesReceived += 1;
|
|
2397
|
+
remoteFrameWsDiagnostics.lastFrameAt = new Date().toISOString();
|
|
2398
|
+
remoteFrameWsDiagnostics.lastFrameDeviceId = String(frameEvent?.deviceId || frame.deviceId || '').trim();
|
|
2399
|
+
remoteFrameWsDiagnostics.lastFrameSeq = Number(frame.frameSeq || 0) || 0;
|
|
2400
|
+
remoteFrameWsDiagnostics.lastFrameBytes = Number(frameEvent?.byteLength || frameEvent?.payload?.length || 0) || 0;
|
|
2401
|
+
remoteFrameWsDiagnostics.lastFrameMatchedClients = 0;
|
|
2402
|
+
remoteFrameWsDiagnostics.lastFrameSentClients = 0;
|
|
2403
|
+
remoteFrameWsDiagnostics.lastFrameDroppedClients = 0;
|
|
2404
|
+
|
|
2355
2405
|
if (remoteFrameClients.size === 0) {
|
|
2406
|
+
remoteFrameWsDiagnostics.framesNoClients += 1;
|
|
2356
2407
|
return;
|
|
2357
2408
|
}
|
|
2358
2409
|
|
|
2359
|
-
const deviceId =
|
|
2410
|
+
const deviceId = remoteFrameWsDiagnostics.lastFrameDeviceId;
|
|
2360
2411
|
if (!deviceId) {
|
|
2412
|
+
remoteFrameWsDiagnostics.framesInvalid += 1;
|
|
2361
2413
|
return;
|
|
2362
2414
|
}
|
|
2363
2415
|
|
|
2364
2416
|
const packet = buildRemoteFrameBinaryPacket(frameEvent);
|
|
2365
2417
|
if (!packet) {
|
|
2418
|
+
remoteFrameWsDiagnostics.framesInvalid += 1;
|
|
2366
2419
|
return;
|
|
2367
2420
|
}
|
|
2368
2421
|
|
|
2422
|
+
let matched = 0;
|
|
2423
|
+
let sent = 0;
|
|
2424
|
+
let dropped = 0;
|
|
2369
2425
|
for (const client of remoteFrameClients) {
|
|
2370
2426
|
if (client.readyState !== 1) {
|
|
2371
2427
|
continue;
|
|
@@ -2376,23 +2432,85 @@ function broadcastRemoteBinaryFrame(frameEvent) {
|
|
|
2376
2432
|
continue;
|
|
2377
2433
|
}
|
|
2378
2434
|
|
|
2435
|
+
matched += 1;
|
|
2379
2436
|
if (client.bufferedAmount > 8 * 1024 * 1024) {
|
|
2380
2437
|
client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
|
|
2438
|
+
client.remoteFrameLastDropAt = new Date().toISOString();
|
|
2439
|
+
remoteFrameWsDiagnostics.framesDropped += 1;
|
|
2440
|
+
dropped += 1;
|
|
2381
2441
|
continue;
|
|
2382
2442
|
}
|
|
2383
2443
|
|
|
2384
2444
|
try {
|
|
2385
2445
|
client.send(packet, { binary: true });
|
|
2386
2446
|
client.remoteFrameSentCount = (client.remoteFrameSentCount || 0) + 1;
|
|
2447
|
+
client.remoteFrameLastSentAt = new Date().toISOString();
|
|
2448
|
+
client.remoteFrameLastDeviceId = deviceId;
|
|
2449
|
+
client.remoteFrameLastSeq = remoteFrameWsDiagnostics.lastFrameSeq;
|
|
2450
|
+
remoteFrameWsDiagnostics.framesSent += 1;
|
|
2451
|
+
sent += 1;
|
|
2387
2452
|
} catch {
|
|
2388
2453
|
client.remoteFrameDroppedCount = (client.remoteFrameDroppedCount || 0) + 1;
|
|
2454
|
+
client.remoteFrameLastDropAt = new Date().toISOString();
|
|
2455
|
+
remoteFrameWsDiagnostics.framesDropped += 1;
|
|
2456
|
+
dropped += 1;
|
|
2389
2457
|
}
|
|
2390
2458
|
}
|
|
2459
|
+
|
|
2460
|
+
remoteFrameWsDiagnostics.lastFrameMatchedClients = matched;
|
|
2461
|
+
remoteFrameWsDiagnostics.lastFrameSentClients = sent;
|
|
2462
|
+
remoteFrameWsDiagnostics.lastFrameDroppedClients = dropped;
|
|
2463
|
+
if (matched === 0) {
|
|
2464
|
+
remoteFrameWsDiagnostics.framesNoSubscribers += 1;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function serializeRemoteFrameWsDiagnostics() {
|
|
2469
|
+
const clients = [];
|
|
2470
|
+
for (const client of remoteFrameClients) {
|
|
2471
|
+
const deviceIds = client.remoteFrameDeviceIds instanceof Set
|
|
2472
|
+
? Array.from(client.remoteFrameDeviceIds)
|
|
2473
|
+
: [];
|
|
2474
|
+
clients.push({
|
|
2475
|
+
id: client.remoteFrameClientId || '',
|
|
2476
|
+
readyState: client.readyState,
|
|
2477
|
+
connectedAt: client.remoteFrameConnectedAt || '',
|
|
2478
|
+
subscribedAt: client.remoteFrameSubscriptionUpdatedAt || '',
|
|
2479
|
+
deviceCount: deviceIds.length,
|
|
2480
|
+
deviceIds: deviceIds.slice(0, 24),
|
|
2481
|
+
autoStartLive: client.remoteFrameAutoStartLive === true,
|
|
2482
|
+
autoStartOptions: client.remoteFrameAutoStartOptions || null,
|
|
2483
|
+
sent: Number(client.remoteFrameSentCount || 0),
|
|
2484
|
+
dropped: Number(client.remoteFrameDroppedCount || 0),
|
|
2485
|
+
bufferedAmount: Number(client.bufferedAmount || 0),
|
|
2486
|
+
lastSentAt: client.remoteFrameLastSentAt || '',
|
|
2487
|
+
lastDropAt: client.remoteFrameLastDropAt || '',
|
|
2488
|
+
lastDeviceId: client.remoteFrameLastDeviceId || '',
|
|
2489
|
+
lastFrameSeq: Number(client.remoteFrameLastSeq || 0) || 0,
|
|
2490
|
+
lastAutoStartAt: client.remoteFrameLastAutoStartAt || '',
|
|
2491
|
+
lastAutoStartStarted: Number(client.remoteFrameLastAutoStartStarted || 0),
|
|
2492
|
+
lastAutoStartSkipped: Number(client.remoteFrameLastAutoStartSkipped || 0)
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
return {
|
|
2497
|
+
protocol: 'mindexec.remote.frames.binary.v1',
|
|
2498
|
+
path: '/api/remote/frames/ws',
|
|
2499
|
+
clientCount: remoteFrameClients.size,
|
|
2500
|
+
...remoteFrameWsDiagnostics,
|
|
2501
|
+
clients
|
|
2502
|
+
};
|
|
2391
2503
|
}
|
|
2392
2504
|
|
|
2393
2505
|
remoteFrameWss.on('connection', (ws, req) => {
|
|
2394
2506
|
remoteFrameClients.add(ws);
|
|
2395
2507
|
ws.binaryType = 'arraybuffer';
|
|
2508
|
+
ws.remoteFrameClientId = `rfws-${++remoteFrameClientSeq}`;
|
|
2509
|
+
ws.remoteFrameConnectedAt = new Date().toISOString();
|
|
2510
|
+
ws.remoteFrameSentCount = 0;
|
|
2511
|
+
ws.remoteFrameDroppedCount = 0;
|
|
2512
|
+
remoteFrameWsDiagnostics.opened += 1;
|
|
2513
|
+
remoteFrameWsDiagnostics.lastOpenAt = ws.remoteFrameConnectedAt;
|
|
2396
2514
|
try {
|
|
2397
2515
|
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
2398
2516
|
updateRemoteFrameClientSubscription(ws, {
|
|
@@ -2416,12 +2534,21 @@ remoteFrameWss.on('connection', (ws, req) => {
|
|
|
2416
2534
|
});
|
|
2417
2535
|
}
|
|
2418
2536
|
});
|
|
2419
|
-
ws.on('close', () =>
|
|
2420
|
-
|
|
2537
|
+
ws.on('close', () => {
|
|
2538
|
+
remoteFrameClients.delete(ws);
|
|
2539
|
+
remoteFrameWsDiagnostics.closed += 1;
|
|
2540
|
+
remoteFrameWsDiagnostics.lastCloseAt = new Date().toISOString();
|
|
2541
|
+
});
|
|
2542
|
+
ws.on('error', () => {
|
|
2543
|
+
remoteFrameClients.delete(ws);
|
|
2544
|
+
remoteFrameWsDiagnostics.errors += 1;
|
|
2545
|
+
remoteFrameWsDiagnostics.lastErrorAt = new Date().toISOString();
|
|
2546
|
+
});
|
|
2421
2547
|
sendRemoteFrameClientJson(ws, {
|
|
2422
2548
|
type: 'RemoteFrameSocketReady',
|
|
2423
2549
|
timestamp: new Date().toISOString(),
|
|
2424
|
-
protocol: 'mindexec.remote.frames.binary.v1'
|
|
2550
|
+
protocol: 'mindexec.remote.frames.binary.v1',
|
|
2551
|
+
clientId: ws.remoteFrameClientId
|
|
2425
2552
|
});
|
|
2426
2553
|
});
|
|
2427
2554
|
|
|
@@ -3014,6 +3141,11 @@ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(proc
|
|
|
3014
3141
|
const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
|
|
3015
3142
|
const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
|
|
3016
3143
|
const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
|
|
3144
|
+
const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
|
|
3145
|
+
&& !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
|
|
3146
|
+
const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
|
|
3147
|
+
const REMOTE_REGISTRY_REALTIME_RECONNECT_MS = Math.max(1000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS || 2500) || 2500);
|
|
3148
|
+
const REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS = Math.max(100, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS || 250) || 250);
|
|
3017
3149
|
let remoteAgentState = createRemoteAgentIdleState();
|
|
3018
3150
|
let remoteAgentSyncReportState = null;
|
|
3019
3151
|
let remoteAgentSyncReportLogKey = '';
|
|
@@ -3026,6 +3158,15 @@ let remoteRegistryFollowerTimer = null;
|
|
|
3026
3158
|
let remoteRegistryFollowerInFlight = false;
|
|
3027
3159
|
let remoteRegistryFollowerStarted = false;
|
|
3028
3160
|
let remoteRegistryFollowerConsecutiveMissingSession = 0;
|
|
3161
|
+
let remoteRegistryRealtimeSocket = null;
|
|
3162
|
+
let remoteRegistryRealtimeSessionKey = '';
|
|
3163
|
+
let remoteRegistryRealtimeTopic = '';
|
|
3164
|
+
let remoteRegistryRealtimeJoinRef = '';
|
|
3165
|
+
let remoteRegistryRealtimeRef = 0;
|
|
3166
|
+
let remoteRegistryRealtimeHeartbeatTimer = null;
|
|
3167
|
+
let remoteRegistryRealtimeReconnectTimer = null;
|
|
3168
|
+
let remoteRegistryRealtimeReconnectContext = null;
|
|
3169
|
+
let remoteRegistryRealtimeLastWakeAt = 0;
|
|
3029
3170
|
let remoteRegistryFollowerState = {
|
|
3030
3171
|
enabled: REMOTE_REGISTRY_FOLLOWER_ENABLED,
|
|
3031
3172
|
status: REMOTE_REGISTRY_FOLLOWER_ENABLED ? 'idle' : 'disabled',
|
|
@@ -3039,6 +3180,26 @@ let remoteRegistryFollowerState = {
|
|
|
3039
3180
|
targetNodeId: '',
|
|
3040
3181
|
authenticated: false
|
|
3041
3182
|
};
|
|
3183
|
+
let remoteRegistryRealtimeState = {
|
|
3184
|
+
enabled: REMOTE_REGISTRY_REALTIME_ENABLED,
|
|
3185
|
+
status: REMOTE_REGISTRY_REALTIME_ENABLED ? 'idle' : 'disabled',
|
|
3186
|
+
reason: '',
|
|
3187
|
+
connected: false,
|
|
3188
|
+
subscribed: false,
|
|
3189
|
+
userId: '',
|
|
3190
|
+
topic: '',
|
|
3191
|
+
lastOpenAt: '',
|
|
3192
|
+
lastJoinAt: '',
|
|
3193
|
+
lastMessageAt: '',
|
|
3194
|
+
lastChangeAt: '',
|
|
3195
|
+
lastCloseAt: '',
|
|
3196
|
+
lastErrorAt: '',
|
|
3197
|
+
lastError: '',
|
|
3198
|
+
reconnects: 0,
|
|
3199
|
+
messages: 0,
|
|
3200
|
+
changes: 0,
|
|
3201
|
+
wakeups: 0
|
|
3202
|
+
};
|
|
3042
3203
|
|
|
3043
3204
|
function createRemoteAgentIdleState(overrides = {}) {
|
|
3044
3205
|
return {
|
|
@@ -3490,6 +3651,11 @@ function isRemoteAgentProcessRunning() {
|
|
|
3490
3651
|
&& !proc.killed;
|
|
3491
3652
|
}
|
|
3492
3653
|
|
|
3654
|
+
function isRemoteAgentRegistryOwned() {
|
|
3655
|
+
return isRemoteAgentProcessRunning()
|
|
3656
|
+
&& /registry/i.test(String(remoteAgentState.source || ''));
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3493
3659
|
function isLocalRemoteHostTargetActive() {
|
|
3494
3660
|
const status = remoteHub.getStatus({ includeSecrets: false });
|
|
3495
3661
|
return status?.hostTargetActive === true
|
|
@@ -4290,6 +4456,424 @@ function updateRemoteRegistryFollowerState(patch = {}) {
|
|
|
4290
4456
|
emitBridgeEvent('RemoteRegistryFollowerUpdated', serializeRemoteRegistryFollowerState());
|
|
4291
4457
|
}
|
|
4292
4458
|
|
|
4459
|
+
function serializeRemoteRegistryRealtimeState() {
|
|
4460
|
+
return {
|
|
4461
|
+
...remoteRegistryRealtimeState,
|
|
4462
|
+
heartbeatMs: REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS,
|
|
4463
|
+
reconnectMs: REMOTE_REGISTRY_REALTIME_RECONNECT_MS,
|
|
4464
|
+
debounceMs: REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
function updateRemoteRegistryRealtimeState(patch = {}) {
|
|
4469
|
+
remoteRegistryRealtimeState = {
|
|
4470
|
+
...remoteRegistryRealtimeState,
|
|
4471
|
+
...patch,
|
|
4472
|
+
enabled: REMOTE_REGISTRY_REALTIME_ENABLED
|
|
4473
|
+
};
|
|
4474
|
+
emitBridgeEvent('RemoteRegistryRealtimeUpdated', serializeRemoteRegistryRealtimeState());
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
function clearRemoteRegistryRealtimeTimers() {
|
|
4478
|
+
if (remoteRegistryRealtimeHeartbeatTimer) {
|
|
4479
|
+
clearInterval(remoteRegistryRealtimeHeartbeatTimer);
|
|
4480
|
+
remoteRegistryRealtimeHeartbeatTimer = null;
|
|
4481
|
+
}
|
|
4482
|
+
if (remoteRegistryRealtimeReconnectTimer) {
|
|
4483
|
+
clearTimeout(remoteRegistryRealtimeReconnectTimer);
|
|
4484
|
+
remoteRegistryRealtimeReconnectTimer = null;
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
function closeRemoteRegistryRealtime(reason = 'stopped') {
|
|
4489
|
+
clearRemoteRegistryRealtimeTimers();
|
|
4490
|
+
remoteRegistryRealtimeReconnectContext = null;
|
|
4491
|
+
const socket = remoteRegistryRealtimeSocket;
|
|
4492
|
+
remoteRegistryRealtimeSocket = null;
|
|
4493
|
+
remoteRegistryRealtimeSessionKey = '';
|
|
4494
|
+
remoteRegistryRealtimeTopic = '';
|
|
4495
|
+
remoteRegistryRealtimeJoinRef = '';
|
|
4496
|
+
|
|
4497
|
+
if (socket && socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) {
|
|
4498
|
+
try {
|
|
4499
|
+
socket.close(1000, String(reason || 'stopped').slice(0, 120));
|
|
4500
|
+
} catch {
|
|
4501
|
+
// Best-effort close only.
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
updateRemoteRegistryRealtimeState({
|
|
4506
|
+
status: REMOTE_REGISTRY_REALTIME_ENABLED ? 'stopped' : 'disabled',
|
|
4507
|
+
reason,
|
|
4508
|
+
connected: false,
|
|
4509
|
+
subscribed: false,
|
|
4510
|
+
userId: '',
|
|
4511
|
+
topic: ''
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
function buildRemoteRegistryRealtimeUrl(config) {
|
|
4516
|
+
try {
|
|
4517
|
+
const url = new URL('/realtime/v1/websocket', config.url);
|
|
4518
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
4519
|
+
url.searchParams.set('apikey', config.key);
|
|
4520
|
+
url.searchParams.set('vsn', '1.0.0');
|
|
4521
|
+
return url.toString();
|
|
4522
|
+
} catch {
|
|
4523
|
+
return '';
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
function getRemoteRegistryRealtimeSessionKey(config, session) {
|
|
4528
|
+
const tokenHash = crypto
|
|
4529
|
+
.createHash('sha256')
|
|
4530
|
+
.update(String(session?.accessToken || ''))
|
|
4531
|
+
.digest('hex')
|
|
4532
|
+
.slice(0, 16);
|
|
4533
|
+
return [
|
|
4534
|
+
String(config?.url || '').replace(/\/+$/, ''),
|
|
4535
|
+
String(session?.userId || ''),
|
|
4536
|
+
tokenHash
|
|
4537
|
+
].join('|');
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
function sendRemoteRegistryRealtimeMessage(socket, topic, event, payload = {}, options = {}) {
|
|
4541
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
4542
|
+
return '';
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
const ref = String(++remoteRegistryRealtimeRef);
|
|
4546
|
+
const message = {
|
|
4547
|
+
topic,
|
|
4548
|
+
event,
|
|
4549
|
+
payload,
|
|
4550
|
+
ref
|
|
4551
|
+
};
|
|
4552
|
+
if (options.joinRef) {
|
|
4553
|
+
message.join_ref = options.joinRef;
|
|
4554
|
+
}
|
|
4555
|
+
socket.send(JSON.stringify(message));
|
|
4556
|
+
return ref;
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
function sendRemoteRegistryRealtimeHeartbeat(socket) {
|
|
4560
|
+
sendRemoteRegistryRealtimeMessage(socket, 'phoenix', 'heartbeat', {});
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
function parseRemoteRegistryRealtimeMessage(data) {
|
|
4564
|
+
let raw = data;
|
|
4565
|
+
if (Buffer.isBuffer(raw)) {
|
|
4566
|
+
raw = raw.toString('utf8');
|
|
4567
|
+
}
|
|
4568
|
+
if (ArrayBuffer.isView(raw)) {
|
|
4569
|
+
raw = Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
const parsed = JSON.parse(String(raw || '{}'));
|
|
4573
|
+
if (Array.isArray(parsed)) {
|
|
4574
|
+
return {
|
|
4575
|
+
joinRef: parsed[0] ?? '',
|
|
4576
|
+
ref: parsed[1] ?? '',
|
|
4577
|
+
topic: parsed[2] ?? '',
|
|
4578
|
+
event: parsed[3] ?? '',
|
|
4579
|
+
payload: parsed[4] ?? {}
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
return {
|
|
4584
|
+
joinRef: parsed.join_ref ?? parsed.joinRef ?? '',
|
|
4585
|
+
ref: parsed.ref ?? '',
|
|
4586
|
+
topic: parsed.topic ?? '',
|
|
4587
|
+
event: parsed.event ?? '',
|
|
4588
|
+
payload: parsed.payload ?? {}
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
function isRemoteRegistryRealtimeChange(message) {
|
|
4593
|
+
if (!message || message.event !== 'postgres_changes') {
|
|
4594
|
+
return false;
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
const data = message.payload?.data || message.payload;
|
|
4598
|
+
const table = String(data?.table || data?.record?.table || '').trim();
|
|
4599
|
+
const schema = String(data?.schema || data?.record?.schema || 'public').trim();
|
|
4600
|
+
return table === 'remote_host_targets' && (!schema || schema === 'public');
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
function wakeRemoteRegistryFollowerFromRealtime(reason = 'realtime-change') {
|
|
4604
|
+
const now = Date.now();
|
|
4605
|
+
if (now - remoteRegistryRealtimeLastWakeAt < REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS) {
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
remoteRegistryRealtimeLastWakeAt = now;
|
|
4610
|
+
updateRemoteRegistryRealtimeState({
|
|
4611
|
+
wakeups: remoteRegistryRealtimeState.wakeups + 1,
|
|
4612
|
+
lastChangeAt: new Date().toISOString(),
|
|
4613
|
+
reason
|
|
4614
|
+
});
|
|
4615
|
+
wakeRemoteRegistryFollower(reason).catch(error => {
|
|
4616
|
+
updateRemoteRegistryRealtimeState({
|
|
4617
|
+
status: 'error',
|
|
4618
|
+
reason: 'wake-failed',
|
|
4619
|
+
lastError: error?.message || String(error || ''),
|
|
4620
|
+
lastErrorAt: new Date().toISOString()
|
|
4621
|
+
});
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
function scheduleRemoteRegistryRealtimeReconnect(reason = 'reconnect') {
|
|
4626
|
+
if (!REMOTE_REGISTRY_REALTIME_ENABLED || isShuttingDown || !remoteRegistryRealtimeReconnectContext) {
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
if (remoteRegistryRealtimeReconnectTimer) {
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
updateRemoteRegistryRealtimeState({
|
|
4635
|
+
status: 'reconnecting',
|
|
4636
|
+
reason,
|
|
4637
|
+
connected: false,
|
|
4638
|
+
subscribed: false,
|
|
4639
|
+
reconnects: remoteRegistryRealtimeState.reconnects + 1
|
|
4640
|
+
});
|
|
4641
|
+
remoteRegistryRealtimeReconnectTimer = setTimeout(() => {
|
|
4642
|
+
remoteRegistryRealtimeReconnectTimer = null;
|
|
4643
|
+
const context = remoteRegistryRealtimeReconnectContext;
|
|
4644
|
+
if (!context || isShuttingDown) {
|
|
4645
|
+
return;
|
|
4646
|
+
}
|
|
4647
|
+
ensureRemoteRegistryRealtimeSubscription(context.config, context.session, 'realtime-reconnect')
|
|
4648
|
+
.catch(error => {
|
|
4649
|
+
updateRemoteRegistryRealtimeState({
|
|
4650
|
+
status: 'error',
|
|
4651
|
+
reason: 'reconnect-failed',
|
|
4652
|
+
lastError: error?.message || String(error || ''),
|
|
4653
|
+
lastErrorAt: new Date().toISOString()
|
|
4654
|
+
});
|
|
4655
|
+
scheduleRemoteRegistryRealtimeReconnect('reconnect-failed');
|
|
4656
|
+
});
|
|
4657
|
+
}, REMOTE_REGISTRY_REALTIME_RECONNECT_MS);
|
|
4658
|
+
remoteRegistryRealtimeReconnectTimer?.unref?.();
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
async function ensureRemoteRegistryRealtimeSubscription(config, session, trigger = 'sync') {
|
|
4662
|
+
if (!REMOTE_REGISTRY_REALTIME_ENABLED) {
|
|
4663
|
+
closeRemoteRegistryRealtime('disabled');
|
|
4664
|
+
return serializeRemoteRegistryRealtimeState();
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
if (!config?.url || !config?.key || !session?.accessToken || !session?.userId) {
|
|
4668
|
+
closeRemoteRegistryRealtime('not-authenticated');
|
|
4669
|
+
return serializeRemoteRegistryRealtimeState();
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
const sessionKey = getRemoteRegistryRealtimeSessionKey(config, session);
|
|
4673
|
+
if (remoteRegistryRealtimeSocket
|
|
4674
|
+
&& remoteRegistryRealtimeSessionKey === sessionKey
|
|
4675
|
+
&& (remoteRegistryRealtimeSocket.readyState === WebSocket.OPEN
|
|
4676
|
+
|| remoteRegistryRealtimeSocket.readyState === WebSocket.CONNECTING)) {
|
|
4677
|
+
return serializeRemoteRegistryRealtimeState();
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
closeRemoteRegistryRealtime('replaced');
|
|
4681
|
+
const wsUrl = buildRemoteRegistryRealtimeUrl(config);
|
|
4682
|
+
if (!wsUrl) {
|
|
4683
|
+
updateRemoteRegistryRealtimeState({
|
|
4684
|
+
status: 'error',
|
|
4685
|
+
reason: 'invalid-realtime-url',
|
|
4686
|
+
lastError: 'invalid-realtime-url',
|
|
4687
|
+
lastErrorAt: new Date().toISOString()
|
|
4688
|
+
});
|
|
4689
|
+
return serializeRemoteRegistryRealtimeState();
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
const topic = `realtime:remote-host-targets-${String(session.userId).replace(/[^a-zA-Z0-9]/g, '') || 'user'}`;
|
|
4693
|
+
const socket = new WebSocket(wsUrl, {
|
|
4694
|
+
headers: {
|
|
4695
|
+
apikey: config.key,
|
|
4696
|
+
Authorization: `Bearer ${session.accessToken}`
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4699
|
+
|
|
4700
|
+
remoteRegistryRealtimeSocket = socket;
|
|
4701
|
+
remoteRegistryRealtimeSessionKey = sessionKey;
|
|
4702
|
+
remoteRegistryRealtimeTopic = topic;
|
|
4703
|
+
remoteRegistryRealtimeReconnectContext = {
|
|
4704
|
+
config: { url: config.url, key: config.key },
|
|
4705
|
+
session: {
|
|
4706
|
+
accessToken: session.accessToken,
|
|
4707
|
+
userId: session.userId,
|
|
4708
|
+
expiresAtMs: session.expiresAtMs
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
|
|
4712
|
+
updateRemoteRegistryRealtimeState({
|
|
4713
|
+
status: 'connecting',
|
|
4714
|
+
reason: trigger,
|
|
4715
|
+
connected: false,
|
|
4716
|
+
subscribed: false,
|
|
4717
|
+
userId: session.userId,
|
|
4718
|
+
topic,
|
|
4719
|
+
lastError: ''
|
|
4720
|
+
});
|
|
4721
|
+
|
|
4722
|
+
socket.on('open', () => {
|
|
4723
|
+
if (remoteRegistryRealtimeSocket !== socket) {
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
const now = new Date().toISOString();
|
|
4728
|
+
remoteRegistryRealtimeJoinRef = String(++remoteRegistryRealtimeRef);
|
|
4729
|
+
updateRemoteRegistryRealtimeState({
|
|
4730
|
+
status: 'joining',
|
|
4731
|
+
reason: 'socket-open',
|
|
4732
|
+
connected: true,
|
|
4733
|
+
subscribed: false,
|
|
4734
|
+
lastOpenAt: now,
|
|
4735
|
+
lastJoinAt: now
|
|
4736
|
+
});
|
|
4737
|
+
socket.send(JSON.stringify({
|
|
4738
|
+
topic,
|
|
4739
|
+
event: 'phx_join',
|
|
4740
|
+
payload: {
|
|
4741
|
+
config: {
|
|
4742
|
+
broadcast: { ack: false, self: false },
|
|
4743
|
+
presence: { enabled: false },
|
|
4744
|
+
postgres_changes: [
|
|
4745
|
+
{
|
|
4746
|
+
event: '*',
|
|
4747
|
+
schema: 'public',
|
|
4748
|
+
table: 'remote_host_targets',
|
|
4749
|
+
filter: `user_id=eq.${session.userId}`
|
|
4750
|
+
}
|
|
4751
|
+
],
|
|
4752
|
+
private: false
|
|
4753
|
+
},
|
|
4754
|
+
access_token: session.accessToken
|
|
4755
|
+
},
|
|
4756
|
+
ref: remoteRegistryRealtimeJoinRef,
|
|
4757
|
+
join_ref: remoteRegistryRealtimeJoinRef
|
|
4758
|
+
}));
|
|
4759
|
+
|
|
4760
|
+
if (remoteRegistryRealtimeHeartbeatTimer) {
|
|
4761
|
+
clearInterval(remoteRegistryRealtimeHeartbeatTimer);
|
|
4762
|
+
}
|
|
4763
|
+
remoteRegistryRealtimeHeartbeatTimer = setInterval(
|
|
4764
|
+
() => sendRemoteRegistryRealtimeHeartbeat(socket),
|
|
4765
|
+
REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS);
|
|
4766
|
+
remoteRegistryRealtimeHeartbeatTimer?.unref?.();
|
|
4767
|
+
});
|
|
4768
|
+
|
|
4769
|
+
socket.on('message', data => {
|
|
4770
|
+
if (remoteRegistryRealtimeSocket !== socket) {
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
let message = null;
|
|
4775
|
+
try {
|
|
4776
|
+
message = parseRemoteRegistryRealtimeMessage(data);
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
updateRemoteRegistryRealtimeState({
|
|
4779
|
+
status: 'error',
|
|
4780
|
+
reason: 'message-parse-failed',
|
|
4781
|
+
lastError: error?.message || String(error || ''),
|
|
4782
|
+
lastErrorAt: new Date().toISOString()
|
|
4783
|
+
});
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
updateRemoteRegistryRealtimeState({
|
|
4788
|
+
messages: remoteRegistryRealtimeState.messages + 1,
|
|
4789
|
+
lastMessageAt: new Date().toISOString()
|
|
4790
|
+
});
|
|
4791
|
+
|
|
4792
|
+
if (message.event === 'phx_reply' && message.ref === remoteRegistryRealtimeJoinRef) {
|
|
4793
|
+
const ok = String(message.payload?.status || '').toLowerCase() === 'ok';
|
|
4794
|
+
updateRemoteRegistryRealtimeState({
|
|
4795
|
+
status: ok ? 'subscribed' : 'error',
|
|
4796
|
+
reason: ok ? 'joined' : 'join-failed',
|
|
4797
|
+
subscribed: ok,
|
|
4798
|
+
lastError: ok ? '' : JSON.stringify(message.payload || {}).slice(0, 240),
|
|
4799
|
+
lastErrorAt: ok ? remoteRegistryRealtimeState.lastErrorAt : new Date().toISOString()
|
|
4800
|
+
});
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
if (message.event === 'system') {
|
|
4805
|
+
const status = String(message.payload?.status || '').toLowerCase();
|
|
4806
|
+
if (status === 'ok') {
|
|
4807
|
+
updateRemoteRegistryRealtimeState({
|
|
4808
|
+
status: 'subscribed',
|
|
4809
|
+
reason: 'system-ok',
|
|
4810
|
+
subscribed: true
|
|
4811
|
+
});
|
|
4812
|
+
} else if (status === 'error' || status === 'timeout') {
|
|
4813
|
+
updateRemoteRegistryRealtimeState({
|
|
4814
|
+
status: 'error',
|
|
4815
|
+
reason: 'system-error',
|
|
4816
|
+
subscribed: false,
|
|
4817
|
+
lastError: String(message.payload?.message || status).slice(0, 240),
|
|
4818
|
+
lastErrorAt: new Date().toISOString()
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4824
|
+
if (message.event === 'phx_error' || message.event === 'phx_close') {
|
|
4825
|
+
updateRemoteRegistryRealtimeState({
|
|
4826
|
+
status: 'error',
|
|
4827
|
+
reason: message.event,
|
|
4828
|
+
subscribed: false,
|
|
4829
|
+
lastError: message.event,
|
|
4830
|
+
lastErrorAt: new Date().toISOString()
|
|
4831
|
+
});
|
|
4832
|
+
scheduleRemoteRegistryRealtimeReconnect(message.event);
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
|
|
4836
|
+
if (isRemoteRegistryRealtimeChange(message)) {
|
|
4837
|
+
updateRemoteRegistryRealtimeState({
|
|
4838
|
+
changes: remoteRegistryRealtimeState.changes + 1
|
|
4839
|
+
});
|
|
4840
|
+
wakeRemoteRegistryFollowerFromRealtime('realtime-change');
|
|
4841
|
+
}
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4844
|
+
socket.on('close', (code, reasonBuffer) => {
|
|
4845
|
+
if (remoteRegistryRealtimeSocket !== socket) {
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
clearRemoteRegistryRealtimeTimers();
|
|
4850
|
+
remoteRegistryRealtimeSocket = null;
|
|
4851
|
+
updateRemoteRegistryRealtimeState({
|
|
4852
|
+
status: 'closed',
|
|
4853
|
+
reason: reasonBuffer?.toString?.() || `close:${code}`,
|
|
4854
|
+
connected: false,
|
|
4855
|
+
subscribed: false,
|
|
4856
|
+
lastCloseAt: new Date().toISOString()
|
|
4857
|
+
});
|
|
4858
|
+
scheduleRemoteRegistryRealtimeReconnect(`close:${code}`);
|
|
4859
|
+
});
|
|
4860
|
+
|
|
4861
|
+
socket.on('error', error => {
|
|
4862
|
+
if (remoteRegistryRealtimeSocket !== socket) {
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
updateRemoteRegistryRealtimeState({
|
|
4867
|
+
status: 'error',
|
|
4868
|
+
reason: 'socket-error',
|
|
4869
|
+
lastError: error?.message || String(error || ''),
|
|
4870
|
+
lastErrorAt: new Date().toISOString()
|
|
4871
|
+
});
|
|
4872
|
+
});
|
|
4873
|
+
|
|
4874
|
+
return serializeRemoteRegistryRealtimeState();
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4293
4877
|
function readSupabaseRuntimeConfig() {
|
|
4294
4878
|
const envUrl = String(process.env.SUPABASE_URL || process.env.MINDEXEC_SUPABASE_URL || '').trim();
|
|
4295
4879
|
const envKey = String(
|
|
@@ -4516,6 +5100,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
|
|
|
4516
5100
|
try {
|
|
4517
5101
|
const config = readSupabaseRuntimeConfig();
|
|
4518
5102
|
if (!config.url || !config.key) {
|
|
5103
|
+
closeRemoteRegistryRealtime('supabase-config-missing');
|
|
4519
5104
|
updateRemoteRegistryFollowerState({
|
|
4520
5105
|
status: 'skipped',
|
|
4521
5106
|
reason: 'supabase-config-missing',
|
|
@@ -4537,9 +5122,10 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
|
|
|
4537
5122
|
const sessionPayload = await readStableAuthSessionPayload();
|
|
4538
5123
|
const session = sessionPayload.found ? parseSupabaseSessionForRegistry(sessionPayload.content) : null;
|
|
4539
5124
|
if (!session) {
|
|
5125
|
+
closeRemoteRegistryRealtime('registry-not-authenticated');
|
|
4540
5126
|
remoteRegistryFollowerConsecutiveMissingSession += 1;
|
|
4541
5127
|
if (remoteRegistryFollowerConsecutiveMissingSession >= REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT
|
|
4542
|
-
&&
|
|
5128
|
+
&& isRemoteAgentRegistryOwned()) {
|
|
4543
5129
|
await stopRemoteAgentConnection('registry-not-authenticated');
|
|
4544
5130
|
}
|
|
4545
5131
|
|
|
@@ -4567,6 +5153,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
|
|
|
4567
5153
|
|
|
4568
5154
|
remoteRegistryFollowerConsecutiveMissingSession = 0;
|
|
4569
5155
|
if (session.expiresAtMs && session.expiresAtMs <= Date.now()) {
|
|
5156
|
+
closeRemoteRegistryRealtime('session-expired');
|
|
4570
5157
|
updateRemoteRegistryFollowerState({
|
|
4571
5158
|
status: 'skipped',
|
|
4572
5159
|
reason: 'session-expired',
|
|
@@ -4586,6 +5173,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
|
|
|
4586
5173
|
}
|
|
4587
5174
|
|
|
4588
5175
|
const localHub = remoteHub.getStatus({ includeSecrets: false });
|
|
5176
|
+
await ensureRemoteRegistryRealtimeSubscription(config, session, trigger);
|
|
4589
5177
|
const target = await fetchRemoteRegistryTarget(config, session);
|
|
4590
5178
|
|
|
4591
5179
|
if (localHub?.hostTargetActive === true) {
|
|
@@ -4652,7 +5240,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
|
|
|
4652
5240
|
}
|
|
4653
5241
|
|
|
4654
5242
|
if (!target?.active || isRemoteRegistryTargetExpired(target)) {
|
|
4655
|
-
if (
|
|
5243
|
+
if (isRemoteAgentRegistryOwned()) {
|
|
4656
5244
|
await stopRemoteAgentConnection('registry-inactive');
|
|
4657
5245
|
}
|
|
4658
5246
|
updateRemoteRegistryFollowerState({
|
|
@@ -9286,8 +9874,10 @@ app.get('/api/status', async (req, res) => {
|
|
|
9286
9874
|
bridgeTokenHeader,
|
|
9287
9875
|
bridgeAuthRequired,
|
|
9288
9876
|
remoteHub: remoteHub.getStatus({ includeSecrets: false }),
|
|
9877
|
+
remoteFrameWs: serializeRemoteFrameWsDiagnostics(),
|
|
9289
9878
|
remoteAgent: serializeRemoteAgentState(),
|
|
9290
9879
|
remoteRegistryFollower: serializeRemoteRegistryFollowerState(),
|
|
9880
|
+
remoteRegistryRealtime: serializeRemoteRegistryRealtimeState(),
|
|
9291
9881
|
shellJobsPath: '/api/shell/jobs',
|
|
9292
9882
|
companyCore: {
|
|
9293
9883
|
baseUrl: companyCoreBaseUrl,
|
|
@@ -11118,6 +11708,12 @@ async function shutdownBridge(signal) {
|
|
|
11118
11708
|
// Ignore registry follower shutdown errors
|
|
11119
11709
|
}
|
|
11120
11710
|
|
|
11711
|
+
try {
|
|
11712
|
+
closeRemoteRegistryRealtime('bridge-shutdown');
|
|
11713
|
+
} catch {
|
|
11714
|
+
// Ignore registry realtime shutdown errors
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11121
11717
|
try {
|
|
11122
11718
|
await stopRemoteAgentConnection('bridge-shutdown');
|
|
11123
11719
|
} catch {
|