@matter-server/ws-controller 0.7.0-alpha.0-20260512-b404bea → 0.7.0
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/dist/esm/controller/ControllerCommandHandler.d.ts +10 -0
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -1
- package/dist/esm/controller/ControllerCommandHandler.js +93 -24
- package/dist/esm/controller/ControllerCommandHandler.js.map +1 -1
- package/dist/esm/controller/MatterController.d.ts +7 -2
- package/dist/esm/controller/MatterController.d.ts.map +1 -1
- package/dist/esm/controller/MatterController.js +46 -3
- package/dist/esm/controller/MatterController.js.map +1 -1
- package/dist/esm/controller/Nodes.d.ts +7 -61
- package/dist/esm/controller/Nodes.d.ts.map +1 -1
- package/dist/esm/controller/Nodes.js +13 -77
- package/dist/esm/controller/Nodes.js.map +1 -1
- package/dist/esm/controller/WebRtcCallbackBridge.d.ts +9 -0
- package/dist/esm/controller/WebRtcCallbackBridge.d.ts.map +1 -0
- package/dist/esm/controller/WebRtcCallbackBridge.js +75 -0
- package/dist/esm/controller/WebRtcCallbackBridge.js.map +6 -0
- package/dist/esm/controller/behaviors/WebRtcTransportRequestorServer.d.ts +35 -0
- package/dist/esm/controller/behaviors/WebRtcTransportRequestorServer.d.ts.map +1 -0
- package/dist/esm/controller/behaviors/WebRtcTransportRequestorServer.js +123 -0
- package/dist/esm/controller/behaviors/WebRtcTransportRequestorServer.js.map +6 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -1
- package/dist/esm/server/WebSocketControllerHandler.js +17 -0
- package/dist/esm/server/WebSocketControllerHandler.js.map +1 -1
- package/package.json +22 -21
- package/src/controller/ControllerCommandHandler.ts +121 -42
- package/src/controller/MatterController.ts +54 -4
- package/src/controller/Nodes.ts +13 -78
- package/src/controller/WebRtcCallbackBridge.ts +79 -0
- package/src/controller/behaviors/WebRtcTransportRequestorServer.ts +149 -0
- package/src/index.ts +2 -1
- package/src/server/WebSocketControllerHandler.ts +24 -0
package/src/controller/Nodes.ts
CHANGED
|
@@ -22,36 +22,24 @@ import { AttributeDataCache } from "./AttributeDataCache.js";
|
|
|
22
22
|
export class Nodes {
|
|
23
23
|
#nodes = new Map<NodeId, PairedNode>();
|
|
24
24
|
#attributeCache = new AttributeDataCache();
|
|
25
|
-
/**
|
|
26
|
-
#previousStates = new Map<NodeId, NodeStates>();
|
|
27
|
-
/** Cached availability so serialization and event paths always agree */
|
|
25
|
+
/** Cached so serialization and event paths always agree on availability. */
|
|
28
26
|
#lastAvailability = new Map<NodeId, boolean>();
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* (e.g., Home Assistant) can resolve node.endpoints[endpoint_id] in their callback.
|
|
28
|
+
* Buffered endpoint_added events. python-matter-server wire contract requires
|
|
29
|
+
* node_updated (carrying the new endpoint) to arrive before endpoint_added,
|
|
30
|
+
* so HA can resolve node.endpoints[endpoint_id] in its callback.
|
|
34
31
|
*/
|
|
35
32
|
#pendingEndpointAdds = new Map<NodeId, EndpointNumber[]>();
|
|
36
33
|
|
|
37
|
-
/**
|
|
38
|
-
* Get the attribute cache instance.
|
|
39
|
-
*/
|
|
40
34
|
get attributeCache(): AttributeDataCache {
|
|
41
35
|
return this.#attributeCache;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
/**
|
|
45
|
-
* Get all node IDs.
|
|
46
|
-
*/
|
|
47
38
|
getIds(): NodeId[] {
|
|
48
39
|
return Array.from(this.#nodes.keys());
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
/**
|
|
52
|
-
* Get a node by ID.
|
|
53
|
-
* @throws ServerError if node not found
|
|
54
|
-
*/
|
|
42
|
+
/** @throws ServerError if node not found */
|
|
55
43
|
get(nodeId: NodeId): PairedNode {
|
|
56
44
|
const node = this.#nodes.get(nodeId);
|
|
57
45
|
if (node === undefined) {
|
|
@@ -60,34 +48,21 @@ export class Nodes {
|
|
|
60
48
|
return node;
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
/**
|
|
64
|
-
* Check if a node exists.
|
|
65
|
-
*/
|
|
66
51
|
has(nodeId: NodeId): boolean {
|
|
67
52
|
return this.#nodes.has(nodeId);
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
/**
|
|
71
|
-
* Add or update a node in storage.
|
|
72
|
-
*/
|
|
73
55
|
set(nodeId: NodeId, node: PairedNode): void {
|
|
74
56
|
this.#nodes.set(nodeId, node);
|
|
75
57
|
}
|
|
76
58
|
|
|
77
|
-
/**
|
|
78
|
-
* Remove a node from storage and clear its attribute cache and state tracking.
|
|
79
|
-
*/
|
|
80
59
|
delete(nodeId: NodeId): void {
|
|
81
60
|
this.#nodes.delete(nodeId);
|
|
82
61
|
this.#attributeCache.delete(nodeId);
|
|
83
|
-
this.#previousStates.delete(nodeId);
|
|
84
62
|
this.#lastAvailability.delete(nodeId);
|
|
85
63
|
this.#pendingEndpointAdds.delete(nodeId);
|
|
86
64
|
}
|
|
87
65
|
|
|
88
|
-
/**
|
|
89
|
-
* Buffer an endpoint_added until the next nodeStructureChanged for that node.
|
|
90
|
-
*/
|
|
91
66
|
queueEndpointAdded(nodeId: NodeId, endpointId: EndpointNumber): void {
|
|
92
67
|
let queue = this.#pendingEndpointAdds.get(nodeId);
|
|
93
68
|
if (queue === undefined) {
|
|
@@ -97,10 +72,7 @@ export class Nodes {
|
|
|
97
72
|
queue.push(endpointId);
|
|
98
73
|
}
|
|
99
74
|
|
|
100
|
-
/**
|
|
101
|
-
* Take ownership of buffered endpoint additions for a node and clear the queue.
|
|
102
|
-
* Returned array is in insertion order; an empty array is returned if nothing is queued.
|
|
103
|
-
*/
|
|
75
|
+
/** Returns insertion-ordered queue; empty if nothing pending. */
|
|
104
76
|
drainPendingEndpointAdds(nodeId: NodeId): EndpointNumber[] {
|
|
105
77
|
const queue = this.#pendingEndpointAdds.get(nodeId);
|
|
106
78
|
if (queue === undefined || queue.length === 0) {
|
|
@@ -110,30 +82,19 @@ export class Nodes {
|
|
|
110
82
|
return queue;
|
|
111
83
|
}
|
|
112
84
|
|
|
113
|
-
/**
|
|
114
|
-
* Initialize state tracking for a newly paired/discovered node.
|
|
115
|
-
* Sets previous state and initial availability so the first stateChanged event
|
|
116
|
-
* has a real previous state instead of undefined.
|
|
117
|
-
*/
|
|
118
85
|
seedState(nodeId: NodeId, initialState: NodeStates): void {
|
|
119
|
-
this.#previousStates.set(nodeId, initialState);
|
|
120
86
|
this.#lastAvailability.set(nodeId, initialState === NodeStates.Connected);
|
|
121
87
|
}
|
|
122
88
|
|
|
123
|
-
/**
|
|
124
|
-
* Process a state change for a node. Reads previous state BEFORE updating,
|
|
125
|
-
* computes new availability, and updates both tracking maps atomically.
|
|
126
|
-
* Returns whether availability changed and the new value.
|
|
127
|
-
*/
|
|
89
|
+
/** `debouncePending` = reconnect timer armed by caller; keeps non-Connected states available. */
|
|
128
90
|
processStateChange(
|
|
129
91
|
nodeId: NodeId,
|
|
130
92
|
newState: NodeStates,
|
|
93
|
+
debouncePending: boolean,
|
|
131
94
|
): { availabilityChanged: true; available: boolean } | { availabilityChanged: false } {
|
|
132
|
-
const previousState = this.#previousStates.get(nodeId);
|
|
133
95
|
const wasAvailable = this.#lastAvailability.get(nodeId) ?? false;
|
|
134
|
-
const available = this.isNodeAvailable(newState,
|
|
96
|
+
const available = this.isNodeAvailable(newState, debouncePending);
|
|
135
97
|
|
|
136
|
-
this.#previousStates.set(nodeId, newState);
|
|
137
98
|
this.#lastAvailability.set(nodeId, available);
|
|
138
99
|
|
|
139
100
|
if (wasAvailable !== available) {
|
|
@@ -142,47 +103,21 @@ export class Nodes {
|
|
|
142
103
|
return { availabilityChanged: false };
|
|
143
104
|
}
|
|
144
105
|
|
|
145
|
-
/**
|
|
146
|
-
* Force a node to unavailable state. Used by reconnect timeout
|
|
147
|
-
* when debounce period expires without reconnection.
|
|
148
|
-
* Only updates #lastAvailability — #previousStates is left as-is because
|
|
149
|
-
* processStateChange() will overwrite it on the next real state transition.
|
|
150
|
-
* Returns true if the node was previously considered available.
|
|
151
|
-
*/
|
|
106
|
+
/** Returns true if the node was previously considered available. */
|
|
152
107
|
forceUnavailable(nodeId: NodeId): boolean {
|
|
153
108
|
const wasAvailable = this.#lastAvailability.get(nodeId) ?? false;
|
|
154
109
|
this.#lastAvailability.set(nodeId, false);
|
|
155
110
|
return wasAvailable;
|
|
156
111
|
}
|
|
157
112
|
|
|
158
|
-
|
|
159
|
-
* Determine if a node should be considered available based on its connection state.
|
|
160
|
-
* Uses debouncing logic similar to Python Matter Server:
|
|
161
|
-
* - Connected: available
|
|
162
|
-
* - Reconnecting when previously Connected: still available (debouncing)
|
|
163
|
-
* - WaitingForDeviceDiscovery or Disconnected: unavailable
|
|
164
|
-
*
|
|
165
|
-
* @param currentState Current connection state
|
|
166
|
-
* @param previousState Previous connection state (undefined if first state change)
|
|
167
|
-
* @returns true if node should be considered available
|
|
168
|
-
*/
|
|
169
|
-
isNodeAvailable(currentState: NodeStates, previousState?: NodeStates): boolean {
|
|
113
|
+
isNodeAvailable(currentState: NodeStates, debouncePending = false): boolean {
|
|
170
114
|
if (currentState === NodeStates.Connected) {
|
|
171
115
|
return true;
|
|
172
116
|
}
|
|
173
|
-
|
|
174
|
-
if (currentState === NodeStates.Reconnecting && previousState === NodeStates.Connected) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
// WaitingForDeviceDiscovery, Disconnected, or Reconnecting from non-connected state
|
|
178
|
-
return false;
|
|
117
|
+
return debouncePending;
|
|
179
118
|
}
|
|
180
119
|
|
|
181
|
-
/**
|
|
182
|
-
* Check if a node is available. Returns the cached availability value set by
|
|
183
|
-
* seedState/processStateChange/forceUnavailable, avoiding the race where
|
|
184
|
-
* recomputing from live state disagrees with the event path's determination.
|
|
185
|
-
*/
|
|
120
|
+
/** Returns the cached value, not a recomputation — avoids disagreement with the event path. */
|
|
186
121
|
isAvailable(nodeId: NodeId): boolean {
|
|
187
122
|
return this.#lastAvailability.get(nodeId) ?? false;
|
|
188
123
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WebRtcCallbackData } from "@matter-server/ws-client";
|
|
8
|
+
import { Logger } from "@matter/main";
|
|
9
|
+
import type { WebRtcTransportRequestorServer } from "./behaviors/WebRtcTransportRequestorServer.js";
|
|
10
|
+
|
|
11
|
+
const logger = Logger.get("WebRtcCallbackBridge");
|
|
12
|
+
|
|
13
|
+
export function attachWebRtcCallbackBridge(
|
|
14
|
+
events: WebRtcTransportRequestorServer.Events,
|
|
15
|
+
emit: (data: WebRtcCallbackData) => void,
|
|
16
|
+
): void {
|
|
17
|
+
events.offer.on((session, request) => {
|
|
18
|
+
logger.info(
|
|
19
|
+
`offer from peer node=${session.peerNodeId} ep=${session.peerEndpointId} session=${session.id} sdpLen=${request.sdp.length}`,
|
|
20
|
+
);
|
|
21
|
+
emit({
|
|
22
|
+
event_type: "offer",
|
|
23
|
+
webrtc_session_id: session.id,
|
|
24
|
+
node_id: session.peerNodeId,
|
|
25
|
+
endpoint_id: session.peerEndpointId,
|
|
26
|
+
fabric_index: session.fabricIndex,
|
|
27
|
+
data: {
|
|
28
|
+
sdp: request.sdp,
|
|
29
|
+
ice_servers: request.iceServers,
|
|
30
|
+
ice_transport_policy: request.iceTransportPolicy,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
events.answer.on((session, sdp) => {
|
|
35
|
+
logger.info(
|
|
36
|
+
`answer from peer node=${session.peerNodeId} ep=${session.peerEndpointId} session=${session.id} sdpLen=${sdp.length}`,
|
|
37
|
+
);
|
|
38
|
+
emit({
|
|
39
|
+
event_type: "answer",
|
|
40
|
+
webrtc_session_id: session.id,
|
|
41
|
+
node_id: session.peerNodeId,
|
|
42
|
+
endpoint_id: session.peerEndpointId,
|
|
43
|
+
fabric_index: session.fabricIndex,
|
|
44
|
+
data: { sdp },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
events.iceCandidates.on((session, candidates) => {
|
|
48
|
+
logger.info(
|
|
49
|
+
`ice_candidates from peer node=${session.peerNodeId} ep=${session.peerEndpointId} session=${session.id} count=${candidates.length}`,
|
|
50
|
+
);
|
|
51
|
+
emit({
|
|
52
|
+
event_type: "ice_candidates",
|
|
53
|
+
webrtc_session_id: session.id,
|
|
54
|
+
node_id: session.peerNodeId,
|
|
55
|
+
endpoint_id: session.peerEndpointId,
|
|
56
|
+
fabric_index: session.fabricIndex,
|
|
57
|
+
data: {
|
|
58
|
+
ice_candidates: candidates.map(c => ({
|
|
59
|
+
candidate: c.candidate,
|
|
60
|
+
sdpMid: c.sdpMid ?? null,
|
|
61
|
+
sdpMLineIndex: c.sdpmLineIndex ?? null,
|
|
62
|
+
})),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
events.end.on((session, reason) => {
|
|
67
|
+
logger.info(
|
|
68
|
+
`end from peer node=${session.peerNodeId} ep=${session.peerEndpointId} session=${session.id} reason=${reason}`,
|
|
69
|
+
);
|
|
70
|
+
emit({
|
|
71
|
+
event_type: "end",
|
|
72
|
+
webrtc_session_id: session.id,
|
|
73
|
+
node_id: session.peerNodeId,
|
|
74
|
+
endpoint_id: session.peerEndpointId,
|
|
75
|
+
fabric_index: session.fabricIndex,
|
|
76
|
+
data: { reason },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Observable } from "@matter/general";
|
|
8
|
+
import { Logger, Node } from "@matter/main";
|
|
9
|
+
import type { ServerNode } from "@matter/main";
|
|
10
|
+
import { AccessControlServer } from "@matter/node/behaviors/access-control";
|
|
11
|
+
import { WebRtcTransportRequestorBehavior } from "@matter/node/behaviors/web-rtc-transport-requestor";
|
|
12
|
+
import { assertRemoteActor, FabricAuthority, NodeSession } from "@matter/protocol";
|
|
13
|
+
import { Status, StatusResponseError } from "@matter/types";
|
|
14
|
+
import { AccessControl } from "@matter/types/clusters/access-control";
|
|
15
|
+
import { WebRtcTransportDefinitions } from "@matter/types/clusters/web-rtc-transport-definitions";
|
|
16
|
+
import { WebRtcTransportRequestor } from "@matter/types/clusters/web-rtc-transport-requestor";
|
|
17
|
+
|
|
18
|
+
type WebRtcSession = WebRtcTransportDefinitions.WebRtcSession;
|
|
19
|
+
|
|
20
|
+
const logger = Logger.get("WebRtcTransportRequestorServer");
|
|
21
|
+
|
|
22
|
+
export class WebRtcTransportRequestorServer extends WebRtcTransportRequestorBehavior {
|
|
23
|
+
declare state: WebRtcTransportRequestorServer.State;
|
|
24
|
+
declare events: WebRtcTransportRequestorServer.Events;
|
|
25
|
+
|
|
26
|
+
upsertSession(session: WebRtcSession): void {
|
|
27
|
+
const enriched: WebRtcSession = {
|
|
28
|
+
...session,
|
|
29
|
+
videoStreamId: session.videoStreams?.[0] ?? null,
|
|
30
|
+
audioStreamId: session.audioStreams?.[0] ?? null,
|
|
31
|
+
};
|
|
32
|
+
const sessions = this.state.currentSessions;
|
|
33
|
+
const idx = sessions.findIndex(s => s.id === session.id);
|
|
34
|
+
if (idx === -1) {
|
|
35
|
+
this.state.currentSessions = [...sessions, enriched];
|
|
36
|
+
} else {
|
|
37
|
+
this.state.currentSessions = sessions.map((s, i) => (i === idx ? enriched : s));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
removeSession(id: number): void {
|
|
42
|
+
this.state.currentSessions = this.state.currentSessions.filter(s => s.id !== id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override async offer(request: WebRtcTransportRequestor.OfferRequest): Promise<void> {
|
|
46
|
+
logger.info(`incoming Offer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
|
|
47
|
+
const session = this.#findSessionStrict(request.webRtcSessionId);
|
|
48
|
+
this.events.offer.emit(session, request);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override async answer(request: WebRtcTransportRequestor.AnswerRequest): Promise<void> {
|
|
52
|
+
logger.info(`incoming Answer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
|
|
53
|
+
const session = this.#findSessionStrict(request.webRtcSessionId);
|
|
54
|
+
this.events.answer.emit(session, request.sdp);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override async iceCandidates(request: WebRtcTransportRequestor.IceCandidatesRequest): Promise<void> {
|
|
58
|
+
logger.info(
|
|
59
|
+
`incoming ICECandidates webRtcSessionId=${request.webRtcSessionId} count=${request.iceCandidates.length}`,
|
|
60
|
+
);
|
|
61
|
+
if (request.iceCandidates.length === 0) {
|
|
62
|
+
throw new StatusResponseError("ICE candidates list must not be empty", Status.InvalidCommand);
|
|
63
|
+
}
|
|
64
|
+
const session = this.#findSessionStrict(request.webRtcSessionId);
|
|
65
|
+
this.events.iceCandidates.emit(session, request.iceCandidates);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override async end(request: WebRtcTransportRequestor.EndRequest): Promise<void> {
|
|
69
|
+
logger.info(`incoming End webRtcSessionId=${request.webRtcSessionId} reason=${request.reason}`);
|
|
70
|
+
const session = this.#findSessionStrict(request.webRtcSessionId);
|
|
71
|
+
this.removeSession(request.webRtcSessionId);
|
|
72
|
+
this.events.end.emit(session, request.reason);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override async initialize() {
|
|
76
|
+
const node = Node.forEndpoint(this.endpoint) as ServerNode;
|
|
77
|
+
logger.info(
|
|
78
|
+
`initialized on endpoint=${this.endpoint.number} (id="${this.endpoint.id}") cluster=0x${WebRtcTransportRequestor.id.toString(16)}`,
|
|
79
|
+
);
|
|
80
|
+
this.reactTo(node.lifecycle.online, this.#nodeOnline);
|
|
81
|
+
if (node.lifecycle.isOnline) {
|
|
82
|
+
await this.#nodeOnline();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async #nodeOnline() {
|
|
87
|
+
const fabricAuthority = this.env.get(FabricAuthority);
|
|
88
|
+
const ownFabric = fabricAuthority.fabrics[0];
|
|
89
|
+
if (!ownFabric) {
|
|
90
|
+
// void: fabricAdded is a synchronous observable; awaiting #nodeOnline here deadlocks setStateOf.
|
|
91
|
+
fabricAuthority.fabricAdded.once(() => void this.#nodeOnline());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const node = Node.forEndpoint(this.endpoint) as ServerNode;
|
|
96
|
+
await node.act(agent => agent.load(AccessControlServer));
|
|
97
|
+
if (node.behaviors.has(AccessControlServer)) {
|
|
98
|
+
if (
|
|
99
|
+
!node
|
|
100
|
+
.stateOf(AccessControlServer)
|
|
101
|
+
.acl.some(
|
|
102
|
+
({ fabricIndex, privilege, authMode, subjects, targets }) =>
|
|
103
|
+
fabricIndex === ownFabric.fabricIndex &&
|
|
104
|
+
privilege === AccessControl.AccessControlEntryPrivilege.Operate &&
|
|
105
|
+
authMode === AccessControl.AccessControlEntryAuthMode.Case &&
|
|
106
|
+
subjects?.length === 0 &&
|
|
107
|
+
targets?.length === 1 &&
|
|
108
|
+
targets[0].endpoint === this.endpoint.number &&
|
|
109
|
+
targets[0].cluster === WebRtcTransportRequestor.id,
|
|
110
|
+
)
|
|
111
|
+
) {
|
|
112
|
+
const acl = [
|
|
113
|
+
...node.stateOf(AccessControlServer).acl,
|
|
114
|
+
{
|
|
115
|
+
fabricIndex: ownFabric.fabricIndex,
|
|
116
|
+
privilege: AccessControl.AccessControlEntryPrivilege.Operate,
|
|
117
|
+
authMode: AccessControl.AccessControlEntryAuthMode.Case,
|
|
118
|
+
subjects: [],
|
|
119
|
+
targets: [{ endpoint: this.endpoint.number, cluster: WebRtcTransportRequestor.id }],
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
await node.setStateOf(AccessControlServer, { acl });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#findSessionStrict(id: number): WebRtcSession {
|
|
128
|
+
assertRemoteActor(this.context);
|
|
129
|
+
NodeSession.assert(this.context.session);
|
|
130
|
+
const peer = this.context.session.peerAddress;
|
|
131
|
+
const session = this.state.currentSessions.find(s => s.id === id);
|
|
132
|
+
if (session === undefined || session.fabricIndex !== peer.fabricIndex || session.peerNodeId !== peer.nodeId) {
|
|
133
|
+
throw new StatusResponseError(`WebRTC session ${id} not found`, Status.NotFound);
|
|
134
|
+
}
|
|
135
|
+
return session;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export namespace WebRtcTransportRequestorServer {
|
|
140
|
+
export class State extends WebRtcTransportRequestorBehavior.State {
|
|
141
|
+
override currentSessions: WebRtcSession[] = new Array<WebRtcSession>();
|
|
142
|
+
}
|
|
143
|
+
export class Events extends WebRtcTransportRequestorBehavior.Events {
|
|
144
|
+
offer = Observable<[session: WebRtcSession, args: WebRtcTransportRequestor.OfferRequest]>();
|
|
145
|
+
answer = Observable<[session: WebRtcSession, sdp: string]>();
|
|
146
|
+
iceCandidates = Observable<[session: WebRtcSession, candidates: WebRtcTransportDefinitions.IceCandidate[]]>();
|
|
147
|
+
end = Observable<[session: WebRtcSession, reason: WebRtcTransportDefinitions.WebRtcEndReason]>();
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// Export controller components
|
|
12
|
+
export * from "./controller/behaviors/WebRtcTransportRequestorServer.js";
|
|
12
13
|
export * from "./controller/ControllerCommandHandler.js";
|
|
13
14
|
export * from "./controller/LegacyDataInjector.js";
|
|
14
15
|
export * from "./controller/MatterController.js";
|
|
@@ -32,4 +33,4 @@ export { formatNodeId } from "./util/formatNodeId.js";
|
|
|
32
33
|
export * from "./util/matterVersion.js";
|
|
33
34
|
|
|
34
35
|
// Re-Export classes from matter.js
|
|
35
|
-
export { Crypto, Environment, LogDestination, LogFormat, LogLevel, Logger } from "@matter/main";
|
|
36
|
+
export { Crypto, Environment, LogDestination, LogFormat, LogLevel, Logger, StorageService } from "@matter/main";
|
|
@@ -84,6 +84,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
84
84
|
#wss?: WebSocketServer;
|
|
85
85
|
#closed = false;
|
|
86
86
|
#shuttingDown = false;
|
|
87
|
+
#serverObservers = new ObserverGroup();
|
|
87
88
|
/** Circular buffer for recent node events (max 25) */
|
|
88
89
|
#eventHistory: MatterNodeEvent[] = [];
|
|
89
90
|
/** Track when each node was last interviewed (connected) - keyed by nodeId */
|
|
@@ -142,6 +143,13 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
142
143
|
async register(server: HttpServer) {
|
|
143
144
|
logger.info(`Starting server: matter-server/${this.#serverVersion} (matter.js/${MATTER_VERSION})`);
|
|
144
145
|
const wss = (this.#wss = new WebSocketServer({ server: server, path: "/ws" }));
|
|
146
|
+
|
|
147
|
+
// WebRTC callbacks fan out to every connected client, so subscribe once at the server
|
|
148
|
+
// level rather than per-connection.
|
|
149
|
+
this.#serverObservers.on(this.#commandHandler.events.webRtcCallback, data => {
|
|
150
|
+
this.#broadcastEvent("webrtc_callback", data);
|
|
151
|
+
});
|
|
152
|
+
|
|
145
153
|
wss.on("connection", ws => {
|
|
146
154
|
if (this.#closed || this.#shuttingDown) {
|
|
147
155
|
try {
|
|
@@ -372,6 +380,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
372
380
|
}
|
|
373
381
|
|
|
374
382
|
this.#closed = true;
|
|
383
|
+
this.#serverObservers.close();
|
|
375
384
|
// Send server_shutdown event to all connected clients before closing
|
|
376
385
|
const shutdownMessage = toBigIntAwareJson({ event: "server_shutdown", data: {} });
|
|
377
386
|
this.#wss.clients.forEach(client => {
|
|
@@ -445,6 +454,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
445
454
|
case "device_command":
|
|
446
455
|
result = await this.#handleDeviceCommand(args);
|
|
447
456
|
break;
|
|
457
|
+
case "send_webrtc_provider_command":
|
|
458
|
+
result = await this.#handleSendWebRtcProviderCommand(args);
|
|
459
|
+
break;
|
|
448
460
|
case "write_attribute":
|
|
449
461
|
result = await this.#handleWriteAttribute(args);
|
|
450
462
|
break;
|
|
@@ -866,6 +878,18 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
866
878
|
return cmdResult;
|
|
867
879
|
}
|
|
868
880
|
|
|
881
|
+
async #handleSendWebRtcProviderCommand(
|
|
882
|
+
args: ArgsOf<"send_webrtc_provider_command">,
|
|
883
|
+
): Promise<ResponseOf<"send_webrtc_provider_command">> {
|
|
884
|
+
const { node_id, endpoint_id, command_name, payload } = args;
|
|
885
|
+
return this.#commandHandler.sendWebRtcProviderCommand({
|
|
886
|
+
nodeId: NodeId(node_id),
|
|
887
|
+
endpointId: EndpointNumber(endpoint_id),
|
|
888
|
+
commandName: command_name,
|
|
889
|
+
payload,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
869
893
|
async #handleInterviewNode(args: ArgsOf<"interview_node">): Promise<ResponseOf<"interview_node">> {
|
|
870
894
|
const { node_id } = args;
|
|
871
895
|
const nodeId = NodeId(node_id);
|