Skip to content

Commit c813745

Browse files
authored
Add DISCONNECT structure event for app connection loss (#26)
Adds DISCONNECT (3) to studio.api.structure to distinguish app going offline from child node removal (REMOVE): - DISCONNECT (3) fires at root level when an app goes offline - REMOVE (0) fires at node level when a child is removed - RECONNECT (2) fires at root level when a disconnected app returns Also populates connectionLocalApps for the primary connection in proxy mode. Previously it was only populated in direct mode, so the primary app never fired DISCONNECT or RECONNECT when its connection dropped in proxy mode. CDP-6069
1 parent 2b67bf0 commit c813745

3 files changed

Lines changed: 37 additions & 31 deletions

File tree

README.rst

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,17 @@ Benefits
6666
Structure Events
6767
----------------
6868

69-
On the root node, ``subscribeToStructure`` tracks application lifecycle with three event types:
69+
On the root node, ``subscribeToStructure`` tracks application lifecycle:
7070

7171
- ``studio.api.structure.ADD`` (1) — An application appeared for the first time
72-
- ``studio.api.structure.REMOVE`` (0) — An application went offline
72+
- ``studio.api.structure.DISCONNECT`` (3) — An application went offline (may reconnect)
7373
- ``studio.api.structure.RECONNECT`` (2) — An application restarted (was seen before, went offline, came back)
7474

75-
On other nodes, ADD and REMOVE fire when children are added or removed at runtime.
76-
RECONNECT only fires at the root level.
75+
On other nodes, ``subscribeToStructure`` fires when children are added or removed
76+
(e.g. components or operators added to a running application):
77+
78+
- ``studio.api.structure.ADD`` (1) — A child node was added
79+
- ``studio.api.structure.REMOVE`` (0) — A child node was removed
7780

7881
When an app restarts, the client automatically restores value and event subscriptions,
7982
so user code does not need to re-subscribe. RECONNECT is informational — use it for
@@ -89,7 +92,7 @@ logging or UI updates.
8992
node.subscribeToValues(v => console.log(`[${appName}] CPULoad: ${v}`));
9093
}).catch(err => console.error(`Failed to find ${appName}.CPULoad:`, err));
9194
}
92-
if (change === studio.api.structure.REMOVE) {
95+
if (change === studio.api.structure.DISCONNECT) {
9396
console.log(`App offline: ${appName}`);
9497
}
9598
if (change === studio.api.structure.RECONNECT) {
@@ -656,9 +659,10 @@ node.subscribeToStructure(structureConsumer)
656659

657660
- Usage
658661

659-
Subscribe to structure changes on this node. Each time a child is added or removed,
660-
structureConsumer is called with the child name and change (ADD == 1, REMOVE == 0).
661-
On the root node, RECONNECT (2) fires when a previously-seen application restarts.
662+
Subscribe to structure changes on this node.
663+
On the root node: ADD (1) when an app appears, DISCONNECT (3) when it goes offline,
664+
RECONNECT (2) when it restarts. On other nodes: ADD (1) when a child is added,
665+
REMOVE (0) when a child is removed.
662666

663667
node.unsubscribeFromStructure(structureConsumer)
664668
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

index.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,8 @@ studio.internal = (function(proto) {
456456
obj.structure = {
457457
REMOVE: 0,
458458
ADD: 1,
459-
RECONNECT: 2
459+
RECONNECT: 2,
460+
DISCONNECT: 3
460461
};
461462

462463
const STRUCTURE_REQUEST_TIMEOUT_MS = 30000;
@@ -803,7 +804,7 @@ studio.internal = (function(proto) {
803804
var everSeenApps = new Set();
804805
var pendingFindWaiters = []; // for find() waiting on late apps
805806
var pendingFetches = [];
806-
var connectionLocalApps = new Map(); // Maps AppConnection → local app name (direct mode)
807+
var connectionLocalApps = new Map(); // Maps AppConnection → local app name
807808
var this_ = this;
808809

809810
function isApplicationNode(node) {
@@ -907,7 +908,7 @@ studio.internal = (function(proto) {
907908
function unannounceApp(appName) {
908909
if (!announcedApps.has(appName)) return;
909910
announcedApps.delete(appName);
910-
notifyStructure(appName, obj.structure.REMOVE);
911+
notifyStructure(appName, obj.structure.DISCONNECT);
911912
}
912913

913914
function notifyApplications(connection) {
@@ -924,8 +925,15 @@ studio.internal = (function(proto) {
924925
var primaryConn = appConnections[0];
925926
var isProxyMode = primaryConn && primaryConn.supportsProxyProtocol();
926927

928+
// Track which app this connection owns (needed for DISCONNECT on connection loss)
929+
system.forEachChild(function(app) {
930+
if (isApplicationNode(app)) {
931+
connectionLocalApps.set(connection, app.name());
932+
}
933+
});
934+
927935
if (isProxyMode) {
928-
// Proxy mode: only handle REMOVE here. ADD/RECONNECT is deferred to
936+
// Proxy mode: handle server-side child REMOVE here. ADD/RECONNECT is deferred to
929937
// notifyApplications() after the proxy tunnel connects (via
930938
// tryConnectPendingSiblings → connectViaProxy), ensuring the sibling
931939
// is actually reachable before announcing it.
@@ -934,14 +942,6 @@ studio.internal = (function(proto) {
934942
unannounceApp(appName);
935943
}
936944
});
937-
} else {
938-
// Direct mode: each connection owns its local app.
939-
// Connection lifecycle directly maps to app lifecycle.
940-
system.forEachChild(function(app) {
941-
if (isApplicationNode(app)) {
942-
connectionLocalApps.set(connection, app.name());
943-
}
944-
});
945945
}
946946

947947
resolve(system);
@@ -953,7 +953,7 @@ studio.internal = (function(proto) {
953953
var appConnection = new obj.AppConnection(url, notificationListener, autoConnect);
954954
appConnections.push(appConnection);
955955

956-
// Direct mode lifecycle: connection close → REMOVE, reconnect → RECONNECT
956+
// Direct mode lifecycle: connection close → DISCONNECT, reconnect → RECONNECT
957957
appConnection.onDisconnected = function() {
958958
var localApp = connectionLocalApps.get(appConnection);
959959
if (localApp) unannounceApp(localApp);
@@ -2431,7 +2431,8 @@ studio.api = (function(internal) {
24312431
*
24322432
* @callback structureConsumer
24332433
* @param {string} node name
2434-
* @param {number} change - ADD (1), REMOVE (0), or RECONNECT (2) from studio.api.structure
2434+
* @param {number} change - At root level: ADD (1), DISCONNECT (3), or RECONNECT (2).
2435+
* At other nodes: ADD (1) or REMOVE (0). See studio.api.structure.
24352436
*/
24362437

24372438
/**
@@ -2547,7 +2548,7 @@ studio.api = (function(internal) {
25472548
var findNodeCacheInvalidator = null; // Set after findNodeCache is created
25482549

25492550
var system = new internal.SystemNode(studioURL, notificationListener, function(appName) {
2550-
// Called on app structure changes (ADD, REMOVE, or RECONNECT)
2551+
// Called on app structure changes (ADD, DISCONNECT, or RECONNECT)
25512552
findNodeCacheInvalidator(appName);
25522553
});
25532554

test/find-and-structure-events.test.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
/**
2-
* find() wait semantics and RECONNECT structure event tests
2+
* find() wait semantics and structure event constant tests
33
*
4-
* Unit tests for the new public API surfaces. Tests that require
4+
* Unit tests for the public API surfaces. Tests that require
55
* a live connection (find() timeout behavior, subscribeToStructure
6-
* RECONNECT events) are covered by the component tests in the
6+
* lifecycle events) are covered by the component tests in the
77
* parent cdp monorepo.
88
*/
99

1010
global.WebSocket = require('ws');
1111
const studio = require('../index');
1212

13-
describe('RECONNECT structure constant', () => {
14-
test('studio.api.structure has ADD, REMOVE, and RECONNECT with correct values', () => {
13+
describe('structure event constants', () => {
14+
test('studio.api.structure has ADD, REMOVE, RECONNECT, and DISCONNECT with correct values', () => {
1515
expect(studio.api.structure.ADD).toBe(1);
1616
expect(studio.api.structure.REMOVE).toBe(0);
1717
expect(studio.api.structure.RECONNECT).toBe(2);
18+
expect(studio.api.structure.DISCONNECT).toBe(3);
1819
});
1920

20-
test('RECONNECT is distinct from ADD and REMOVE', () => {
21-
const values = [studio.api.structure.ADD, studio.api.structure.REMOVE, studio.api.structure.RECONNECT];
22-
expect(new Set(values).size).toBe(3);
21+
test('all structure constants are distinct', () => {
22+
const values = [studio.api.structure.ADD, studio.api.structure.REMOVE, studio.api.structure.RECONNECT, studio.api.structure.DISCONNECT];
23+
expect(new Set(values).size).toBe(4);
2324
});
2425
});
2526

0 commit comments

Comments
 (0)