Skip to content

Commit 491509e

Browse files
committed
Add proxy protocol support for CDP 5.1+
Enable single WebSocket connection to access multiple CDP applications via ServiceMessage tunneling (compatVersion >= 4). Key features: - ServicesRequest/ServicesNotification for proxy service discovery - ServiceMessage tunneling (eConnect, eConnected, eData, eDisconnect, eError) - Send buffering to prevent race conditions before eConnected - Dynamic sibling discovery via subscribeToStructure - Client-level structure subscriptions for app ADD/REMOVE notifications - 30-second connect timeout with automatic cleanup - Graceful cleanup on primary connection close Also: - Update studioapi.proto.js with service message types - Add README proxy example - Add Jest tests and GitHub Actions CI
1 parent b88174a commit 491509e

17 files changed

Lines changed: 9830 additions & 390 deletions

.github/workflows/test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
node-version: [18, 20, 22]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Use Node.js ${{ matrix.node-version }}
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: ${{ matrix.node-version }}
24+
cache: 'npm'
25+
26+
- name: Install dependencies
27+
run: npm ci
28+
29+
- name: Run tests
30+
run: npm test

README.rst

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,65 @@ Installation
1212

1313
$ npm install cdp-client
1414

15+
Proxy Support (CDP 5.1+)
16+
------------------------
17+
18+
The client supports the Generic Proxy Protocol which allows a single WebSocket connection to access
19+
multiple backend CDP applications through multiplexing.
20+
21+
When connecting to a CDP application configured as a proxy, the client automatically:
22+
23+
- Sends a ServicesRequest to discover available proxy services
24+
- Receives ServicesNotification with available backend applications
25+
- Establishes virtual connections to backend applications with ``type: 'websocketproxy'`` and ``metadata.proxy_type: 'studioapi'``
26+
27+
All discovered applications appear as children of the root system node, enabling transparent access
28+
to their structure and values through the standard API.
29+
30+
Use Cases
31+
~~~~~~~~~
32+
33+
- Simplified firewall configuration - only one port needs to be opened
34+
- SSH port forwarding - forward a single port to access entire CDP system
35+
36+
Example
37+
~~~~~~~
38+
39+
.. code:: javascript
40+
41+
// Connect to a proxy-enabled CDP application (with authentication)
42+
const client = new studio.api.Client("127.0.0.1:7690", {
43+
credentialsRequested: async (request) => {
44+
return { Username: "cdpuser", Password: "cdpuser" };
45+
}
46+
});
47+
48+
// Track subscribed apps to avoid duplicates
49+
const subscribedApps = new Set();
50+
51+
function subscribeToApp(app) {
52+
const appName = app.name();
53+
if (subscribedApps.has(appName)) return;
54+
subscribedApps.add(appName);
55+
56+
// subscribeWithResume automatically restores subscriptions after reconnection
57+
client.subscribeWithResume(appName + '.CPULoad', value => {
58+
console.log(`[${appName}] CPULoad: ${value}`);
59+
});
60+
}
61+
62+
client.root().then(root => {
63+
// Subscribe to apps already visible
64+
root.forEachChild(app => subscribeToApp(app));
65+
66+
// Subscribe to structure changes to catch sibling apps as they're discovered
67+
root.subscribeToStructure((name, change) => {
68+
if (change === 1) { // ADD
69+
root.child(name).then(app => subscribeToApp(app));
70+
}
71+
});
72+
}).catch(err => console.error("Connection failed:", err));
73+
1574
API
1675
---
1776

@@ -303,6 +362,20 @@ client.find(path)
303362
// use the load object referring to CPULoad in MyApp
304363
});
305364
365+
client.close()
366+
^^^^^^^^^^^^^^
367+
368+
- Usage
369+
370+
Close all connections managed by this client. This stops reconnection attempts
371+
and cleans up all resources. Call this when you are done using the client.
372+
373+
- Example
374+
375+
.. code:: javascript
376+
377+
client.close();
378+
306379
Instance Methods / INode
307380
~~~~~~~~~~~~~~~~~~~~~~~~
308381

