@matter/node 0.16.0-alpha.0-20251027-17770fb28 → 0.16.0-alpha.0-20251030-e9ca79f93

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.
Files changed (120) hide show
  1. package/dist/cjs/behavior/Behavior.d.ts +1 -0
  2. package/dist/cjs/behavior/Behavior.d.ts.map +1 -1
  3. package/dist/cjs/behavior/Behavior.js +5 -0
  4. package/dist/cjs/behavior/Behavior.js.map +1 -1
  5. package/dist/cjs/behavior/internal/BehaviorBacking.js +1 -1
  6. package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
  7. package/dist/cjs/behavior/state/managed/Datasource.d.ts +4 -5
  8. package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
  9. package/dist/cjs/behavior/state/managed/Datasource.js +6 -2
  10. package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
  11. package/dist/cjs/behavior/state/managed/ManagedReference.d.ts +3 -2
  12. package/dist/cjs/behavior/state/managed/ManagedReference.d.ts.map +1 -1
  13. package/dist/cjs/behavior/state/managed/ManagedReference.js +65 -20
  14. package/dist/cjs/behavior/state/managed/ManagedReference.js.map +1 -1
  15. package/dist/cjs/behavior/state/managed/values/ListManager.js +2 -1
  16. package/dist/cjs/behavior/state/managed/values/ListManager.js.map +1 -1
  17. package/dist/cjs/behavior/state/managed/values/StructManager.js +9 -1
  18. package/dist/cjs/behavior/state/managed/values/StructManager.js.map +1 -1
  19. package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  20. package/dist/cjs/behaviors/access-control/AccessControlServer.js +3 -3
  21. package/dist/cjs/behaviors/access-control/AccessControlServer.js.map +1 -1
  22. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  23. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +3 -9
  24. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  25. package/dist/cjs/behaviors/service-area/ServiceAreaServer.js +2 -2
  26. package/dist/cjs/behaviors/service-area/ServiceAreaServer.js.map +1 -1
  27. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.d.ts +58 -0
  28. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.d.ts.map +1 -0
  29. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js +306 -0
  30. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js.map +6 -0
  31. package/dist/cjs/behaviors/thermostat/AtomicWriteState.d.ts +33 -0
  32. package/dist/cjs/behaviors/thermostat/AtomicWriteState.d.ts.map +1 -0
  33. package/dist/cjs/behaviors/thermostat/AtomicWriteState.js +86 -0
  34. package/dist/cjs/behaviors/thermostat/AtomicWriteState.js.map +6 -0
  35. package/dist/cjs/behaviors/thermostat/ThermostatBehavior.d.ts +12 -0
  36. package/dist/cjs/behaviors/thermostat/ThermostatBehavior.d.ts.map +1 -1
  37. package/dist/cjs/behaviors/thermostat/ThermostatInterface.d.ts +1 -0
  38. package/dist/cjs/behaviors/thermostat/ThermostatInterface.d.ts.map +1 -1
  39. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts +894 -3
  40. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  41. package/dist/cjs/behaviors/thermostat/ThermostatServer.js +1216 -1
  42. package/dist/cjs/behaviors/thermostat/ThermostatServer.js.map +2 -2
  43. package/dist/cjs/devices/water-heater.d.ts +24 -0
  44. package/dist/cjs/devices/water-heater.d.ts.map +1 -1
  45. package/dist/cjs/endpoint/Endpoint.d.ts +36 -2
  46. package/dist/cjs/endpoint/Endpoint.d.ts.map +1 -1
  47. package/dist/cjs/endpoint/Endpoint.js +17 -14
  48. package/dist/cjs/endpoint/Endpoint.js.map +1 -1
  49. package/dist/cjs/endpoint/properties/EndpointContainer.d.ts +1 -0
  50. package/dist/cjs/endpoint/properties/EndpointContainer.d.ts.map +1 -1
  51. package/dist/cjs/endpoint/properties/EndpointContainer.js +3 -0
  52. package/dist/cjs/endpoint/properties/EndpointContainer.js.map +1 -1
  53. package/dist/esm/behavior/Behavior.d.ts +1 -0
  54. package/dist/esm/behavior/Behavior.d.ts.map +1 -1
  55. package/dist/esm/behavior/Behavior.js +5 -0
  56. package/dist/esm/behavior/Behavior.js.map +1 -1
  57. package/dist/esm/behavior/internal/BehaviorBacking.js +2 -2
  58. package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
  59. package/dist/esm/behavior/state/managed/Datasource.d.ts +4 -5
  60. package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
  61. package/dist/esm/behavior/state/managed/Datasource.js +7 -3
  62. package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
  63. package/dist/esm/behavior/state/managed/ManagedReference.d.ts +3 -2
  64. package/dist/esm/behavior/state/managed/ManagedReference.d.ts.map +1 -1
  65. package/dist/esm/behavior/state/managed/ManagedReference.js +66 -21
  66. package/dist/esm/behavior/state/managed/ManagedReference.js.map +1 -1
  67. package/dist/esm/behavior/state/managed/values/ListManager.js +2 -1
  68. package/dist/esm/behavior/state/managed/values/ListManager.js.map +1 -1
  69. package/dist/esm/behavior/state/managed/values/StructManager.js +9 -1
  70. package/dist/esm/behavior/state/managed/values/StructManager.js.map +1 -1
  71. package/dist/esm/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  72. package/dist/esm/behaviors/access-control/AccessControlServer.js +3 -3
  73. package/dist/esm/behaviors/access-control/AccessControlServer.js.map +1 -1
  74. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  75. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +3 -9
  76. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  77. package/dist/esm/behaviors/service-area/ServiceAreaServer.js +2 -2
  78. package/dist/esm/behaviors/service-area/ServiceAreaServer.js.map +1 -1
  79. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.d.ts +58 -0
  80. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.d.ts.map +1 -0
  81. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js +293 -0
  82. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js.map +6 -0
  83. package/dist/esm/behaviors/thermostat/AtomicWriteState.d.ts +33 -0
  84. package/dist/esm/behaviors/thermostat/AtomicWriteState.d.ts.map +1 -0
  85. package/dist/esm/behaviors/thermostat/AtomicWriteState.js +66 -0
  86. package/dist/esm/behaviors/thermostat/AtomicWriteState.js.map +6 -0
  87. package/dist/esm/behaviors/thermostat/ThermostatBehavior.d.ts +12 -0
  88. package/dist/esm/behaviors/thermostat/ThermostatBehavior.d.ts.map +1 -1
  89. package/dist/esm/behaviors/thermostat/ThermostatInterface.d.ts +1 -0
  90. package/dist/esm/behaviors/thermostat/ThermostatInterface.d.ts.map +1 -1
  91. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts +894 -3
  92. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  93. package/dist/esm/behaviors/thermostat/ThermostatServer.js +1225 -1
  94. package/dist/esm/behaviors/thermostat/ThermostatServer.js.map +2 -2
  95. package/dist/esm/devices/water-heater.d.ts +24 -0
  96. package/dist/esm/devices/water-heater.d.ts.map +1 -1
  97. package/dist/esm/endpoint/Endpoint.d.ts +36 -2
  98. package/dist/esm/endpoint/Endpoint.d.ts.map +1 -1
  99. package/dist/esm/endpoint/Endpoint.js +17 -14
  100. package/dist/esm/endpoint/Endpoint.js.map +1 -1
  101. package/dist/esm/endpoint/properties/EndpointContainer.d.ts +1 -0
  102. package/dist/esm/endpoint/properties/EndpointContainer.d.ts.map +1 -1
  103. package/dist/esm/endpoint/properties/EndpointContainer.js +3 -0
  104. package/dist/esm/endpoint/properties/EndpointContainer.js.map +1 -1
  105. package/package.json +7 -7
  106. package/src/behavior/Behavior.ts +10 -0
  107. package/src/behavior/internal/BehaviorBacking.ts +2 -2
  108. package/src/behavior/state/managed/Datasource.ts +14 -7
  109. package/src/behavior/state/managed/ManagedReference.ts +67 -19
  110. package/src/behavior/state/managed/values/ListManager.ts +1 -0
  111. package/src/behavior/state/managed/values/StructManager.ts +13 -3
  112. package/src/behaviors/access-control/AccessControlServer.ts +3 -7
  113. package/src/behaviors/general-diagnostics/GeneralDiagnosticsServer.ts +5 -9
  114. package/src/behaviors/service-area/ServiceAreaServer.ts +2 -2
  115. package/src/behaviors/thermostat/AtomicWriteHandler.ts +412 -0
  116. package/src/behaviors/thermostat/AtomicWriteState.ts +91 -0
  117. package/src/behaviors/thermostat/ThermostatInterface.ts +2 -0
  118. package/src/behaviors/thermostat/ThermostatServer.ts +1487 -3
  119. package/src/endpoint/Endpoint.ts +61 -5
  120. package/src/endpoint/properties/EndpointContainer.ts +4 -0
