@matter/node 0.17.3 → 0.17.4-alpha.0-20260623-5647634fc

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 (47) hide show
  1. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts +10 -2
  2. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -1
  3. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js +31 -23
  4. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js.map +1 -1
  5. package/dist/cjs/behavior/state/managed/Datasource.d.ts +9 -0
  6. package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
  7. package/dist/cjs/behavior/state/managed/Datasource.js +3 -0
  8. package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
  9. package/dist/cjs/node/client/ClientStructure.d.ts.map +1 -1
  10. package/dist/cjs/node/client/ClientStructure.js +3 -3
  11. package/dist/cjs/node/client/ClientStructure.js.map +1 -1
  12. package/dist/cjs/node/server/ServerEndpointInitializer.js +1 -1
  13. package/dist/cjs/node/server/ServerEndpointInitializer.js.map +1 -1
  14. package/dist/cjs/storage/client/DatasourceCache.d.ts +5 -0
  15. package/dist/cjs/storage/client/DatasourceCache.d.ts.map +1 -1
  16. package/dist/cjs/storage/client/DatasourceCache.js +9 -2
  17. package/dist/cjs/storage/client/DatasourceCache.js.map +1 -1
  18. package/dist/cjs/storage/server/ServerNodeStore.d.ts.map +1 -1
  19. package/dist/cjs/storage/server/ServerNodeStore.js +3 -1
  20. package/dist/cjs/storage/server/ServerNodeStore.js.map +1 -1
  21. package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts +10 -2
  22. package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -1
  23. package/dist/esm/behavior/cluster/FabricScopedDataHandler.js +31 -23
  24. package/dist/esm/behavior/cluster/FabricScopedDataHandler.js.map +1 -1
  25. package/dist/esm/behavior/state/managed/Datasource.d.ts +9 -0
  26. package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
  27. package/dist/esm/behavior/state/managed/Datasource.js +3 -0
  28. package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
  29. package/dist/esm/node/client/ClientStructure.d.ts.map +1 -1
  30. package/dist/esm/node/client/ClientStructure.js +3 -3
  31. package/dist/esm/node/client/ClientStructure.js.map +1 -1
  32. package/dist/esm/node/server/ServerEndpointInitializer.js +1 -1
  33. package/dist/esm/node/server/ServerEndpointInitializer.js.map +1 -1
  34. package/dist/esm/storage/client/DatasourceCache.d.ts +5 -0
  35. package/dist/esm/storage/client/DatasourceCache.d.ts.map +1 -1
  36. package/dist/esm/storage/client/DatasourceCache.js +9 -2
  37. package/dist/esm/storage/client/DatasourceCache.js.map +1 -1
  38. package/dist/esm/storage/server/ServerNodeStore.d.ts.map +1 -1
  39. package/dist/esm/storage/server/ServerNodeStore.js +3 -1
  40. package/dist/esm/storage/server/ServerNodeStore.js.map +1 -1
  41. package/package.json +6 -6
  42. package/src/behavior/cluster/FabricScopedDataHandler.ts +58 -34
  43. package/src/behavior/state/managed/Datasource.ts +15 -0
  44. package/src/node/client/ClientStructure.ts +3 -4
  45. package/src/node/server/ServerEndpointInitializer.ts +1 -1
  46. package/src/storage/client/DatasourceCache.ts +12 -2
  47. package/src/storage/server/ServerNodeStore.ts +2 -0
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Behavior } from "#behavior/Behavior.js";
8
+ import type { Agent } from "#endpoint/Agent.js";
8
9
  import type { Endpoint } from "#endpoint/Endpoint.js";
9
10
  import type { SupportedElements } from "#endpoint/properties/Behaviors.js";
10
11
  import type { ServerNode } from "#node/ServerNode.js";
@@ -34,18 +35,15 @@ async function forBehaviors(
34
35
  }
35
36
 
36
37
  /**
37
- * Sanitize fabric-scoped attributes for a given endpoint and cluster.
38
- * The changed state is returned in an array of promises and should be awaited to ensure the state is updated.
38
+ * Compute a state patch that strips fabric-scoped attribute entries no longer belonging to an allowed fabric.
39
+ * Returns `undefined` when nothing needs to change.
39
40
  */
