@matter/node 0.16.0-alpha.0-20251110-c4c70a41b → 0.16.0-alpha.0-20251111-11cc8c3bd
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/cjs/behavior/internal/BehaviorBacking.d.ts +2 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.d.ts.map +1 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.js +11 -3
- package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/cjs/behavior/internal/ClientBehaviorBacking.d.ts +2 -0
- package/dist/cjs/behavior/internal/ClientBehaviorBacking.d.ts.map +1 -1
- package/dist/cjs/behavior/internal/ClientBehaviorBacking.js +4 -0
- package/dist/cjs/behavior/internal/ClientBehaviorBacking.js.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.d.ts +4 -0
- package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.js +5 -0
- package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/cjs/endpoint/properties/Behaviors.d.ts +12 -2
- package/dist/cjs/endpoint/properties/Behaviors.d.ts.map +1 -1
- package/dist/cjs/endpoint/properties/Behaviors.js +44 -10
- package/dist/cjs/endpoint/properties/Behaviors.js.map +2 -2
- package/dist/cjs/node/client/ClientStructure.d.ts +31 -1
- package/dist/cjs/node/client/ClientStructure.d.ts.map +1 -1
- package/dist/cjs/node/client/ClientStructure.js +257 -81
- package/dist/cjs/node/client/ClientStructure.js.map +1 -1
- package/dist/cjs/node/client/ClientStructureEvents.d.ts +5 -2
- package/dist/cjs/node/client/ClientStructureEvents.d.ts.map +1 -1
- package/dist/cjs/node/client/ClientStructureEvents.js +38 -2
- package/dist/cjs/node/client/ClientStructureEvents.js.map +1 -1
- package/dist/cjs/node/client/Peers.d.ts +3 -0
- package/dist/cjs/node/client/Peers.d.ts.map +1 -1
- package/dist/cjs/node/client/Peers.js +3 -0
- package/dist/cjs/node/client/Peers.js.map +1 -1
- package/dist/cjs/node/server/ServerEndpointInitializer.js +1 -1
- package/dist/cjs/node/server/ServerEndpointInitializer.js.map +1 -1
- package/dist/cjs/storage/EndpointStore.d.ts +5 -1
- package/dist/cjs/storage/EndpointStore.d.ts.map +1 -1
- package/dist/cjs/storage/EndpointStore.js +7 -1
- package/dist/cjs/storage/EndpointStore.js.map +1 -1
- package/dist/cjs/storage/client/ClientEndpointStore.d.ts +2 -1
- package/dist/cjs/storage/client/ClientEndpointStore.d.ts.map +1 -1
- package/dist/cjs/storage/client/DatasourceCache.d.ts +14 -3
- package/dist/cjs/storage/client/DatasourceCache.d.ts.map +1 -1
- package/dist/cjs/storage/client/DatasourceCache.js +10 -0
- package/dist/cjs/storage/client/DatasourceCache.js.map +1 -1
- package/dist/esm/behavior/internal/BehaviorBacking.d.ts +2 -1
- package/dist/esm/behavior/internal/BehaviorBacking.d.ts.map +1 -1
- package/dist/esm/behavior/internal/BehaviorBacking.js +11 -3
- package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/esm/behavior/internal/ClientBehaviorBacking.d.ts +2 -0
- package/dist/esm/behavior/internal/ClientBehaviorBacking.d.ts.map +1 -1
- package/dist/esm/behavior/internal/ClientBehaviorBacking.js +4 -0
- package/dist/esm/behavior/internal/ClientBehaviorBacking.js.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.d.ts +4 -0
- package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.js +5 -0
- package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/esm/endpoint/properties/Behaviors.d.ts +12 -2
- package/dist/esm/endpoint/properties/Behaviors.d.ts.map +1 -1
- package/dist/esm/endpoint/properties/Behaviors.js +44 -10
- package/dist/esm/endpoint/properties/Behaviors.js.map +2 -2
- package/dist/esm/node/client/ClientStructure.d.ts +31 -1
- package/dist/esm/node/client/ClientStructure.d.ts.map +1 -1
- package/dist/esm/node/client/ClientStructure.js +258 -82
- package/dist/esm/node/client/ClientStructure.js.map +1 -1
- package/dist/esm/node/client/ClientStructureEvents.d.ts +5 -2
- package/dist/esm/node/client/ClientStructureEvents.d.ts.map +1 -1
- package/dist/esm/node/client/ClientStructureEvents.js +39 -3
- package/dist/esm/node/client/ClientStructureEvents.js.map +1 -1
- package/dist/esm/node/client/Peers.d.ts +3 -0
- package/dist/esm/node/client/Peers.d.ts.map +1 -1
- package/dist/esm/node/client/Peers.js +3 -0
- package/dist/esm/node/client/Peers.js.map +1 -1
- package/dist/esm/node/server/ServerEndpointInitializer.js +1 -1
- package/dist/esm/node/server/ServerEndpointInitializer.js.map +1 -1
- package/dist/esm/storage/EndpointStore.d.ts +5 -1
- package/dist/esm/storage/EndpointStore.d.ts.map +1 -1
- package/dist/esm/storage/EndpointStore.js +7 -1
- package/dist/esm/storage/EndpointStore.js.map +1 -1
- package/dist/esm/storage/client/ClientEndpointStore.d.ts +2 -1
- package/dist/esm/storage/client/ClientEndpointStore.d.ts.map +1 -1
- package/dist/esm/storage/client/DatasourceCache.d.ts +14 -3
- package/dist/esm/storage/client/DatasourceCache.d.ts.map +1 -1
- package/dist/esm/storage/client/DatasourceCache.js +10 -0
- package/dist/esm/storage/client/DatasourceCache.js.map +1 -1
- package/package.json +7 -7
- package/src/behavior/internal/BehaviorBacking.ts +16 -3
- package/src/behavior/internal/ClientBehaviorBacking.ts +10 -1
- package/src/behavior/state/managed/Datasource.ts +13 -0
- package/src/endpoint/properties/Behaviors.ts +53 -12
- package/src/node/client/ClientStructure.ts +352 -91
- package/src/node/client/ClientStructureEvents.ts +55 -7
- package/src/node/client/Peers.ts +4 -0
- package/src/node/server/ServerEndpointInitializer.ts +1 -1
- package/src/storage/EndpointStore.ts +8 -1
- package/src/storage/client/DatasourceCache.ts +28 -4
|
@@ -11,7 +11,7 @@ import { Descriptor } from "#clusters/descriptor";
|
|
|
11
11
|
import { Endpoint } from "#endpoint/Endpoint.js";
|
|
12
12
|
import { EndpointType } from "#endpoint/type/EndpointType.js";
|
|
13
13
|
import { RootEndpoint } from "#endpoints/root";
|
|
14
|
-
import { InternalError, Logger } from "#general";
|
|
14
|
+
import { Diagnostic, InternalError, isDeepEqual, Logger } from "#general";
|
|
15
15
|
import {
|
|
16
16
|
AcceptedCommandList,
|
|
17
17
|
AttributeList,
|
|
@@ -43,20 +43,21 @@ const PARTS_LIST_ATTR_ID = Descriptor.Cluster.attributes.partsList.id;
|
|
|
43
43
|
*/
|
|
44
44
|
export class ClientStructure {
|
|
45
45
|
#nodeStore: ClientNodeStore;
|
|
46
|
-
#endpoints
|
|
46
|
+
#endpoints = new Map<EndpointNumber, EndpointStructure>();
|
|
47
47
|
#emitEvent: ClientEventEmitter;
|
|
48
48
|
#node: ClientNode;
|
|
49
49
|
#subscribedFabricFiltered?: boolean;
|
|
50
|
-
#
|
|
50
|
+
#pendingChanges = new Map<EndpointStructure, PendingChange>();
|
|
51
|
+
#pendingEvents = Array<PendingEvent>();
|
|
51
52
|
#events: ClientStructureEvents;
|
|
52
53
|
|
|
53
54
|
constructor(node: ClientNode) {
|
|
54
55
|
this.#node = node;
|
|
55
56
|
this.#nodeStore = node.env.get(ClientNodeStore);
|
|
56
|
-
this.#endpoints
|
|
57
|
+
this.#endpoints.set(node.number, {
|
|
57
58
|
endpoint: node,
|
|
58
|
-
clusters:
|
|
59
|
-
};
|
|
59
|
+
clusters: new Map(),
|
|
60
|
+
});
|
|
60
61
|
this.#emitEvent = ClientEventEmitter(node, this);
|
|
61
62
|
this.#events = this.#node.env.get(ClientStructureEvents);
|
|
62
63
|
}
|
|
@@ -88,18 +89,21 @@ export class ClientStructure {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
default:
|
|
100
|
-
throw new InternalError(`Unexpected ${opcode} operation in initial hierarchy load`);
|
|
92
|
+
const changes = this.#pendingChanges;
|
|
93
|
+
this.#pendingChanges = new Map();
|
|
94
|
+
for (const [structure, change] of changes.entries()) {
|
|
95
|
+
// Only installs should be queued
|
|
96
|
+
if (!change.install || change.erase || change.rebuild) {
|
|
97
|
+
throw new InternalError(
|
|
98
|
+
`Unexpected erase and/or rebuild during initialization of ${structure.endpoint}`,
|
|
99
|
+
);
|
|
101
100
|
}
|
|
101
|
+
|
|
102
|
+
this.#pendingChanges.delete(structure);
|
|
103
|
+
this.#install(structure);
|
|
102
104
|
}
|
|
105
|
+
|
|
106
|
+
this.#emitPendingEvents();
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/**
|
|
@@ -131,11 +135,11 @@ export class ClientStructure {
|
|
|
131
135
|
for (const {
|
|
132
136
|
endpoint: { number: endpointId },
|
|
133
137
|
clusters,
|
|
134
|
-
} of
|
|
138
|
+
} of this.#endpoints.values()) {
|
|
135
139
|
for (const {
|
|
136
140
|
id: clusterId,
|
|
137
141
|
store: { version },
|
|
138
|
-
} of
|
|
142
|
+
} of clusters.values()) {
|
|
139
143
|
if (!scope.isRelevant(endpointId, clusterId)) {
|
|
140
144
|
continue;
|
|
141
145
|
}
|
|
@@ -196,25 +200,25 @@ export class ClientStructure {
|
|
|
196
200
|
|
|
197
201
|
// We don't apply structural changes until we've processed all attribute data if a.) listeners might otherwise
|
|
198
202
|
// see partially initialized endpoints, or b.) the change requires an async operation
|
|
199
|
-
for (const [endpoint,
|
|
200
|
-
this.#
|
|
203
|
+
for (const [endpoint, change] of this.#pendingChanges.entries()) {
|
|
204
|
+
this.#pendingChanges.delete(endpoint);
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
if (change.erase) {
|
|
207
|
+
await this.#erase(endpoint);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
206
210
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
logger.error(`Error erasing peer endpoint ${endpoint.endpoint}:`, e);
|
|
214
|
-
}
|
|
215
|
-
break;
|
|
211
|
+
if (change.rebuild) {
|
|
212
|
+
await this.#rebuild(endpoint);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (change.install) {
|
|
216
|
+
this.#install(endpoint);
|
|
216
217
|
}
|
|
217
218
|
}
|
|
219
|
+
|
|
220
|
+
// Likewise, we don't emit events until we've applied all structural changes
|
|
221
|
+
this.#emitPendingEvents();
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
/** Reference to the default subscription used when the node was started. */
|
|
@@ -285,7 +289,7 @@ export class ClientStructure {
|
|
|
285
289
|
* Obtain the {@link Endpoint} for a {@link EndpointNumber}.
|
|
286
290
|
*/
|
|
287
291
|
endpointFor(endpoint: EndpointNumber): Endpoint | undefined {
|
|
288
|
-
return this.#endpoints
|
|
292
|
+
return this.#endpoints.get(endpoint)?.endpoint;
|
|
289
293
|
}
|
|
290
294
|
|
|
291
295
|
/**
|
|
@@ -294,9 +298,27 @@ export class ClientStructure {
|
|
|
294
298
|
* This is invoked in a batch when we've collected all sequential values for the current endpoint/cluster.
|
|
295
299
|
*/
|
|
296
300
|
async #updateCluster(attrs: AttributeUpdates) {
|
|
297
|
-
// TODO: Detect changes in revision/features/attributes/commands and update behavior if needed
|
|
298
301
|
const endpoint = this.#endpointFor(attrs.endpointId);
|
|
299
302
|
const cluster = this.#clusterFor(endpoint, attrs.clusterId);
|
|
303
|
+
|
|
304
|
+
if (cluster.behavior && FeatureMap.id in attrs.values) {
|
|
305
|
+
if (!isDeepEqual(cluster.features, attrs.values[FeatureMap.id])) {
|
|
306
|
+
cluster.behavior = undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (cluster.behavior && AttributeList.id in attrs.values) {
|
|
311
|
+
if (!isDeepEqual(cluster.attributes, attrs.values[AttributeList.id])) {
|
|
312
|
+
cluster.behavior = undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (cluster.behavior && AcceptedCommandList.id in attrs.values) {
|
|
317
|
+
if (!isDeepEqual(cluster.attributes, attrs.values[AttributeList.id])) {
|
|
318
|
+
cluster.behavior = undefined;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
300
322
|
await cluster.store.externalSet(attrs.values);
|
|
301
323
|
this.#synchronizeCluster(endpoint, cluster);
|
|
302
324
|
}
|
|
@@ -309,30 +331,34 @@ export class ClientStructure {
|
|
|
309
331
|
*
|
|
310
332
|
* Invoked once we've loaded all attributes in an interaction.
|
|
311
333
|
*/
|
|
312
|
-
#synchronizeCluster(
|
|
334
|
+
#synchronizeCluster(structure: EndpointStructure, cluster: ClusterStructure) {
|
|
335
|
+
const { endpoint } = structure;
|
|
336
|
+
|
|
313
337
|
// Generate a behavior if enough information is available
|
|
314
|
-
if (cluster.behavior === undefined
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
338
|
+
if (cluster.behavior === undefined) {
|
|
339
|
+
if (cluster.store.initialValues) {
|
|
340
|
+
const {
|
|
341
|
+
[ClusterRevision.id]: clusterRevision,
|
|
342
|
+
[FeatureMap.id]: features,
|
|
343
|
+
[AttributeList.id]: attributeList,
|
|
344
|
+
[AcceptedCommandList.id]: commandList,
|
|
345
|
+
} = cluster.store.initialValues;
|
|
346
|
+
|
|
347
|
+
if (typeof clusterRevision === "number") {
|
|
348
|
+
cluster.revision = clusterRevision;
|
|
349
|
+
}
|
|
325
350
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
351
|
+
if (typeof features === "object" && features !== null && !Array.isArray(features)) {
|
|
352
|
+
cluster.features = features as FeatureBitmap;
|
|
353
|
+
}
|
|
329
354
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
if (Array.isArray(attributeList)) {
|
|
356
|
+
cluster.attributes = attributeList.filter(attr => typeof attr === "number") as AttributeId[];
|
|
357
|
+
}
|
|
333
358
|
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
if (Array.isArray(commandList)) {
|
|
360
|
+
cluster.commands = commandList.filter(cmd => typeof cmd === "number") as CommandId[];
|
|
361
|
+
}
|
|
336
362
|
}
|
|
337
363
|
|
|
338
364
|
if (
|
|
@@ -341,10 +367,17 @@ export class ClientStructure {
|
|
|
341
367
|
cluster.attributes !== undefined &&
|
|
342
368
|
cluster.commands !== undefined
|
|
343
369
|
) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (endpoint.
|
|
347
|
-
|
|
370
|
+
const behaviorType = PeerBehavior(cluster as PeerBehavior.ClusterShape);
|
|
371
|
+
|
|
372
|
+
if (endpoint.lifecycle.isInstalled) {
|
|
373
|
+
cluster.pendingBehavior = behaviorType;
|
|
374
|
+
this.#scheduleStructureChange(
|
|
375
|
+
structure,
|
|
376
|
+
endpoint.behaviors.supported[behaviorType.id] ? "rebuild" : "install",
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
cluster.behavior = behaviorType;
|
|
380
|
+
endpoint.behaviors.inject(behaviorType);
|
|
348
381
|
}
|
|
349
382
|
}
|
|
350
383
|
}
|
|
@@ -352,19 +385,21 @@ export class ClientStructure {
|
|
|
352
385
|
// Special handling for descriptor cluster
|
|
353
386
|
if (cluster.id === Descriptor.Cluster.id) {
|
|
354
387
|
let attrs;
|
|
355
|
-
if (cluster.behavior && endpoint.
|
|
356
|
-
attrs = endpoint.
|
|
388
|
+
if (cluster.behavior && endpoint.behaviors.isActive(cluster.behavior.id)) {
|
|
389
|
+
attrs = endpoint.stateOf(cluster.behavior);
|
|
357
390
|
} else {
|
|
358
391
|
attrs = cluster.store.initialValues ?? {};
|
|
359
392
|
}
|
|
360
|
-
this.#synchronizeDescriptor(
|
|
393
|
+
this.#synchronizeDescriptor(structure, attrs);
|
|
361
394
|
}
|
|
362
395
|
}
|
|
363
396
|
|
|
364
|
-
#synchronizeDescriptor(
|
|
397
|
+
#synchronizeDescriptor(structure: EndpointStructure, attrs: Record<number, unknown>) {
|
|
398
|
+
const { endpoint } = structure;
|
|
399
|
+
|
|
365
400
|
const deviceTypeList = attrs[DEVICE_TYPE_LIST_ATTR_ID] as Descriptor.DeviceType[];
|
|
366
401
|
if (Array.isArray(deviceTypeList)) {
|
|
367
|
-
const endpointType = endpoint.
|
|
402
|
+
const endpointType = endpoint.type;
|
|
368
403
|
for (const dt of deviceTypeList) {
|
|
369
404
|
if (typeof dt?.deviceType !== "number") {
|
|
370
405
|
continue;
|
|
@@ -377,7 +412,7 @@ export class ClientStructure {
|
|
|
377
412
|
}
|
|
378
413
|
|
|
379
414
|
// Root endpoint really needs to be a root endpoint so ignore any noise that would disrupt that
|
|
380
|
-
if (!endpoint.
|
|
415
|
+
if (!endpoint.number && endpointType.deviceType !== RootEndpoint.deviceType) {
|
|
381
416
|
endpointType.deviceRevision = dt.revision;
|
|
382
417
|
break;
|
|
383
418
|
}
|
|
@@ -401,14 +436,25 @@ export class ClientStructure {
|
|
|
401
436
|
|
|
402
437
|
const serverList = attrs[SERVER_LIST_ATTR_ID];
|
|
403
438
|
if (Array.isArray(serverList)) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
439
|
+
const currentlySupported = new Set(
|
|
440
|
+
Object.values(endpoint.behaviors.supported)
|
|
441
|
+
.map(type => (type as ClusterBehavior.Type).cluster?.id)
|
|
442
|
+
.filter(id => id !== undefined),
|
|
443
|
+
);
|
|
444
|
+
|
|
407
445
|
for (const cluster of serverList) {
|
|
408
446
|
if (typeof cluster === "number") {
|
|
409
|
-
this.#clusterFor(
|
|
447
|
+
this.#clusterFor(structure, cluster as ClusterId);
|
|
448
|
+
currentlySupported.delete(cluster as ClusterId);
|
|
410
449
|
}
|
|
411
450
|
}
|
|
451
|
+
|
|
452
|
+
if (currentlySupported.size) {
|
|
453
|
+
for (const id of currentlySupported) {
|
|
454
|
+
this.#clusterFor(structure, id).pendingDelete = true;
|
|
455
|
+
}
|
|
456
|
+
this.#scheduleStructureChange(structure, "rebuild");
|
|
457
|
+
}
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
// The remaining logic deals with the parts list
|
|
@@ -427,7 +473,7 @@ export class ClientStructure {
|
|
|
427
473
|
|
|
428
474
|
let isAlreadyDescendant = false;
|
|
429
475
|
for (let owner = this.#ownerOf(part); owner; owner = this.#ownerOf(owner)) {
|
|
430
|
-
if (owner ===
|
|
476
|
+
if (owner === structure) {
|
|
431
477
|
isAlreadyDescendant = true;
|
|
432
478
|
break;
|
|
433
479
|
}
|
|
@@ -437,24 +483,24 @@ export class ClientStructure {
|
|
|
437
483
|
continue;
|
|
438
484
|
}
|
|
439
485
|
|
|
440
|
-
part.pendingOwner =
|
|
441
|
-
this.#
|
|
486
|
+
part.pendingOwner = structure;
|
|
487
|
+
this.#scheduleStructureChange(part, "install");
|
|
442
488
|
}
|
|
443
489
|
|
|
444
|
-
// For the root partsList specifically, if an endpoint is no longer present then it has been
|
|
490
|
+
// For the root partsList specifically, if an endpoint is no longer present then it has been removed from the
|
|
445
491
|
// node. Schedule for erase
|
|
446
|
-
if (endpoint.
|
|
492
|
+
if (endpoint.maybeNumber === 0) {
|
|
447
493
|
const numbersUsed = new Set(partsList);
|
|
448
|
-
for (const descendent of (endpoint
|
|
494
|
+
for (const descendent of (endpoint as Node).endpoints) {
|
|
449
495
|
// Skip root endpoint and uninitialized numbers (though latter shouldn't be possible)
|
|
450
496
|
if (!descendent.maybeNumber) {
|
|
451
497
|
continue;
|
|
452
498
|
}
|
|
453
499
|
|
|
454
500
|
if (!numbersUsed.has(descendent.number)) {
|
|
455
|
-
const endpoint = this.#endpoints
|
|
501
|
+
const endpoint = this.#endpoints.get(descendent.number);
|
|
456
502
|
if (endpoint) {
|
|
457
|
-
this.#
|
|
503
|
+
this.#scheduleStructureChange(endpoint, "erase");
|
|
458
504
|
}
|
|
459
505
|
}
|
|
460
506
|
}
|
|
@@ -462,7 +508,7 @@ export class ClientStructure {
|
|
|
462
508
|
}
|
|
463
509
|
|
|
464
510
|
#endpointFor(number: EndpointNumber) {
|
|
465
|
-
let endpoint = this.#endpoints
|
|
511
|
+
let endpoint = this.#endpoints.get(number);
|
|
466
512
|
if (endpoint) {
|
|
467
513
|
return endpoint;
|
|
468
514
|
}
|
|
@@ -477,15 +523,15 @@ export class ClientStructure {
|
|
|
477
523
|
deviceRevision: EndpointType.UNKNOWN_DEVICE_REVISION,
|
|
478
524
|
}),
|
|
479
525
|
}),
|
|
480
|
-
clusters:
|
|
526
|
+
clusters: new Map(),
|
|
481
527
|
};
|
|
482
|
-
this.#endpoints
|
|
528
|
+
this.#endpoints.set(number, endpoint);
|
|
483
529
|
|
|
484
530
|
return endpoint;
|
|
485
531
|
}
|
|
486
532
|
|
|
487
533
|
#clusterFor(endpoint: EndpointStructure, id: ClusterId) {
|
|
488
|
-
let cluster = endpoint.clusters
|
|
534
|
+
let cluster = endpoint.clusters.get(id);
|
|
489
535
|
if (cluster) {
|
|
490
536
|
return cluster;
|
|
491
537
|
}
|
|
@@ -494,8 +540,11 @@ export class ClientStructure {
|
|
|
494
540
|
kind: "discovered",
|
|
495
541
|
id,
|
|
496
542
|
store: this.#nodeStore.storeForEndpoint(endpoint.endpoint).createStoreForBehavior(id.toString()),
|
|
543
|
+
behavior: undefined,
|
|
544
|
+
pendingBehavior: undefined,
|
|
545
|
+
pendingDelete: undefined,
|
|
497
546
|
};
|
|
498
|
-
endpoint.clusters
|
|
547
|
+
endpoint.clusters.set(id, cluster);
|
|
499
548
|
|
|
500
549
|
return cluster;
|
|
501
550
|
}
|
|
@@ -516,15 +565,188 @@ export class ClientStructure {
|
|
|
516
565
|
}
|
|
517
566
|
}
|
|
518
567
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Erase an endpoint that disappeared from the peer.
|
|
570
|
+
*/
|
|
571
|
+
async #erase(structure: EndpointStructure) {
|
|
572
|
+
const { endpoint } = structure;
|
|
573
|
+
|
|
574
|
+
logger.debug(
|
|
575
|
+
"Removing endpoint",
|
|
576
|
+
Diagnostic.strong(endpoint.toString()),
|
|
577
|
+
"because it is no longer present on the peer",
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
this.#endpoints.delete(endpoint.number);
|
|
581
|
+
try {
|
|
582
|
+
await endpoint.delete();
|
|
583
|
+
} catch (e) {
|
|
584
|
+
logger.error(`Error erasing peer endpoint ${endpoint}:`, e);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Replace clusters after activation because fixed global attributes have changed.
|
|
590
|
+
*
|
|
591
|
+
* Currently we apply granular updates to clusters. This will possibly result in subtle errors if peers change in
|
|
592
|
+
* incompatible ways, but the backings are designed to be fairly resilient to this. This is simpler for API users
|
|
593
|
+
* to deal with in the common case where they can just ignore. If it becomes problematic we can revert to replacing
|
|
594
|
+
* entire endpoints or behaviors when there are structural changes.
|
|
595
|
+
*/
|
|
596
|
+
async #rebuild(structure: EndpointStructure) {
|
|
597
|
+
const { endpoint, clusters } = structure;
|
|
598
|
+
|
|
599
|
+
for (const cluster of clusters.values()) {
|
|
600
|
+
const { behavior, pendingBehavior, pendingDelete } = cluster;
|
|
601
|
+
|
|
602
|
+
if (pendingDelete) {
|
|
603
|
+
if (!behavior) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await endpoint.behaviors.drop(behavior.id);
|
|
608
|
+
try {
|
|
609
|
+
await cluster.store.erase();
|
|
610
|
+
} catch (e) {
|
|
611
|
+
logger.error("Error clearing cluster storage:", e);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
this.#pendingEvents.push({
|
|
615
|
+
kind: "cluster",
|
|
616
|
+
endpoint: structure,
|
|
617
|
+
cluster,
|
|
618
|
+
subkind: "delete",
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!pendingBehavior) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const subkind = pendingBehavior.id in endpoint.behaviors.supported ? "replace" : "add";
|
|
629
|
+
|
|
630
|
+
endpoint.behaviors.inject(pendingBehavior);
|
|
631
|
+
|
|
632
|
+
cluster.behavior = pendingBehavior;
|
|
633
|
+
delete cluster.pendingBehavior;
|
|
634
|
+
|
|
635
|
+
this.#pendingEvents.push({
|
|
636
|
+
kind: "cluster",
|
|
637
|
+
subkind,
|
|
638
|
+
endpoint: structure,
|
|
639
|
+
cluster,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Install the endpoint and/or new behaviors.
|
|
646
|
+
*/
|
|
647
|
+
#install(structure: EndpointStructure) {
|
|
648
|
+
const { endpoint, pendingOwner, clusters } = structure;
|
|
649
|
+
|
|
650
|
+
// Handle endpoint installation
|
|
651
|
+
if (pendingOwner) {
|
|
652
|
+
endpoint.owner = pendingOwner.endpoint;
|
|
653
|
+
structure.pendingOwner = undefined;
|
|
654
|
+
this.#pendingEvents.push({ kind: "endpoint", endpoint: structure });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Handle behavior installation
|
|
658
|
+
for (const cluster of clusters.values()) {
|
|
659
|
+
const { pendingBehavior } = cluster;
|
|
660
|
+
|
|
661
|
+
// Skip if there is already a behavior even if there's a pending behavior because this needs to be handled
|
|
662
|
+
// by #rebuild
|
|
663
|
+
if (!pendingBehavior || endpoint.behaviors.supported[pendingBehavior.id]) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Add support for the cluster
|
|
668
|
+
endpoint.behaviors.inject(pendingBehavior);
|
|
669
|
+
cluster.behavior = pendingBehavior;
|
|
670
|
+
cluster.pendingBehavior = undefined;
|
|
671
|
+
|
|
672
|
+
// We emit cluster events during the endpoint event so only add cluster event manually if the endpoint is
|
|
673
|
+
// already installed
|
|
674
|
+
if (!pendingOwner) {
|
|
675
|
+
this.#pendingEvents.push({
|
|
676
|
+
kind: "cluster",
|
|
677
|
+
subkind: "add",
|
|
678
|
+
endpoint: structure,
|
|
679
|
+
cluster,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Queue a structural change for processing once a read response is fully processed.
|
|
687
|
+
*/
|
|
688
|
+
#scheduleStructureChange(endpoint: EndpointStructure, kind: keyof PendingChange) {
|
|
689
|
+
const pending = this.#pendingChanges.get(endpoint);
|
|
690
|
+
if (pending) {
|
|
691
|
+
pending[kind] = true;
|
|
692
|
+
} else {
|
|
693
|
+
this.#pendingChanges.set(endpoint, { [kind]: true });
|
|
523
694
|
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Emit pending events.
|
|
699
|
+
*
|
|
700
|
+
* We do this after all structural updates are complete so that listeners can expect composed parts and dependent
|
|
701
|
+
* behaviors to be installed.
|
|
702
|
+
*/
|
|
703
|
+
#emitPendingEvents() {
|
|
704
|
+
const events = this.#pendingEvents;
|
|
705
|
+
this.#pendingEvents = [];
|
|
706
|
+
for (const event of events) {
|
|
707
|
+
switch (event.kind) {
|
|
708
|
+
case "endpoint": {
|
|
709
|
+
const {
|
|
710
|
+
endpoint: { endpoint, clusters },
|
|
711
|
+
} = event;
|
|
712
|
+
this.#events.emitEndpoint(endpoint);
|
|
713
|
+
|
|
714
|
+
// Emit all cluster events now. This is a minor optimization
|
|
715
|
+
for (const { behavior } of clusters.values()) {
|
|
716
|
+
if (behavior) {
|
|
717
|
+
this.#events.emitCluster(endpoint, behavior);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
524
722
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
723
|
+
case "cluster": {
|
|
724
|
+
const {
|
|
725
|
+
endpoint: { endpoint },
|
|
726
|
+
cluster: { behavior },
|
|
727
|
+
} = event;
|
|
728
|
+
|
|
729
|
+
if (!behavior) {
|
|
730
|
+
// Shouldn't happen
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
switch (event.subkind) {
|
|
735
|
+
case "add":
|
|
736
|
+
this.#events.emitCluster(endpoint, behavior);
|
|
737
|
+
break;
|
|
738
|
+
|
|
739
|
+
case "delete":
|
|
740
|
+
this.#events.emitClusterDeleted(endpoint, behavior);
|
|
741
|
+
break;
|
|
742
|
+
|
|
743
|
+
case "replace":
|
|
744
|
+
this.#events.emitClusterReplaced(endpoint, behavior);
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
528
750
|
}
|
|
529
751
|
}
|
|
530
752
|
|
|
@@ -539,12 +761,51 @@ interface AttributeUpdates {
|
|
|
539
761
|
interface EndpointStructure {
|
|
540
762
|
pendingOwner?: EndpointStructure;
|
|
541
763
|
endpoint: Endpoint;
|
|
542
|
-
clusters:
|
|
764
|
+
clusters: Map<ClusterId, ClusterStructure>;
|
|
543
765
|
}
|
|
544
766
|
|
|
545
767
|
interface ClusterStructure extends Partial<PeerBehavior.DiscoveredClusterShape> {
|
|
546
768
|
kind: "discovered";
|
|
547
769
|
id: ClusterId;
|
|
548
770
|
behavior?: ClusterBehavior.Type;
|
|
549
|
-
|
|
771
|
+
pendingBehavior?: ClusterBehavior.Type;
|
|
772
|
+
pendingDelete?: boolean;
|
|
773
|
+
store: DatasourceCache;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Queue entry for structural changes.
|
|
778
|
+
*/
|
|
779
|
+
interface PendingChange {
|
|
780
|
+
/**
|
|
781
|
+
* Erase an endpoint.
|
|
782
|
+
*/
|
|
783
|
+
erase?: boolean;
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Install new endpoint and/or behaviors.
|
|
787
|
+
*/
|
|
788
|
+
install?: boolean;
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Handle replacement or deletion of behaviors on active endpoint.
|
|
792
|
+
*/
|
|
793
|
+
rebuild?: boolean;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Queue entry for pending notifications.
|
|
798
|
+
*/
|
|
799
|
+
export type PendingEvent = EndpointEvent | ClusterEvent;
|
|
800
|
+
|
|
801
|
+
interface EndpointEvent {
|
|
802
|
+
kind: "endpoint";
|
|
803
|
+
endpoint: EndpointStructure;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
interface ClusterEvent {
|
|
807
|
+
kind: "cluster";
|
|
808
|
+
subkind: "add" | "delete" | "replace";
|
|
809
|
+
endpoint: EndpointStructure;
|
|
810
|
+
cluster: ClusterStructure;
|
|
550
811
|
}
|