@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.
Files changed (91) hide show
  1. package/dist/cjs/behavior/internal/BehaviorBacking.d.ts +2 -1
  2. package/dist/cjs/behavior/internal/BehaviorBacking.d.ts.map +1 -1
  3. package/dist/cjs/behavior/internal/BehaviorBacking.js +11 -3
  4. package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
  5. package/dist/cjs/behavior/internal/ClientBehaviorBacking.d.ts +2 -0
  6. package/dist/cjs/behavior/internal/ClientBehaviorBacking.d.ts.map +1 -1
  7. package/dist/cjs/behavior/internal/ClientBehaviorBacking.js +4 -0
  8. package/dist/cjs/behavior/internal/ClientBehaviorBacking.js.map +1 -1
  9. package/dist/cjs/behavior/state/managed/Datasource.d.ts +4 -0
  10. package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
  11. package/dist/cjs/behavior/state/managed/Datasource.js +5 -0
  12. package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
  13. package/dist/cjs/endpoint/properties/Behaviors.d.ts +12 -2
  14. package/dist/cjs/endpoint/properties/Behaviors.d.ts.map +1 -1
  15. package/dist/cjs/endpoint/properties/Behaviors.js +44 -10
  16. package/dist/cjs/endpoint/properties/Behaviors.js.map +2 -2
  17. package/dist/cjs/node/client/ClientStructure.d.ts +31 -1
  18. package/dist/cjs/node/client/ClientStructure.d.ts.map +1 -1
  19. package/dist/cjs/node/client/ClientStructure.js +257 -81
  20. package/dist/cjs/node/client/ClientStructure.js.map +1 -1
  21. package/dist/cjs/node/client/ClientStructureEvents.d.ts +5 -2
  22. package/dist/cjs/node/client/ClientStructureEvents.d.ts.map +1 -1
  23. package/dist/cjs/node/client/ClientStructureEvents.js +38 -2
  24. package/dist/cjs/node/client/ClientStructureEvents.js.map +1 -1
  25. package/dist/cjs/node/client/Peers.d.ts +3 -0
  26. package/dist/cjs/node/client/Peers.d.ts.map +1 -1
  27. package/dist/cjs/node/client/Peers.js +3 -0
  28. package/dist/cjs/node/client/Peers.js.map +1 -1
  29. package/dist/cjs/node/server/ServerEndpointInitializer.js +1 -1
  30. package/dist/cjs/node/server/ServerEndpointInitializer.js.map +1 -1
  31. package/dist/cjs/storage/EndpointStore.d.ts +5 -1
  32. package/dist/cjs/storage/EndpointStore.d.ts.map +1 -1
  33. package/dist/cjs/storage/EndpointStore.js +7 -1
  34. package/dist/cjs/storage/EndpointStore.js.map +1 -1
  35. package/dist/cjs/storage/client/ClientEndpointStore.d.ts +2 -1
  36. package/dist/cjs/storage/client/ClientEndpointStore.d.ts.map +1 -1
  37. package/dist/cjs/storage/client/DatasourceCache.d.ts +14 -3
  38. package/dist/cjs/storage/client/DatasourceCache.d.ts.map +1 -1
  39. package/dist/cjs/storage/client/DatasourceCache.js +10 -0
  40. package/dist/cjs/storage/client/DatasourceCache.js.map +1 -1
  41. package/dist/esm/behavior/internal/BehaviorBacking.d.ts +2 -1
  42. package/dist/esm/behavior/internal/BehaviorBacking.d.ts.map +1 -1
  43. package/dist/esm/behavior/internal/BehaviorBacking.js +11 -3
  44. package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
  45. package/dist/esm/behavior/internal/ClientBehaviorBacking.d.ts +2 -0
  46. package/dist/esm/behavior/internal/ClientBehaviorBacking.d.ts.map +1 -1
  47. package/dist/esm/behavior/internal/ClientBehaviorBacking.js +4 -0
  48. package/dist/esm/behavior/internal/ClientBehaviorBacking.js.map +1 -1
  49. package/dist/esm/behavior/state/managed/Datasource.d.ts +4 -0
  50. package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
  51. package/dist/esm/behavior/state/managed/Datasource.js +5 -0
  52. package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
  53. package/dist/esm/endpoint/properties/Behaviors.d.ts +12 -2
  54. package/dist/esm/endpoint/properties/Behaviors.d.ts.map +1 -1
  55. package/dist/esm/endpoint/properties/Behaviors.js +44 -10
  56. package/dist/esm/endpoint/properties/Behaviors.js.map +2 -2
  57. package/dist/esm/node/client/ClientStructure.d.ts +31 -1
  58. package/dist/esm/node/client/ClientStructure.d.ts.map +1 -1
  59. package/dist/esm/node/client/ClientStructure.js +258 -82
  60. package/dist/esm/node/client/ClientStructure.js.map +1 -1
  61. package/dist/esm/node/client/ClientStructureEvents.d.ts +5 -2
  62. package/dist/esm/node/client/ClientStructureEvents.d.ts.map +1 -1
  63. package/dist/esm/node/client/ClientStructureEvents.js +39 -3
  64. package/dist/esm/node/client/ClientStructureEvents.js.map +1 -1
  65. package/dist/esm/node/client/Peers.d.ts +3 -0
  66. package/dist/esm/node/client/Peers.d.ts.map +1 -1
  67. package/dist/esm/node/client/Peers.js +3 -0
  68. package/dist/esm/node/client/Peers.js.map +1 -1
  69. package/dist/esm/node/server/ServerEndpointInitializer.js +1 -1
  70. package/dist/esm/node/server/ServerEndpointInitializer.js.map +1 -1
  71. package/dist/esm/storage/EndpointStore.d.ts +5 -1
  72. package/dist/esm/storage/EndpointStore.d.ts.map +1 -1
  73. package/dist/esm/storage/EndpointStore.js +7 -1
  74. package/dist/esm/storage/EndpointStore.js.map +1 -1
  75. package/dist/esm/storage/client/ClientEndpointStore.d.ts +2 -1
  76. package/dist/esm/storage/client/ClientEndpointStore.d.ts.map +1 -1
  77. package/dist/esm/storage/client/DatasourceCache.d.ts +14 -3
  78. package/dist/esm/storage/client/DatasourceCache.d.ts.map +1 -1
  79. package/dist/esm/storage/client/DatasourceCache.js +10 -0
  80. package/dist/esm/storage/client/DatasourceCache.js.map +1 -1
  81. package/package.json +7 -7
  82. package/src/behavior/internal/BehaviorBacking.ts +16 -3
  83. package/src/behavior/internal/ClientBehaviorBacking.ts +10 -1
  84. package/src/behavior/state/managed/Datasource.ts +13 -0
  85. package/src/endpoint/properties/Behaviors.ts +53 -12
  86. package/src/node/client/ClientStructure.ts +352 -91
  87. package/src/node/client/ClientStructureEvents.ts +55 -7
  88. package/src/node/client/Peers.ts +4 -0
  89. package/src/node/server/ServerEndpointInitializer.ts +1 -1
  90. package/src/storage/EndpointStore.ts +8 -1
  91. 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: Record<EndpointNumber, EndpointStructure> = {};