40
- async function sanitizeAttributeData(
41
+ function computeFabricScopedUpdate(
41
42
  endpoint: Endpoint,
42
43
  type: Behavior.Type,
43
- cluster: ClusterType,
44
44
  supportedAttributes: Set<string>,
45
45
  allowedIndices: FabricIndex[],
46
- ) {
47
- const stateUpdatePromises = new Array<Promise<void>>();
48
-
46
+ ): Val.Struct | undefined {
49
47
  // Build a set of fabric-scoped attribute names from the schema
50
48
  const fabricScopedAttrs = new Set<string>();
51
49
  const schema = Schema(type) as ClusterModel;
@@ -58,11 +56,11 @@ async function sanitizeAttributeData(
58
56
  }
59
57
 
60
58
  const stateUpdate = {} as Val.Struct;
61
- // Iterate over all attributes and check if they are fabric-scoped
59
+ const state = endpoint.stateOf(type) as Val.Struct;
62
60
  for (const attributeName of supportedAttributes) {
63
61
  if (fabricScopedAttrs.has(attributeName)) {
64
- const value = (endpoint.stateOf(type) as Val.Struct)[attributeName];
65
- // If the value contains data for the fabric being removed, remove the data
62
+ const value = state[attributeName];
63
+ // If the value contains data for a fabric no longer present, remove that data
66
64
  if (Array.isArray(value) && value.length > 0) {
67
65
  const filtered = value.filter(entry => allowedIndices.includes(entry.fabricIndex));
68
66
  if (filtered.length !== value.length) {
@@ -71,20 +69,38 @@ async function sanitizeAttributeData(
71
69
  }
72
70
  }
73
71
  }
74
- // If we have any state updates for this behavior, we need to set the state.
75
- // Errors are being logged and ignored
76
- if (Object.keys(stateUpdate).length > 0) {
77
- const { resolver, promise } = createPromise<void>();
78
- (endpoint.eventsOf(type) as Behavior.EventsOf<any>).stateChanged?.on(resolver);
79
- try {
80
- await endpoint.setStateOf(type, stateUpdate);
81
- stateUpdatePromises.push(withTimeout(Seconds(5), promise)); // 5s should be enough for state change
82
- } catch (error) {
83
- logger.warn(
84
- `Could not sanitize fabric-scoped attributes for cluster ${cluster.name} on endpoint ${endpoint.id}`,
85
- error,
86
- );
87
- }
72
+ return Object.keys(stateUpdate).length > 0 ? stateUpdate : undefined;
73
+ }
74
+
75
+ /**
76
+ * Sanitize fabric-scoped attributes for an already-active endpoint via {@link Endpoint.setStateOf}.
77
+ * The changed state is returned in an array of promises and should be awaited to ensure the state is updated.
78
+ */
79
+ async function sanitizeAttributeData(
80
+ endpoint: Endpoint,
81
+ type: Behavior.Type,
82
+ cluster: ClusterType,
83
+ supportedAttributes: Set<string>,
84
+ allowedIndices: FabricIndex[],
85
+ ) {
86
+ const stateUpdate = computeFabricScopedUpdate(endpoint, type, supportedAttributes, allowedIndices);
87
+ if (stateUpdate === undefined) {
88
+ return [];
89
+ }
90
+
91
+ const stateUpdatePromises = new Array<Promise<void>>();
92
+ // Best-effort during online fabric removal: a per-cluster failure must not abort the removal already in progress.
93
+ // The startup path (limitEndpointAttributeDataToAllowedFabrics) instead lets failures throw.
94
+ const { resolver, promise } = createPromise<void>();
95
+ (endpoint.eventsOf(type) as Behavior.EventsOf<any>).stateChanged?.on(resolver);
96
+ try {
97
+ await endpoint.setStateOf(type, stateUpdate);
98
+ stateUpdatePromises.push(withTimeout(Seconds(5), promise)); // 5s should be enough for state change
99
+ } catch (error) {
100
+ logger.warn(
101
+ `Could not sanitize fabric-scoped attributes for cluster ${cluster.name} on endpoint ${endpoint.id}`,
102
+ error,
103
+ );
88
104
  }
89
105
 
90
106
  return stateUpdatePromises;
@@ -151,16 +167,24 @@ export async function limitNodeDataToAllowedFabrics(node: ServerNode, allowedInd
151
167
  }
152
168
  }
153
169
 
154
- export async function limitEndpointAttributeDataToAllowedFabrics(endpoint: Endpoint, allowedIndices: FabricIndex[]) {
155
- const stateUpdatePromises = new Array<Promise<void>>();
156
- await forBehaviors(endpoint, async (type, cluster, elements) => {
157
- if (elements.attributes.size) {
158
- stateUpdatePromises.push(
159
- ...(await sanitizeAttributeData(endpoint, type, cluster, elements.attributes, allowedIndices)),
160
- );
170
+ /**
171
+ * Sanitize fabric-scoped attributes for a single endpoint while it is still initializing.
172
+ *
173
+ * Invoked from {@link EndpointInitializer.behaviorsInitialized}, which runs inside the endpoint's construction
174
+ * context. The endpoint is not yet active, so {@link Endpoint.setStateOf} (which opens a fresh action that asserts an
175
+ * active endpoint) would throw. Instead we patch state through the supplied {@link Agent}, whose context is the
176
+ * construction transaction that commits when initialization completes.
177
+ */
178
+ export async function limitEndpointAttributeDataToAllowedFabrics(agent: Agent, allowedIndices: FabricIndex[]) {
179
+ await forBehaviors(agent.endpoint, async (type, _cluster, elements) => {
180
+ if (!elements.attributes.size) {
181
+ return;
182
+ }
183
+ const stateUpdate = computeFabricScopedUpdate(agent.endpoint, type, elements.attributes, allowedIndices);
184
+ if (stateUpdate === undefined) {
185
+ return;
161
186
  }
187
+ const behavior = agent.get(type);
188
+ behavior.type.supervisor.patch(stateUpdate, behavior.state, agent.endpoint.path);
162
189
  });
163
-
164
- // Wait for all state changed to be executed before processing events
165
- await Promise.allSettled(stateUpdatePromises);
166
190
  }
@@ -192,6 +192,12 @@ export namespace Datasource {
192
192
  */
193
193
  consumer?: ExternallyMutableStore.Consumer;
194
194
 
195
+ /**
196
+ * Current values, preferring the live consumer over {@link Store.initialValues}. Reflects up-to-date
197
+ * data while a consumer is attached, where {@link Store.initialValues} may be absent.
198
+ */
199
+ currentValues?: Val.Struct;
200
+
195
201
  /**
196
202
  * The current version of the data.
197
203
  */
@@ -213,6 +219,11 @@ export namespace Datasource {
213
219
  */
214
220
  readValues(keys: Set<string>): Val.Struct;
215
221
 
222
+ /**
223
+ * Read a non-destructive copy of all current values.
224
+ */
225
+ snapshot(): Val.Struct;
226
+
216
227
  /**
217
228
  * Release all values from the datasource, transferring ownership back to the store.
218
229
  */
@@ -557,6 +568,10 @@ class DatasourceImpl implements Datasource, Datasource.ExternallyMutableStore.Co
557
568
  return result;
558
569
  }
559
570
 
571
+ snapshot() {
572
+ return { ...this.#values };
573
+ }
574
+
560
575
  releaseValues() {
561
576
  const { values } = this;
562
577
  this.values = {};
@@ -501,9 +501,8 @@ export class ClientStructure {
501
501
 
502
502
  // Generate a behavior if enough information is available
503
503
  if (cluster.behavior === undefined) {
504
- if (cluster.store.initialValues) {
505
- const values = cluster.store.initialValues;
506
-
504
+ const values = cluster.store.currentValues;
505
+ if (values) {
507
506
  const clusterRevision = getStoreValue(values, ClusterRevision.id, "clusterRevision");
508
507
  const features = getStoreValue(values, FeatureMap.id, "featureMap");
509
508
  const attributeList = getStoreValue(values, AttributeList.id, "attributeList");
@@ -579,7 +578,7 @@ export class ClientStructure {
579
578
  if (cluster.behavior && endpoint.behaviors.isActive(cluster.behavior.id)) {
580
579
  attrs = endpoint.stateOf(cluster.behavior);
581
580
  } else {
582
- attrs = cluster.store.initialValues ?? {};
581
+ attrs = cluster.store.currentValues ?? {};
583
582
  }
584
583
  this.#synchronizeDescriptor(structure, attrs);
585
584
  }
@@ -116,7 +116,7 @@ export class ServerEndpointInitializer extends EndpointInitializer {
116
116
  if (agent.env.has(FabricManager)) {
117
117
  const fabricIndices = agent.env.get(FabricManager).fabrics.map(fabric => fabric.fabricIndex);
118
118
  if (fabricIndices.length > 0) {
119
- return limitEndpointAttributeDataToAllowedFabrics(agent.endpoint, fabricIndices);
119
+ return limitEndpointAttributeDataToAllowedFabrics(agent, fabricIndices);
120
120
  }
121
121
  }
122
122
  }
@@ -52,9 +52,20 @@ export class DatasourceCache implements Datasource.ExternallyMutableStore, Remot
52
52
  this.#consumer = consumer;
53
53
  if (consumer !== undefined) {
54
54
  this.#reclaimed = false;
55
+
56
+ // Consumer owns the values while attached; drop our seed copy so we don't shadow it.
57
+ this.initialValues = undefined;
55
58
  }
56
59
  }
57
60
 
61
+ /**
62
+ * Current values, preferring the live consumer over the seed snapshot, so cluster structure can rebuild from
63
+ * up-to-date data while a behavior is active.
64
+ */
65
+ get currentValues(): Val.Struct | undefined {
66
+ return this.#consumer && !this.#reclaimed ? this.#consumer.snapshot() : this.initialValues;
67
+ }
68
+
58
69
  async set(transaction: Transaction, values: Val.Struct) {
59
70
  let participant = transaction.getParticipant(this.#writer);
60
71
  if (participant === undefined) {
@@ -126,8 +137,7 @@ export class DatasourceCache implements Datasource.ExternallyMutableStore, Remot
126
137
  if (!this.initialValues) {
127
138
  this.initialValues = {};
128
139
  }
129
- const valuesStruct = Object.fromEntries(values) as Val.Struct;
130
- Object.assign(this.initialValues, valuesStruct);
140
+ Object.assign(this.initialValues, Object.fromEntries(values));
131
141
  }
132
142
  }
133
143
 
@@ -105,6 +105,8 @@ export class ServerNodeStore extends NodeStore implements Destructable {
105
105
  Diagnostic.dict({
106
106
  location: root?.path ?? "(unknown location)",
107
107
  driver: this.#storageManager?.driverId ?? "unknown",
108
+ // Only meaningful on close, where a non-zero count means a holder did not release
109
+ ...(what === "Closed" && root ? { locks: root.lockCount } : {}),
108
110
  }),
109
111
  );
110
112
  }