Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ Benefits
Structure Events
----------------

On the root node, ``subscribeToStructure`` tracks application lifecycle with three event types:
On the root node, ``subscribeToStructure`` tracks application lifecycle:

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

On other nodes, ADD and REMOVE fire when children are added or removed at runtime.
RECONNECT only fires at the root level.
On other nodes, ``subscribeToStructure`` fires when children are added or removed
(e.g. components or operators added to a running application):

- ``studio.api.structure.ADD`` (1) — A child node was added
- ``studio.api.structure.REMOVE`` (0) — A child node was removed
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you called subscribeToStructure on a deeped node (not root node) then shouldn't those nodes also report DISCONNECT and RECONNECT? I mean add/remove should refer to when something in CDP node tree actually changes, e.g. a CDPOperator is added/removed from a component.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REMOVE now only fires for tree node changes, not app lifecycle. DISCONNECT and RECONNECT currently only fire when subscribed on the system node, not on individual app nodes. I could do a follow up to add it for deeper nodes.


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

- Usage

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

node.unsubscribeFromStructure(structureConsumer)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
31 changes: 16 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,8 @@ studio.internal = (function(proto) {
obj.structure = {
REMOVE: 0,
ADD: 1,
RECONNECT: 2
RECONNECT: 2,
DISCONNECT: 3
};

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

function isApplicationNode(node) {
Expand Down Expand Up @@ -907,7 +908,7 @@ studio.internal = (function(proto) {
function unannounceApp(appName) {
if (!announcedApps.has(appName)) return;
announcedApps.delete(appName);
notifyStructure(appName, obj.structure.REMOVE);
notifyStructure(appName, obj.structure.DISCONNECT);
}

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

// Track which app this connection owns (needed for DISCONNECT on connection loss)
system.forEachChild(function(app) {
if (isApplicationNode(app)) {
connectionLocalApps.set(connection, app.name());
}
});

if (isProxyMode) {
// Proxy mode: only handle REMOVE here. ADD/RECONNECT is deferred to
// Proxy mode: handle server-side child REMOVE here. ADD/RECONNECT is deferred to
// notifyApplications() after the proxy tunnel connects (via
// tryConnectPendingSiblings → connectViaProxy), ensuring the sibling
// is actually reachable before announcing it.
Expand All @@ -934,14 +942,6 @@ studio.internal = (function(proto) {
unannounceApp(appName);
}
});
} else {
// Direct mode: each connection owns its local app.
// Connection lifecycle directly maps to app lifecycle.
system.forEachChild(function(app) {
if (isApplicationNode(app)) {
connectionLocalApps.set(connection, app.name());
}
});
}

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

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

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

var system = new internal.SystemNode(studioURL, notificationListener, function(appName) {
// Called on app structure changes (ADD, REMOVE, or RECONNECT)
// Called on app structure changes (ADD, DISCONNECT, or RECONNECT)
findNodeCacheInvalidator(appName);
});

Expand Down
17 changes: 9 additions & 8 deletions test/find-and-structure-events.test.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
/**
* find() wait semantics and RECONNECT structure event tests
* find() wait semantics and structure event constant tests
*
* Unit tests for the new public API surfaces. Tests that require
* Unit tests for the public API surfaces. Tests that require
* a live connection (find() timeout behavior, subscribeToStructure
* RECONNECT events) are covered by the component tests in the
* lifecycle events) are covered by the component tests in the
* parent cdp monorepo.
*/

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

describe('RECONNECT structure constant', () => {
test('studio.api.structure has ADD, REMOVE, and RECONNECT with correct values', () => {
describe('structure event constants', () => {
test('studio.api.structure has ADD, REMOVE, RECONNECT, and DISCONNECT with correct values', () => {
expect(studio.api.structure.ADD).toBe(1);
expect(studio.api.structure.REMOVE).toBe(0);
expect(studio.api.structure.RECONNECT).toBe(2);
expect(studio.api.structure.DISCONNECT).toBe(3);
});

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

Expand Down
Loading