Skip to content

Commit 53d5a07

Browse files
committed
Implement find() wait and RECONNECT structure event
find() now waits for the target app to appear instead of failing immediately. subscribeToStructure fires RECONNECT (2) when an app reappears after being removed, distinguishing restarts from first-time appearances. Changes: - find() waits indefinitely by default; { timeout: N } limits the wait; { timeout: 0 } preserves the old immediate-fail behavior - find() works without prior root() call (triggers connection internally) - close() is terminal: sets isClosed flag, rejects pending find() waiters, prevents reconnection after close - RECONNECT constant (2) added alongside ADD (1) and REMOVE (0) - Per-app lifecycle via announceApp/unannounceApp: each app is tracked independently, fixing the bug where one sibling disconnecting lost all sibling connections - Subscription keepalive via inactivityResendInterval field (client requests server resend values/events after 120s of no changes, matching C++ client ServicesProtocol::InactivityResendIntervalSec) - Value dedup via lastServerTimestamp and event dedup via recentEventIds to handle server replay after reconnection - Exponential backoff with jitter for reconnection (1s to 30s max) - Stall detection: force reconnect after 150s of server silence with active subscriptions (must exceed 120s keepalive interval) - Direct mode sibling discovery uses server-pushed eStructureChangeResponse instead of client-side polling - CDP_FORCE_DIRECT_MODE=1 env var to force direct mode on proxy-capable servers (for testing) - Cache key fix: use join('.') instead of toString() for correct invalidateApp prefix matching on app structure changes - Remove unused serviceId storage on proxy connections - Fix instanceId serialization: pass through as-is instead of defaulting falsy values to 0 - appAddress() helper for DRY server address construction - README updated with Structure Events section and find() options - Version bump to 3.0.0 (breaking: find() default changed from immediate fail to indefinite wait) CDP-6069
1 parent ea72cfd commit 53d5a07

4 files changed

Lines changed: 495 additions & 61 deletions

File tree

README.rst

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Example
3838
root.subscribeToStructure((name, change) => {
3939
if (change === studio.api.structure.ADD)
4040
subscribeToApp(name);
41+
if (change === studio.api.structure.RECONNECT)
42+
console.log(`${name} restarted, subscriptions intact`);
4143
});
4244
}).catch(err => console.error("Connection failed:", err));
4345
@@ -61,6 +63,38 @@ Benefits
6163
- Simplified firewall configuration - only one port needs to be opened
6264
- SSH port forwarding - forward a single port to access entire CDP system
6365

66+
Structure Events
67+
----------------
68+
69+
The ``subscribeToStructure`` callback receives three event types:
70+
71+
- ``studio.api.structure.ADD`` (1) — An application appeared for the first time
72+
- ``studio.api.structure.REMOVE`` (0) — An application went offline
73+
- ``studio.api.structure.RECONNECT`` (2) — An application restarted (was seen before, went offline, came back)
74+
75+
The RECONNECT event distinguishes first-time discovery from application restarts. When an app
76+
restarts, the client automatically restores value and event subscriptions, so user code does
77+
not need to re-subscribe. RECONNECT is informational — use it for logging or UI updates.
78+
79+
.. code:: javascript
80+
81+
client.root().then(root => {
82+
root.subscribeToStructure((appName, change) => {
83+
if (change === studio.api.structure.ADD) {
84+
console.log(`New app online: ${appName}`);
85+
client.find(appName + '.CPULoad').then(node => {
86+
node.subscribeToValues(v => console.log(`[${appName}] CPULoad: ${v}`));
87+
}).catch(err => console.error(`Failed to find ${appName}.CPULoad:`, err));
88+
}
89+
if (change === studio.api.structure.REMOVE) {
90+
console.log(`App offline: ${appName}`);
91+
}
92+
if (change === studio.api.structure.RECONNECT) {
93+
console.log(`App restarted: ${appName}, subscriptions intact`);
94+
}
95+
});
96+
}).catch(err => console.error("Connection failed:", err));
97+
6498
API
6599
---
66100

@@ -325,31 +359,45 @@ client.root()
325359
// use the system INode object to access connected structure.
326360
});
327361
328-
client.find(path)
329-
^^^^^^^^^^^^^^^^^
362+
client.find(path, options)
363+
^^^^^^^^^^^^^^^^^^^^^^^^^
330364

331365
- Arguments
332366

333-
path - Path of the object to look for.
367+
path - Dot-separated path to target node (e.g. ``'App2.CPULoad'``).
368+
369+
options - Optional object. ``{ timeout: milliseconds }`` to limit wait time.
370+
Use ``{ timeout: 0 }`` to fail immediately if the app is not available.
334371

335372
- Returns
336373

337374
Promise containing requested INode object when fulfilled.
338375

339-
- Restriction
376+
- Behavior
340377

341-
The requested node must reside in the application client was connected to.
378+
Waits indefinitely for the target application to appear. If the application is already
379+
available, resolves immediately. No prior ``root()`` call is needed — ``find()`` triggers
380+
the connection internally.
342381

343-
- Usage
382+
- Examples
344383

345-
The provided path must contain dot separated path to target node. **Root node is not considered part of the path.**
384+
.. code:: javascript
346385
347-
- Example
386+
// Waits indefinitely for App2 to appear
387+
client.find("App2.CPULoad").then(function (load) {
388+
load.subscribeToValues(function (value) {
389+
console.log("CPULoad:", value);
390+
});
391+
});
348392
349-
.. code:: javascript
393+
// Wait up to 5 seconds
394+
client.find("App2.CPULoad", { timeout: 5000 }).catch(function (err) {
395+
console.log(err.message); // "App2 not found within 5000ms"
396+
});
350397
351-
client.find("MyApp.CPULoad").then(function (load) {
352-
// use the load object referring to CPULoad in MyApp
398+
// Fail immediately if not available (old behavior)
399+
client.find("App2.CPULoad", { timeout: 0 }).catch(function (err) {
400+
console.log("Not available right now");
353401
});
354402
355403
client.close()
@@ -604,8 +652,9 @@ node.subscribeToStructure(structureConsumer)
604652

605653
- Usage
606654

607-
Subscribe to structure changes on this node. Each time child is added or removed from current node
608-
structureConsumer function is called with the name of the node and change argument where ADD == 1 and REMOVE == 0.
655+
Subscribe to structure changes on this node. Each time a child is added or removed,
656+
structureConsumer is called with the child name and change (ADD == 1, REMOVE == 0).
657+
On the root node, RECONNECT (2) fires when a previously-seen application restarts.
609658

610659
node.unsubscribeFromStructure(structureConsumer)
611660
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)