@@ -334,11 +407,11 @@ node.info()
334407
+------------------+------------------------------+---------------------------------------------------------------+
335408
| Property | Type | Description |
336409
+==================+==============================+===============================================================+
337-
| Info.node_id | number | Application wide unique ID for each instance in CDP structure |
410+
| Info.nodeId | number | Application wide unique ID for each instance in CDP structure |
338411
+------------------+------------------------------+---------------------------------------------------------------+
339412
| Info.name | string | Nodes short name |
340413
+------------------+------------------------------+---------------------------------------------------------------+
341-
| Info.node_type | studio.protocol.CDPNodeType | | Direct CDP base type of the class. One of the following: |
414+
| Info.nodeType | studio.protocol.CDPNodeType | | Direct CDP base type of the class. One of the following: |
342415
| | | - CDP_UNDEFINED |
343416
| | | - CDP_APPLICATION |
344417
| | | - CDP_COMPONENT |
@@ -351,7 +424,7 @@ node.info()
351424
| | | - CDP_OPERATOR |
352425
| | | - CDP_NODE |
353426
+------------------+------------------------------+---------------------------------------------------------------+
354-
| Info.value_type | studio.protocol.CDPValueType | | Optional: Value primitive type the node holds |
427+
| Info.valueType | studio.protocol.CDPValueType | | Optional: Value primitive type the node holds |
355428
| | | | if node may hold a value. One of the following: |
356429
| | | - eUNDEFINED |
357430
| | | - eDOUBLE |
@@ -367,15 +440,15 @@ node.info()
367440
| | | - eBOOL |
368441
| | | - eSTRING |
369442
+------------------+------------------------------+---------------------------------------------------------------+
370-
| Info.type_name | string | Optional: Class name of the reflected node |
443+
| Info.typeName | string | Optional: Class name of the reflected node |
371444
+------------------+------------------------------+---------------------------------------------------------------+
372-
| Info.server_addr | string | Optional: StudioAPI IP present on application nodes that |
373-
| | | have **Info.is_local == false** |
445+
| Info.serverAddr | string | Optional: StudioAPI IP present on application nodes that |
446+
| | | have **Info.isLocal == false** |
374447
+------------------+------------------------------+---------------------------------------------------------------+
375-
| Info.server_port | number | Optional: StudioAPI Port present on application nodes that |
376-
| | | have **Info.is_local == false** |
448+
| Info.serverPort | number | Optional: StudioAPI Port present on application nodes that |
449+
| | | have **Info.isLocal == false** |
377450
+------------------+------------------------------+---------------------------------------------------------------+
378-
| Info.is_local | boolean | Optional: When multiple applications are present in root node |
451+
| Info.isLocal | boolean | Optional: When multiple applications are present in root node |
379452
| | | this flag is set to true for the application that the client |
380453
| | | is connected to |
381454
+------------------+------------------------------+---------------------------------------------------------------+
@@ -435,7 +508,7 @@ node.forEachChild(iteratorCallback)
435508
.. code:: javascript
436509
437510
cdpapp.forEachChild(function (child) {
438-
if (child.info().node_type == studio.protocol.CDPNodeType.CDP_COMPONENT) {
511+
if (child.info().nodeType == studio.protocol.CDPNodeType.CDP_COMPONENT) {
439512
// Use child object of type {INode} that is a CDP component.
440513
}
441514
});
@@ -477,7 +550,7 @@ node.subscribeToValues(valueConsumer, fs, sampleRate)
477550
- Usage
478551

479552
Subscribe to value changes on this node. On each value change valueConsumer function is called
480-
with value of the nodes value_type and UTC Unix timestamp in nanoseconds (nanoseconds from 01.01.1970).
553+
with value of the nodes valueType and UTC Unix timestamp in nanoseconds (nanoseconds from 01.01.1970).
481554
Timestamp refers to the time of value change in connected application on target controller.
482555

483556
- Example
@@ -579,7 +652,7 @@ node.subscribeToEvents(eventConsumer, timestampFrom)
579652
+-------------------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------+
580653
| Event.code | studio.protocol.EventCode | Optional: Event code flags. Any of: |
581654
| | +-----------------------------+---------------------------------------------------------------------------------------------+
582-
| | | - eAlarmSet | The alarm's Set flag/state was set. The alarm changed state to "Unack-Set" |
655+
| | | - aAlarmSet | The alarm's Set flag/state was set. The alarm changed state to "Unack-Set" |
583656
| | +-----------------------------+---------------------------------------------------------------------------------------------+
584657
| | | - eAlarmClr | The alarm's Set flag was cleared. The Unack state is unchanged. |
585658
| | +-----------------------------+---------------------------------------------------------------------------------------------+
@@ -591,7 +664,7 @@ node.subscribeToEvents(eventConsumer, timestampFrom)
591664
| | +-----------------------------+---------------------------------------------------------------------------------------------+
592665
| | | - eNodeBoot | The provider reports that the CDPEventNode just have booted. |
593666
+-------------------+-----------------------------+-----------------------------+---------------------------------------------------------------------------------------------+
594-
| Event.status | studio.protocol.EventStatus | Optional: Value primitive type the node holds if node may hold a value. Any of: |
667+
| Event.status | studio.protocol.EventStatus | Optional: Status flags as a numeric bitfield. Possible flag values: |
595668
| | +-----------------------------+---------------------------------------------------------------------------------------------+
596669
| | | - eStatusOK | No alarm set |
597670
| | +-----------------------------+---------------------------------------------------------------------------------------------+

0 commit comments

Comments
 (0)