@@ -31,6 +31,7 @@ type Container = Record<string | number, Val>;
31
31
  * @param id the lookup ID in the case of structs
32
32
  * @param assertWriteOk enforces ACLs and read-only
33
33
  * @param clone clones the container prior to write; undefined if not transactional
34
+ * @param session the access control session
34
35
  *
35
36
  * @returns a reference to the property
36
37
  */
@@ -41,6 +42,7 @@ export function ManagedReference(
41
42
  id: number | undefined,
42
43
  assertWriteOk: (value: Val) => void,
43
44
  clone: (container: Val) => Val,
45
+ session: AccessControl.Session,
44
46
  ) {
45
47
  let expired = false;
46
48
  let location = {
@@ -51,10 +53,24 @@ export function ManagedReference(
51
53
  const key = primaryKey === "id" ? (id ?? name) : name;
52
54
  const altKey = primaryKey === "id" ? (key === name ? undefined : name) : id;
53
55
  let value: unknown;
54
- if (key in (parent.value as Container)) {
55
- value = (parent.value as Container)[key];
56
- } else if (altKey !== undefined) {
57
- value = (parent.value as Container)[altKey];
56
+
57
+ let dynamicContainer: Val.Struct | undefined;
58
+ if ((parent.value as Val.Dynamic)[Val.properties]) {
59
+ dynamicContainer = (parent.value as Val.Dynamic)[Val.properties](parent.rootOwner, session);
60
+ if (key in (dynamicContainer as Container)) {
61
+ value = (dynamicContainer as Container)[key];
62
+ } else if (altKey !== undefined && altKey in (dynamicContainer as Container)) {
63
+ value = (dynamicContainer as Container)[altKey];
64
+ } else {
65
+ dynamicContainer = undefined;
66
+ }
67
+ }
68
+ if (dynamicContainer === undefined) {
69
+ if (key in (parent.value as Container)) {
70
+ value = (parent.value as Container)[key];
71
+ } else if (altKey !== undefined) {
72
+ value = (parent.value as Container)[altKey];
73
+ }
58
74
  }
59
75
 
60
76
  const reference: Val.Reference = {
@@ -97,9 +113,16 @@ export function ManagedReference(
97
113
 
98
114
  // Now use change to complete the update
99
115
  this.change(() => {
100
- (parent.value as Container)[key] = newValue;
101
- if (altKey !== undefined && altKey in parent.value) {
102
- delete (parent.value as Container)[altKey];
116
+ if (dynamicContainer) {
117
+ (dynamicContainer as Container)[key] = newValue;
118
+ if (altKey !== undefined && altKey in dynamicContainer) {
119
+ delete (dynamicContainer as Container)[altKey];
120
+ }
121
+ } else {
122
+ (parent.value as Container)[key] = newValue;
123
+ if (altKey !== undefined && altKey in parent.value) {
124
+ delete (parent.value as Container)[altKey];
125
+ }
103
126
  }
104
127
  });
105
128
  },
@@ -108,11 +131,21 @@ export function ManagedReference(
108
131
  if (!parent.original) {
109
132
  return undefined;
110
133
  }
111
- if (key in parent.original) {
112
- return (parent.original as Container)[key];
113
- }
114
- if (altKey !== undefined) {
115
- return (parent.original as Container)[altKey];
134
+ if (dynamicContainer !== undefined) {
135
+ const origProperties = (parent.original as Val.Dynamic)[Val.properties](parent.rootOwner, session);
136
+ if (key in (origProperties as Container)) {
137
+ return (origProperties as Container)[key];
138
+ }
139
+ if (altKey !== undefined) {
140
+ return (origProperties as Container)[altKey];
141
+ }
142
+ } else {
143
+ if (key in parent.original) {
144
+ return (parent.original as Container)[key];
145
+ }
146
+ if (altKey !== undefined) {
147
+ return (parent.original as Container)[altKey];
148
+ }
116
149
  }
117
150
  },
118
151
 
@@ -125,9 +158,16 @@ export function ManagedReference(
125
158
  // In transactions, clone the value if we haven't done so yet
126
159
  if (clone && value === this.original) {
127
160
  const newValue = clone(value);
128
- (parent.value as Container)[key] = newValue;
129
- if (altKey !== undefined && altKey in (parent.value as Container)) {
130
- delete (parent.value as Container)[altKey];
161
+ if (dynamicContainer !== undefined) {
162
+ (dynamicContainer as Container)[key] = newValue;
163
+ if (altKey !== undefined && altKey in dynamicContainer) {
164
+ delete (dynamicContainer as Container)[altKey];
165
+ }
166
+ } else {
167
+ (parent.value as Container)[key] = newValue;
168
+ if (altKey !== undefined && altKey in (parent.value as Container)) {
169
+ delete (parent.value as Container)[altKey];
170
+ }
131
171
  }
132
172
  replaceValue(newValue);
133
173
  }
@@ -149,10 +189,18 @@ export function ManagedReference(
149
189
  }
150
190
 
151
191
  let value;
152
- if (key in parent.value) {
153
- value = (parent.value as Container)[key];
154
- } else if (altKey !== undefined && altKey in parent.value) {
155
- value = (parent.value as Container)[altKey];
192
+ if (dynamicContainer !== undefined) {
193
+ if (key in dynamicContainer) {
194
+ value = (dynamicContainer as Container)[key];
195
+ } else if (altKey !== undefined && altKey in dynamicContainer) {
196
+ value = (dynamicContainer as Container)[altKey];
197
+ }
198
+ } else {
199
+ if (key in parent.value) {
200
+ value = (parent.value as Container)[key];
201
+ } else if (altKey !== undefined && altKey in parent.value) {
202
+ value = (parent.value as Container)[altKey];
203
+ }
156
204
  }
157
205
 
158
206
  replaceValue(value);
@@ -160,6 +160,7 @@ function createProxy(config: ListConfig, reference: Val.Reference<Val.List>, ses
160
160
  () => true,
161
161
 
162
162
  val => (Array.isArray(val) ? [...(val as Val.List)] : isObject(val) ? { ...val } : val),
163
+ session,
163
164
  );
164
165
 
165
166
  manageEntry(subref, session);
@@ -279,7 +279,7 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
279
279
  };
280
280
 
281
281
  if (manage === PrimitiveManager) {
282
- // For primitives we don't need a manager so just proxy reads directly
282
+ // For primitives, we don't need a manager so just proxy reads directly
283
283
  descriptor.get = function (this: Struct) {
284
284
  if (access.mayRead(this[Internal.session], this[Internal.reference].location)) {
285
285
  const struct = this[Internal.reference].value as Val.Dynamic;
@@ -310,7 +310,7 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
310
310
  }
311
311
  };
312
312
  } else {
313
- // For collections we create a managed value
313
+ // For collections, we create a managed value
314
314
  let cloneContainer: (container: Val) => Val;
315
315
  switch (schema.effectiveMetatype) {
316
316
  case Metatype.array:
@@ -373,6 +373,8 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
373
373
  return defaultReader?.(this);
374
374
  }
375
375
 
376
+ // Value is null or a dynamic property, so just return it
377
+ // TODO Consider to also use Management for dynamic properties
376
378
  if (value === null) {
377
379
  return value;
378
380
  }
@@ -394,7 +396,15 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
394
396
  };
395
397
 
396
398
  // Clone the container before write
397
- const ref = ManagedReference(this[Internal.reference], pk, name, id, assertWriteOk, cloneContainer);
399
+ const ref = ManagedReference(
400
+ this[Internal.reference],
401
+ pk,
402
+ name,
403
+ id,
404
+ assertWriteOk,
405
+ cloneContainer,
406
+ this[Internal.session],
407
+ );
398
408
 
399
409
  ref.owner = manage(ref, this[Internal.session]);
400
410
 
@@ -52,13 +52,9 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
52
52
  override initialize(): MaybePromise {
53
53
  this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges); // Enhanced Validation
54
54
  this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange); // Event handling for changes
55
- if (
56
- this.state.extension !== undefined &&
57
- this.events.extension$Changing !== undefined &&
58
- this.events.extension$Changed !== undefined
59
- ) {
60
- this.reactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges); // Enhanced Validation
61
- this.reactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange); // Event handling for changes
55
+ if (this.state.extension !== undefined) {
56
+ this.maybeReactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges); // Enhanced Validation
57
+ this.maybeReactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange); // Event handling for changes
62
58
  }
63
59
 
64
60
  const lifecycle = this.endpoint.lifecycle as NodeLifecycle;
@@ -85,15 +85,11 @@ export class GeneralDiagnosticsServer extends Base {
85
85
  this.reactTo(lifecycle.online, this.#online, { lock: true });
86
86
  this.reactTo(lifecycle.goingOffline, this.#goingOffline, { lock: true });
87
87
 
88
- if (this.events.activeHardwareFaults$Changed !== undefined) {
89
- this.reactTo(this.events.activeHardwareFaults$Changed, this.#triggerActiveHardwareFaultsChangedEvent);
90
- }
91
- if (this.events.activeRadioFaults$Changed !== undefined) {
92
- this.reactTo(this.events.activeRadioFaults$Changed, this.#triggerActiveRadioFaultsChangedEvent);
93
- }
94
- if (this.events.activeNetworkFaults$Changed !== undefined) {
95
- this.reactTo(this.events.activeNetworkFaults$Changed, this.#triggerActiveNetworkFaultsChangedEvent);
96
- }
88
+ this.maybeReactTo(this.events.activeHardwareFaults$Changed, this.#triggerActiveHardwareFaultsChangedEvent);
89
+
90
+ this.maybeReactTo(this.events.activeRadioFaults$Changed, this.#triggerActiveRadioFaultsChangedEvent);
91
+
92
+ this.maybeReactTo(this.events.activeNetworkFaults$Changed, this.#triggerActiveNetworkFaultsChangedEvent);
97
93
  }
98
94
 
99
95
  #validateTestEnabledKey(enableKey: Bytes) {
@@ -40,9 +40,9 @@ export class ServiceAreaBaseServer extends ServiceAreaBase {
40
40
  this.reactTo(this.events.supportedMaps$Changing, this.#assertSupportedMaps);
41
41
  this.#assertSelectedAreas(this.state.selectedAreas);
42
42
  this.reactTo(this.events.selectedAreas$Changing, this.#assertSelectedAreas);
43
- if (this.state.currentArea !== undefined && this.events.currentArea$Changing !== undefined) {
43
+ if (this.state.currentArea !== undefined) {
44
44
  this.#assertCurrentArea(this.state.currentArea);
45
- this.reactTo(this.events.currentArea$Changing, this.#assertCurrentArea);
45
+ this.maybeReactTo(this.events.currentArea$Changing, this.#assertCurrentArea);
46
46
  }
47
47
  if (this.state.progress !== undefined) {
48
48
  this.#assertProgress(this.state.progress);
@@ -0,0 +1,412 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { ActionContext, Behavior, ClusterBehavior, type ClusterState, ValueSupervisor } from "#behavior/index.js";
8
+ import { Thermostat } from "#clusters/thermostat";
9
+ import { Endpoint } from "#endpoint/Endpoint.js";
10
+ import { BasicSet, Environment, Environmental, InternalError, Logger, ObserverGroup, serialize } from "#general";
11
+ import { ClusterModel, DataModelPath } from "#model";
12
+ import {
13
+ AccessControl,
14
+ assertRemoteActor,
15
+ Fabric,
16
+ FabricManager,
17
+ hasRemoteActor,
18
+ PeerAddress,
19
+ Subject,
20
+ Val,
21
+ } from "#protocol";
22
+ import { AttributeId, NodeId, Status, StatusResponse, StatusResponseError } from "#types";
23
+ import { AtomicWriteState } from "./AtomicWriteState.js";
24
+
25
+ const logger = Logger.get("AtomicWriteHandler");
26
+
27
+ /**
28
+ * Handles atomic write handling according to Matter definitions.
29
+ * The implementation tries to be generic, but is currently only used by the Thermostat cluster, so the atomic write
30
+ * types are imported from there.
31
+ *
32
+ * The logic requires that the cluster behavior implements the following additional events as "pure Observable()" events,
33
+ * because the current implementation logic requires error thrown by the event handlers to signal validation failures to
34
+ * be thrown back to te emitter. This is not the case for official state events.
35
+ * * `${attributeName}$AtomicChanging` - emitted when an attribute is changed as part of an atomic write, before the value
36
+ * is actually changed. Receives the new value, the old value and the action context as parameters.
37
+ * * `${attributeName}$AtomicChanged` - emitted when an attribute is changed as part of an atomic write, after the value
38
+ * is actually changed. Receives the new value, the old value and the action context as parameters.
39
+ *
40
+ * TODO: Move out of thermostat behavior into a more generic behavior handler once used by other clusters too. Then we
41
+ * also need to adjust how it is handled.
42
+ * Proper solution might be to add the handling of the atomic Request command on interaction level and leave the
43
+ * transaction open until it is rolled back or committed. This might have side effects on other parts of the system though.
44
+ * So lets do that later when we have more clusters using it.
45
+ */
46
+ export class AtomicWriteHandler {
47
+ #observers = new ObserverGroup();
48
+ #pendingWrites = new BasicSet<AtomicWriteState>();
49
+
50
+ constructor(fabricManager: FabricManager) {
51
+ this.#observers.on(fabricManager.events.deleted, fabric => this.#handleFabricRemoval(fabric));
52
+ }
53
+
54
+ static [Environmental.create](env: Environment) {
55
+ const instance = new AtomicWriteHandler(env.get(FabricManager));
56
+ env.set(AtomicWriteHandler, instance);
57
+ return instance;
58
+ }
59
+
60
+ close() {
61
+ this.#observers.close();
62
+ for (const writeState of Array.from(this.#pendingWrites)) {
63
+ writeState.close();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Initializes an AtomicWrite state for the given request, context, endpoint and cluster.
69
+ * It also implements all relevant validation according to the Matter spec.
70
+ */
71
+ #initializeState<B extends Behavior.Type>(
72
+ { requestType, attributeRequests, timeout }: Thermostat.AtomicRequest,
73
+ context: ActionContext,
74
+ endpoint: Endpoint,
75
+ cluster: B,
76
+ ) {
77
+ if (!ClusterBehavior.is(cluster) || !cluster.schema) {
78
+ throw new InternalError("Cluster behavior expected for atomic write handler");
79
+ }
80
+
81
+ // Ensure we have a valid peer and so also associated fabric
82
+ const peerAddress = this.#assertValidPeer(context);
83
+
84
+ // Validate AttributeRequests
85
+ if (attributeRequests.length === 0) {
86
+ throw new StatusResponse.InvalidCommandError("No attribute requests provided");
87
+ }
88
+ const attributes = new Map<AttributeId, string>();
89
+ for (const attr of attributeRequests) {
90
+ const [attributeName, _] =
91
+ Object.entries((cluster as ClusterBehavior.Type).cluster.attributes).find(
92
+ ([_, { id }]) => id === attr,
93
+ ) ?? [];
94
+ if (attributeName === undefined || endpoint.stateOf(cluster.id)[attr] === undefined) {
95
+ throw new StatusResponse.InvalidCommandError(`Attribute ${attr} not supported by cluster`);
96
+ }
97
+ if (attributes.has(attr)) {
98
+ throw new StatusResponse.InvalidCommandError("Duplicate attribute in attribute requests");
99
+ }
100
+ attributes.set(attr, attributeName);
101
+ }
102
+
103
+ const existingState = this.#pendingWrites.find(
104
+ s =>
105
+ PeerAddress.is(s.peerAddress, peerAddress) &&
106
+ s.endpoint.number == endpoint.number &&
107
+ s.clusterId === (cluster as ClusterBehavior.Type).cluster.id,
108
+ );
109
+
110
+ if (requestType === Thermostat.RequestType.BeginWrite) {
111
+ if (timeout === undefined) {
112
+ throw new StatusResponse.InvalidCommandError("Timeout missing for BeginWrite request");
113
+ }
114
+
115
+ if (
116
+ existingState !== undefined &&
117
+ existingState.attributeRequests.some(attr => attributeRequests.includes(attr))
118
+ ) {
119
+ throw new StatusResponse.InvalidCommandError(
120
+ "An atomic write for at least one of the attributes is already in progress for this peer",
121
+ );
122
+ }
123
+
124
+ const initialValues: Val.Struct = {};
125
+ for (const attr of attributeRequests) {
126
+ initialValues[attr] = endpoint.stateOf(cluster.id)[attr];
127
+ }
128
+
129
+ const state = new AtomicWriteState(
130
+ peerAddress,
131
+ endpoint,
132
+ cluster.cluster.id,
133
+ attributeRequests,
134
+ timeout,
135
+ attributes,
136
+ initialValues,
137
+ );
138
+ this.#pendingWrites.add(state);
139
+ state.closed.on(() => void this.#pendingWrites.delete(state));
140
+ logger.debug("Added atomic write state:", state);
141
+ return state;
142
+ }
143
+ if (existingState === undefined) {
144
+ throw new StatusResponse.InvalidInStateError("No atomic write in progress for this peer");
145
+ }
146
+ if (
147
+ existingState.attributeRequests.length !== attributeRequests.length ||
148
+ !existingState.attributeRequests.every(attr => attributeRequests.includes(attr))
149
+ ) {
150
+ throw new StatusResponse.InvalidInStateError("Attribute requests do not match existing atomic write");
151
+ }
152
+ return existingState;
153
+ }
154
+
155
+ /**
156
+ * Implements the begin write logic for an atomic write.
157
+ */
158
+ beginWrite(
159
+ request: Thermostat.AtomicRequest,
160
+ context: ActionContext,
161
+ endpoint: Endpoint,
162
+ cluster: Behavior.Type,
163
+ ): Thermostat.AtomicResponse {
164
+ if (!hasRemoteActor(context)) {
165
+ throw new StatusResponse.InvalidCommandError("AtomicRequest requires a remote actor");
166
+ }
167
+ if (!ClusterBehavior.is(cluster) || !cluster.schema) {
168
+ throw new InternalError("Cluster behavior expected for atomic write handler");
169
+ }
170
+ let commandStatusCode = Status.Success;
171
+ const attributeStatus = request.attributeRequests.map(attr => {
172
+ let statusCode = Status.Success;
173
+ const attributeModel = (cluster.schema!.conformant as ClusterModel.Conformant).attributes.for(attr);
174
+ if (!attributeModel?.quality.atomic) {
175
+ statusCode = Status.InvalidAction;
176
+ } else if (this.#pendingWriteStateForAttribute(endpoint, cluster, attr) !== undefined) {
177
+ statusCode = Status.Busy;
178
+ } else {
179
+ const { writeLevel } = cluster.supervisor.get(attributeModel).access.limits;
180
+ const location = {
181
+ path: DataModelPath.none,
182
+ endpoint: endpoint.number,
183
+ cluster: cluster.cluster.id,
184
+ owningFabric: context.fabric,
185
+ };
186
+ if (context.authorityAt(writeLevel, location) !== AccessControl.Authority.Granted) {
187
+ statusCode = Status.UnsupportedAccess;
188
+ }
189
+ }
190
+
191
+ if (statusCode !== Status.Success) {
192
+ commandStatusCode = Status.Failure;
193
+ }
194
+ return {
195
+ attributeId: attr,
196
+ statusCode,
197
+ };
198
+ });
199
+
200
+ let timeout;
201
+ if (commandStatusCode === Status.Success) {
202
+ const state = this.#initializeState(request, context, endpoint, cluster);
203
+ timeout = state.timeout;
204
+ }
205
+
206
+ return {
207
+ statusCode: commandStatusCode,
208
+ attributeStatus,
209
+ timeout,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Handles writing a value for an attribute as part of an ongoing atomic write.
215
+ * It uses the *$AtomicChanging* event to trigger validation of the partial write.
216
+ */
217
+ writeAttribute(
218
+ context: ValueSupervisor.Session,
219
+ endpoint: Endpoint,
220
+ cluster: Behavior.Type,
221
+ attribute: AttributeId,
222
+ value: unknown,
223
+ ) {
224
+ const state = this.#assertPendingWriteForAttributeAndPeer(context, endpoint, cluster, attribute);
225
+ const attributeName = state.attributeNames.get(attribute)!;
226
+ logger.debug(`Writing pending value for attribute ${attributeName}, ${attribute} in atomic write`, value);
227
+ // TODO currently we only handle this one changing, so checking other state within the event potentially use
228
+ // older values. We need to tweak the state for a complete solution. But ok for now!
229
+ endpoint
230
+ .eventsOf(cluster.id)
231
+ [
232
+ `${attributeName}$AtomicChanging`
233
+ ]?.emit(value, state.pendingAttributeValues[attribute] !== undefined ? state.pendingAttributeValues[attribute] : state.initialValues[attribute], context);
234
+ state.pendingAttributeValues[attribute] = value;
235
+ logger.debug("Atomic write state after current write:", state);
236
+ }
237
+
238
+ /**
239
+ * Implements the commit logic for an atomic write.
240
+ */
241
+ async commitWrite<B extends Behavior.Type>(
242
+ request: Thermostat.AtomicRequest,
243
+ context: ActionContext,
244
+ endpoint: Endpoint,
245
+ cluster: B,
246
+ clusterState: ClusterState.Type<any, B>,
247
+ ): Promise<Thermostat.AtomicResponse> {
248
+ const state = this.#initializeState(request, context, endpoint, cluster);
249
+
250
+ let commandStatusCode = Status.Success;
251
+ const attributeStatus = [];
252
+ for (const [attr, value] of Object.entries(state.pendingAttributeValues)) {
253
+ let statusCode = Status.Success;
254
+ try {
255
+ const attributeName = state.attributeNames.get(AttributeId(Number(attr)))!;
256
+ endpoint
257
+ .eventsOf(cluster.id)
258
+ [`${attributeName}$AtomicChanging`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
259
+ endpoint
260
+ .eventsOf(cluster.id)
261
+ [`${attributeName}$AtomicChanged`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
262
+ (clusterState as any)[attr] = value;
263
+ await context.transaction?.commit();
264
+ } catch (error) {
265
+ await context.transaction?.rollback();
266
+ logger.info(`Failed to write attribute ${attr} during atomic write commit: ${error}`);
267
+ statusCode = error instanceof StatusResponseError ? error.code : Status.Failure;
268
+ // If one fails with ConstraintError, the whole command should return ConstraintError, otherwise Failure
269
+ commandStatusCode =
270
+ commandStatusCode === Status.Failure
271
+ ? Status.Failure
272
+ : commandStatusCode === Status.ConstraintError
273
+ ? Status.ConstraintError
274
+ : Status.Failure;
275
+ }
276
+ attributeStatus.push({
277
+ attributeId: AttributeId(Number(attr)),
278
+ statusCode,
279
+ });
280
+ }
281
+ state.close(); // Irrelevant of the outcome the state is closed
282
+ return {
283
+ statusCode: commandStatusCode,
284
+ attributeStatus,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Implements the rollback logic for an atomic write.
290
+ */
291
+ rollbackWrite(
292
+ request: Thermostat.AtomicRequest,
293
+ context: ActionContext,
294
+ endpoint: Endpoint,
295
+ cluster: Behavior.Type,
296
+ ): Thermostat.AtomicResponse {
297
+ const state = this.#initializeState(request, context, endpoint, cluster);
298
+ state.close();
299
+ return {
300
+ statusCode: Status.Success,
301
+ attributeStatus: state.attributeRequests.map(attr => ({
302
+ attributeId: attr,
303
+ statusCode: Status.Success,
304
+ })),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Handles fabric removal by closing all pending atomic write states for peers on the removed fabric.
310
+ */
311
+ #handleFabricRemoval(fabric: Fabric) {
312
+ const fabricIndex = fabric.fabricIndex;
313
+ for (const writeState of Array.from(this.#pendingWrites)) {
314
+ if (writeState.peerAddress.fabricIndex === fabricIndex) {
315
+ logger.debug(
316
+ `Closing atomic write state for peer ${writeState.peerAddress.toString()} on endpoint ${writeState.endpoint.id} due to fabric removal`,
317
+ );
318
+ writeState.close();
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Returns the pending write state for the given attribute, if any.
325
+ */
326
+ #pendingWriteStateForAttribute(endpoint: Endpoint, cluster: Behavior.Type, attribute: AttributeId) {
327
+ const writeStates = this.#pendingWrites.filter(
328
+ s => s.endpoint.number == endpoint.number && s.clusterId === (cluster as ClusterBehavior.Type).cluster.id,
329
+ );
330
+ if (writeStates.length === 0) {
331
+ return undefined;
332
+ }
333
+ const attrWriteStates = writeStates.filter(({ attributeRequests }) => attributeRequests.includes(attribute));
334
+ if (attrWriteStates.length === 0) {
335
+ return undefined;
336
+ }
337
+ if (attrWriteStates.length > 1) {
338
+ throw new InternalError("Multiple atomic write states found for the same attribute. Should never happen");
339
+ }
340
+ return attrWriteStates[0];
341
+ }
342
+
343
+ /**
344
+ * Returns the pending value for the given attribute and peer, if any.
345
+ */
346
+ pendingValueForAttributeAndPeer(
347
+ context: ValueSupervisor.Session,
348
+ endpoint: Endpoint,
349
+ cluster: Behavior.Type,
350
+ attribute: AttributeId,
351
+ ) {
352
+ const peerAddress = this.#derivePeerAddress(context);
353
+ if (peerAddress === undefined) {
354
+ // No valid peer address could be derived from the session
355
+ return undefined;
356
+ }
357
+ const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
358
+ if (attrWriteState === undefined) {
359
+ // No pending write for this attribute
360
+ return undefined;
361
+ }
362
+ if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
363
+ // Pending state s for an other peer
364
+ return undefined;
365
+ }
366
+ logger.debug(
367
+ `Found pending value for attribute ${attribute} for peer ${peerAddress.nodeId}`,
368
+ serialize(attrWriteState.pendingAttributeValues[attribute]),
369
+ );
370
+ return attrWriteState.pendingAttributeValues[attribute];
371
+ }
372
+
373
+ #assertPendingWriteForAttributeAndPeer(
374
+ session: ValueSupervisor.Session,
375
+ endpoint: Endpoint,
376
+ cluster: Behavior.Type,
377
+ attribute: AttributeId,
378
+ ) {
379
+ const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
380
+ if (attrWriteState === undefined) {
381
+ throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this attribute");
382
+ }
383
+ const peerAddress = this.#derivePeerAddress(session);
384
+ if (peerAddress === undefined) {
385
+ throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this peer");
386
+ }
387
+ if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
388
+ throw new StatusResponse.BusyError("Attribute is part of an atomic write in progress for a different peer");
389
+ }
390
+ return attrWriteState;
391
+ }
392
+
393
+ #derivePeerAddress(session: ValueSupervisor.Session) {
394
+ if (
395
+ hasRemoteActor(session) &&
396
+ Subject.isNode(session.subject) &&
397
+ NodeId.isOperationalNodeId(session.subject.id)
398
+ ) {
399
+ return PeerAddress({ fabricIndex: session.fabric, nodeId: NodeId(session.subject.id) });
400
+ }
401
+ }
402
+
403
+ #assertValidPeer(context: ActionContext) {
404
+ assertRemoteActor(context); // Also validate that we have a remote actor for processing the command
405
+
406
+ const peerAddress = this.#derivePeerAddress(context);
407
+ if (!context.session.associatedFabric || peerAddress === undefined) {
408
+ throw new StatusResponse.InvalidCommandError("AtomicRequest requires an operational session");
409
+ }
410
+ return peerAddress;
411
+ }
412
+ }