46
+ #endpoints = new Map<EndpointNumber, EndpointStructure>();
47
47
  #emitEvent: ClientEventEmitter;
48
48
  #node: ClientNode;
49
49
  #subscribedFabricFiltered?: boolean;
50
- #pending = new Map<EndpointStructure, "erase" | "reparent">();
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[node.number] = {
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
- for (const [endpoint, opcode] of this.#pending.entries()) {
92
- this.#pending.delete(endpoint);
93
-
94
- switch (opcode) {
95
- case "reparent":
96
- this.#install(endpoint);
97
- break;
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 Object.values(this.#endpoints)) {
138
+ } of this.#endpoints.values()) {
135
139
  for (const {
136
140
  id: clusterId,
137
141
  store: { version },
138
- } of Object.values(clusters)) {
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, opcode] of this.#pending.entries()) {
200
- this.#pending.delete(endpoint);
203
+ for (const [endpoint, change] of this.#pendingChanges.entries()) {
204
+ this.#pendingChanges.delete(endpoint);
201
205
 
202
- switch (opcode) {
203
- case "reparent":
204
- this.#install(endpoint);
205
- break;
206
+ if (change.erase) {
207
+ await this.#erase(endpoint);
208
+ continue;
209
+ }
206
210
 
207
- case "erase":
208
- logger.debug(`Removing endpoint ${endpoint.endpoint} because it is no longer present on the peer`);
209
- delete this.#endpoints[endpoint.endpoint.number];
210
- try {
211
- await endpoint.endpoint.delete();
212
- } catch (e) {
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[endpoint]?.endpoint;
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(endpoint: EndpointStructure, cluster: ClusterStructure) {
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 && cluster.store.initialValues) {
315
- const {
316
- [ClusterRevision.id]: clusterRevision,
317
- [FeatureMap.id]: features,
318
- [AttributeList.id]: attributeList,
319
- [AcceptedCommandList.id]: commandList,
320
- } = cluster.store.initialValues;
321
-
322
- if (typeof clusterRevision === "number") {
323
- cluster.revision = clusterRevision;
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
- if (typeof features === "object" && features !== null && !Array.isArray(features)) {
327
- cluster.features = features as FeatureBitmap;
328
- }
351
+ if (typeof features === "object" && features !== null && !Array.isArray(features)) {
352
+ cluster.features = features as FeatureBitmap;
353
+ }
329
354
 
330
- if (Array.isArray(attributeList)) {
331
- cluster.attributes = attributeList.filter(attr => typeof attr === "number") as AttributeId[];
332
- }
355
+ if (Array.isArray(attributeList)) {
356
+ cluster.attributes = attributeList.filter(attr => typeof attr === "number") as AttributeId[];
357
+ }
333
358
 
334
- if (Array.isArray(commandList)) {
335
- cluster.commands = commandList.filter(cmd => typeof cmd === "number") as CommandId[];
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
- cluster.behavior = PeerBehavior(cluster as PeerBehavior.ClusterShape);
345
- endpoint.endpoint.behaviors.require(cluster.behavior);
346
- if (endpoint.endpoint.lifecycle.isInstalled) {
347
- this.#events.emitCluster(endpoint.endpoint, cluster.behavior);
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.endpoint.behaviors.isActive(cluster.behavior.id)) {
356
- attrs = endpoint.endpoint.stateOf(cluster.behavior);
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(endpoint, attrs);
393
+ this.#synchronizeDescriptor(structure, attrs);
361
394
  }
362
395
  }
363
396
 
364
- #synchronizeDescriptor(endpoint: EndpointStructure, attrs: Record<number, unknown>) {
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.endpoint.type;
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.endpoint.number && endpointType.deviceType !== RootEndpoint.deviceType) {
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
- // TODO: Remove clusters that are no longer present
405
- // Including events vis parts/endpoints on node (per endpoint and generic "changed")?
406
- // Including data cleanup
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(endpoint, cluster as ClusterId);
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 === endpoint) {
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 = endpoint;
441
- this.#pending.set(part, "reparent");
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 removd from the
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.endpoint.maybeNumber === 0) {
492
+ if (endpoint.maybeNumber === 0) {
447
493
  const numbersUsed = new Set(partsList);
448
- for (const descendent of (endpoint.endpoint as Node).endpoints) {
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[descendent.number];
501
+ const endpoint = this.#endpoints.get(descendent.number);
456
502
  if (endpoint) {
457
- this.#pending.set(endpoint, "erase");
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[number];
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[number] = endpoint;
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[id];
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[id] = cluster;
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
- #install(endpoint: EndpointStructure) {
520
- const { pendingOwner } = endpoint;
521
- if (!pendingOwner) {
522
- return;
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
- endpoint.endpoint.owner = pendingOwner.endpoint;
526
- endpoint.pendingOwner = undefined;
527
- this.#events.emitEndpoint(endpoint.endpoint);
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: Record<ClusterId, ClusterStructure>;
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
- store: Datasource.ExternallyMutableStore;
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
  }