@matter/node 0.14.1-alpha.0-20250607-a93593303 → 0.15.0-alpha.0-20250613-a55f991d4

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 (108) hide show
  1. package/dist/cjs/behavior/Events.d.ts +8 -3
  2. package/dist/cjs/behavior/Events.d.ts.map +1 -1
  3. package/dist/cjs/behavior/Events.js +5 -1
  4. package/dist/cjs/behavior/Events.js.map +1 -1
  5. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js +3 -3
  6. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  7. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts +16 -0
  8. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -0
  9. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js +119 -0
  10. package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js.map +6 -0
  11. package/dist/cjs/behavior/cluster/index.d.ts +1 -0
  12. package/dist/cjs/behavior/cluster/index.d.ts.map +1 -1
  13. package/dist/cjs/behavior/cluster/index.js +1 -0
  14. package/dist/cjs/behavior/cluster/index.js.map +1 -1
  15. package/dist/cjs/behavior/context/server/OnlineContext.d.ts +2 -1
  16. package/dist/cjs/behavior/context/server/OnlineContext.d.ts.map +1 -1
  17. package/dist/cjs/behavior/context/server/OnlineContext.js +22 -7
  18. package/dist/cjs/behavior/context/server/OnlineContext.js.map +1 -1
  19. package/dist/cjs/behavior/state/managed/Datasource.d.ts +6 -5
  20. package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
  21. package/dist/cjs/behavior/state/managed/Datasource.js +25 -14
  22. package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
  23. package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts +7 -3
  24. package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
  25. package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts +20 -36
  26. package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  27. package/dist/cjs/behaviors/access-control/AccessControlServer.js +153 -87
  28. package/dist/cjs/behaviors/access-control/AccessControlServer.js.map +1 -1
  29. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
  30. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
  31. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js.map +2 -2
  32. package/dist/cjs/endpoint/properties/Behaviors.d.ts.map +1 -1
  33. package/dist/cjs/endpoint/properties/Behaviors.js +10 -0
  34. package/dist/cjs/endpoint/properties/Behaviors.js.map +1 -1
  35. package/dist/cjs/node/ServerNode.d.ts +2 -2
  36. package/dist/cjs/node/ServerNode.d.ts.map +1 -1
  37. package/dist/cjs/node/ServerNode.js +2 -2
  38. package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -1
  39. package/dist/cjs/node/server/InteractionServer.js +10 -44
  40. package/dist/cjs/node/server/InteractionServer.js.map +2 -2
  41. package/dist/cjs/node/server/ProtocolService.js +1 -1
  42. package/dist/cjs/node/server/ProtocolService.js.map +1 -1
  43. package/dist/cjs/node/server/ServerEnvironment.d.ts +3 -0
  44. package/dist/cjs/node/server/ServerEnvironment.d.ts.map +1 -1
  45. package/dist/cjs/node/server/ServerEnvironment.js +12 -2
  46. package/dist/cjs/node/server/ServerEnvironment.js.map +1 -1
  47. package/dist/esm/behavior/Events.d.ts +8 -3
  48. package/dist/esm/behavior/Events.d.ts.map +1 -1
  49. package/dist/esm/behavior/Events.js +5 -2
  50. package/dist/esm/behavior/Events.js.map +1 -1
  51. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js +4 -4
  52. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  53. package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts +16 -0
  54. package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -0
  55. package/dist/esm/behavior/cluster/FabricScopedDataHandler.js +99 -0
  56. package/dist/esm/behavior/cluster/FabricScopedDataHandler.js.map +6 -0
  57. package/dist/esm/behavior/cluster/index.d.ts +1 -0
  58. package/dist/esm/behavior/cluster/index.d.ts.map +1 -1
  59. package/dist/esm/behavior/cluster/index.js +1 -0
  60. package/dist/esm/behavior/cluster/index.js.map +1 -1
  61. package/dist/esm/behavior/context/server/OnlineContext.d.ts +2 -1
  62. package/dist/esm/behavior/context/server/OnlineContext.d.ts.map +1 -1
  63. package/dist/esm/behavior/context/server/OnlineContext.js +29 -9
  64. package/dist/esm/behavior/context/server/OnlineContext.js.map +1 -1
  65. package/dist/esm/behavior/state/managed/Datasource.d.ts +6 -5
  66. package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
  67. package/dist/esm/behavior/state/managed/Datasource.js +25 -14
  68. package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
  69. package/dist/esm/behavior/supervision/ValueSupervisor.d.ts +7 -3
  70. package/dist/esm/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
  71. package/dist/esm/behaviors/access-control/AccessControlServer.d.ts +20 -36
  72. package/dist/esm/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  73. package/dist/esm/behaviors/access-control/AccessControlServer.js +153 -88
  74. package/dist/esm/behaviors/access-control/AccessControlServer.js.map +1 -1
  75. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
  76. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
  77. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js.map +1 -1
  78. package/dist/esm/endpoint/properties/Behaviors.d.ts.map +1 -1
  79. package/dist/esm/endpoint/properties/Behaviors.js +10 -0
  80. package/dist/esm/endpoint/properties/Behaviors.js.map +1 -1
  81. package/dist/esm/node/ServerNode.d.ts +2 -2
  82. package/dist/esm/node/ServerNode.d.ts.map +1 -1
  83. package/dist/esm/node/ServerNode.js +3 -3
  84. package/dist/esm/node/ServerNode.js.map +1 -1
  85. package/dist/esm/node/server/InteractionServer.d.ts.map +1 -1
  86. package/dist/esm/node/server/InteractionServer.js +10 -44
  87. package/dist/esm/node/server/InteractionServer.js.map +1 -1
  88. package/dist/esm/node/server/ProtocolService.js +1 -1
  89. package/dist/esm/node/server/ProtocolService.js.map +1 -1
  90. package/dist/esm/node/server/ServerEnvironment.d.ts +3 -0
  91. package/dist/esm/node/server/ServerEnvironment.d.ts.map +1 -1
  92. package/dist/esm/node/server/ServerEnvironment.js +12 -2
  93. package/dist/esm/node/server/ServerEnvironment.js.map +1 -1
  94. package/package.json +7 -7
  95. package/src/behavior/Events.ts +8 -3
  96. package/src/behavior/cluster/ClusterBehaviorUtil.ts +4 -4
  97. package/src/behavior/cluster/FabricScopedDataHandler.ts +142 -0
  98. package/src/behavior/cluster/index.ts +1 -0
  99. package/src/behavior/context/server/OnlineContext.ts +39 -9
  100. package/src/behavior/state/managed/Datasource.ts +37 -20
  101. package/src/behavior/supervision/ValueSupervisor.ts +8 -3
  102. package/src/behaviors/access-control/AccessControlServer.ts +210 -102
  103. package/src/behaviors/operational-credentials/OperationalCredentialsServer.ts +10 -18
  104. package/src/endpoint/properties/Behaviors.ts +12 -1
  105. package/src/node/ServerNode.ts +3 -3
  106. package/src/node/server/InteractionServer.ts +10 -63
  107. package/src/node/server/ProtocolService.ts +1 -1
  108. package/src/node/server/ServerEnvironment.ts +16 -2
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Behavior, ClusterBehavior } from "#behavior/index.js";
8
+ import { Endpoint } from "#endpoint/Endpoint.js";
9
+ import { SupportedElements } from "#endpoint/properties/Behaviors.js";
10
+ import { createPromise, deepCopy, isObject, Logger, MaybePromise, withTimeout } from "#general";
11
+ import { ServerNode } from "#node/ServerNode.js";
12
+ import { OccurrenceManager, Val } from "#protocol";
13
+ import { ClusterType, FabricIndex, ObjectSchema } from "#types";
14
+
15
+ const logger = Logger.get("FabricScopedDataHandler");
16
+
17
+ /** Helper function to iterate over all behaviors of an endpoint */
18
+ async function forBehaviors(
19
+ endpoint: Endpoint,
20
+ callback: (type: Behavior.Type, cluster: ClusterType, elements: SupportedElements) => Promise<void>,
21
+ ) {
22
+ for (const type of Object.values(endpoint.behaviors.supported)) {
23
+ const cluster = (type as ClusterBehavior.Type)?.cluster;
24
+ if (!cluster) {
25
+ continue;
26
+ }
27
+ const elements = endpoint.behaviors.elementsOf(type);
28
+ if (elements.attributes.size || elements.events.size) {
29
+ await callback(type, cluster, elements);
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Sanitize fabric-scoped attributes for a given endpoint and cluster.
36
+ * The changed state is returned in an array of promises and should be awaited to ensure the state is updated.
37
+ */
38
+ async function sanitizeAttributeData(
39
+ endpoint: Endpoint,
40
+ type: Behavior.Type,
41
+ cluster: ClusterType,
42
+ supportedAttributes: Set<string>,
43
+ allowedIndices: FabricIndex[],
44
+ ) {
45
+ const stateUpdatePromises = new Array<Promise<void>>();
46
+
47
+ const stateUpdate = {} as Val.Struct;
48
+ // Iterate over all attributes and check if they are fabric-scoped
49
+ for (const attributeName of supportedAttributes) {
50
+ const attr = cluster.attributes[attributeName];
51
+ if (attr.fabricScoped) {
52
+ const value = (endpoint.stateOf(type) as Val.Struct)[attributeName];
53
+ // If the value contains data for the fabric being removed, remove the data
54
+ if (Array.isArray(value) && value.length > 0) {
55
+ const filtered = deepCopy(value).filter(entry => allowedIndices.includes(entry.fabricIndex));
56
+ if (filtered.length !== value.length) {
57
+ stateUpdate[attributeName] = filtered;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ // If we have any state updates for this behavior, we need to set the state.
63
+ // Errors are being logged and ignored
64
+ if (Object.keys(stateUpdate).length > 0) {
65
+ const { resolver, promise } = createPromise<void>();
66
+ (endpoint.eventsOf(type) as Behavior.EventsOf<any>).stateChanged?.on(resolver);
67
+ try {
68
+ await endpoint.setStateOf(type, stateUpdate);
69
+ stateUpdatePromises.push(withTimeout(5_000, promise)); // 5s should be enough for state change
70
+ } catch (error) {
71
+ logger.warn(
72
+ `Could not sanitize fabric-scoped attributes for cluster ${cluster.name} on endpoint ${endpoint.id}`,
73
+ error,
74
+ );
75
+ }
76
+ }
77
+
78
+ return stateUpdatePromises;
79
+ }
80
+
81
+ /**
82
+ * Sanitize Fabric scoped data and events to a list of allowed fabrics.
83
+ * The logic walks through all endpoints and removes relevant fabric-scoped attribute values for the relevant fabric
84
+ * from the state. After all state changes are processed, it removes all occurrences of fabric-scoped events.
85
+ */
86
+ export async function limitNodeDataToAllowedFabrics(node: ServerNode, allowedIndices: FabricIndex[]) {
87
+ const fabricRelevantEvents = new Set<string>();
88
+ const stateUpdatePromises = new Array<Promise<void>>();
89
+ await node.visit(async endpoint => {
90
+ await forBehaviors(endpoint, async (type, cluster, elements) => {
91
+ if (elements.attributes.size) {
92
+ stateUpdatePromises.push(
93
+ ...(await sanitizeAttributeData(endpoint, type, cluster, elements.attributes, allowedIndices)),
94
+ );
95
+ }
96
+ if (elements.events.size) {
97
+ // If we have events also check if they are fabric scoped and collect them
98
+ for (const eventName of elements.events) {
99
+ const event = cluster.events[eventName];
100
+ if ((event.schema as ObjectSchema<any>).isFabricScoped) {
101
+ fabricRelevantEvents.add(`${cluster.id}-${event.id}`);
102
+ }
103
+ }
104
+ }
105
+ });
106
+ });
107
+
108
+ // Wait for all state changed to be executed before processing events
109
+ await Promise.allSettled(stateUpdatePromises);
110
+
111
+ // Now we can remove all occurrences of fabric-scoped events when payload is bound to this fabric index
112
+ if (fabricRelevantEvents.size > 0) {
113
+ const occurrences = node.env.get(OccurrenceManager);
114
+ for await (const event of occurrences.get()) {
115
+ if (
116
+ fabricRelevantEvents.has(`${event.clusterId}-${event.eventId}`) &&
117
+ isObject(event.payload) &&
118
+ !allowedIndices.includes(event.payload.fabricIndex as FabricIndex)
119
+ ) {
120
+ // Remove occurrences of fabric-scoped events
121
+ const result = occurrences.remove(event.number);
122
+ if (MaybePromise.is(result)) {
123
+ await result;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ export async function limitEndpointAttributeDataToAllowedFabrics(endpoint: Endpoint, allowedIndices: FabricIndex[]) {
131
+ const stateUpdatePromises = new Array<Promise<void>>();
132
+ await forBehaviors(endpoint, async (type, cluster, elements) => {
133
+ if (elements.attributes.size) {
134
+ stateUpdatePromises.push(
135
+ ...(await sanitizeAttributeData(endpoint, type, cluster, elements.attributes, allowedIndices)),
136
+ );
137
+ }
138
+ });
139
+
140
+ // Wait for all state changed to be executed before processing events
141
+ await Promise.allSettled(stateUpdatePromises);
142
+ }
@@ -9,4 +9,5 @@ export * from "./ClusterBehaviorUtil.js";
9
9
  export * from "./ClusterEvents.js";
10
10
  export * from "./ClusterInterface.js";
11
11
  export * from "./ClusterState.js";
12
+ export * from "./FabricScopedDataHandler.js";
12
13
  export * from "./ValidatedElements.js";
@@ -4,21 +4,33 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { AccessControlServer } from "#behaviors/access-control";
8
7
  import { Agent } from "#endpoint/Agent.js";
9
8
  import { Endpoint } from "#endpoint/Endpoint.js";
10
9
  import { EndpointType } from "#endpoint/type/EndpointType.js";
11
- import { Diagnostic, ImplementationError, InternalError, MaybePromise, Transaction } from "#general";
10
+ import { AsyncObservable, Diagnostic, ImplementationError, InternalError, MaybePromise, Transaction } from "#general";
12
11
  import { AccessLevel } from "#model";
13
12
  import type { Node } from "#node/Node.js";
14
13
  import type { Message, NodeProtocol } from "#protocol";
15
- import { AccessControl, AclEndpointContext, MessageExchange, SecureSession, Subject } from "#protocol";
14
+ import {
15
+ AccessControl,
16
+ AclEndpointContext,
17
+ FabricAccessControl,
18
+ MessageExchange,
19
+ SecureSession,
20
+ Subject,
21
+ } from "#protocol";
16
22
  import { FabricIndex, NodeId } from "#types";
17
23
  import { ActionContext } from "../ActionContext.js";
18
24
  import { Contextual } from "../Contextual.js";
19
25
  import { NodeActivity } from "../NodeActivity.js";
20
26
  import { ContextAgents } from "./ContextAgents.js";
21
27
 
28
+ /**
29
+ * Caches completion events per exchange. Uses if multiple OnlineContext instances are created for an exchange.
30
+ * Entries will be cleaned up when the exchange is closed.
31
+ */
32
+ const exchangeCompleteEvents = new WeakMap<MessageExchange, AsyncObservable<[session?: ActionContext | undefined]>>();
33
+
22
34
  /**
23
35
  * Operate in online context. Public Matter API interactions happen in online context.
24
36
  */
@@ -27,6 +39,7 @@ export function OnlineContext(options: OnlineContext.Options) {
27
39
  let subject: Subject;
28
40
  let nodeProtocol: NodeProtocol | undefined;
29
41
  let accessLevelCache: Map<AccessControl.Location, number[]> | undefined;
42
+ let aclManager: FabricAccessControl;
30
43
 
31
44
  const { exchange, message } = options;
32
45
  const session = exchange?.session;
@@ -35,6 +48,8 @@ export function OnlineContext(options: OnlineContext.Options) {
35
48
  SecureSession.assert(session);
36
49
  fabric = session.fabric?.fabricIndex;
37
50
  subject = session.subjectFor(message);
51
+ // Without a fabric, we assume default PASE based access controls and use a fresh FabricAccessControlManager instance
52
+ aclManager = session?.fabric?.acl ?? new FabricAccessControl();
38
53
  } else {
39
54
  fabric = options.fabric;
40
55
  if (options.subject !== undefined) {
@@ -42,6 +57,7 @@ export function OnlineContext(options: OnlineContext.Options) {
42
57
  } else {
43
58
  throw new ImplementationError("OnlineContext requires an authorized subject");
44
59
  }
60
+ aclManager = options.aclManager ?? new FabricAccessControl();
45
61
  }
46
62
 
47
63
  // If we have subjects, the first is the main one, used for diagnostics
@@ -135,6 +151,23 @@ export function OnlineContext(options: OnlineContext.Options) {
135
151
  SecureSession.assert(session);
136
152
  }
137
153
  let agents: undefined | ContextAgents;
154
+ let interactionComplete: AsyncObservable<[session?: ActionContext | undefined]> | undefined;
155
+ if (exchange !== undefined) {
156
+ interactionComplete = exchangeCompleteEvents.get(exchange);
157
+ if (interactionComplete === undefined) {
158
+ interactionComplete = new AsyncObservable();
159
+ exchangeCompleteEvents.set(exchange, interactionComplete);
160
+ }
161
+
162
+ const notifyInteractionComplete = () => {
163
+ exchange.closing.off(notifyInteractionComplete);
164
+ exchangeCompleteEvents.delete(exchange);
165
+ if (context.interactionComplete?.isObserved) {
166
+ context.interactionComplete.emit(context);
167
+ }
168
+ };
169
+ exchange.closing.on(notifyInteractionComplete);
170
+ }
138
171
  const context: ActionContext = {
139
172
  ...options,
140
173
  session,
@@ -144,7 +177,7 @@ export function OnlineContext(options: OnlineContext.Options) {
144
177
  fabric,
145
178
  transaction,
146
179
 
147
- interactionComplete: exchange?.closed,
180
+ interactionComplete,
148
181
 
149
182
  ...methods,
150
183
 
@@ -166,11 +199,7 @@ export function OnlineContext(options: OnlineContext.Options) {
166
199
  throw new InternalError("OnlineContext initialized without node");
167
200
  }
168
201
 
169
- const accessControl = options.node.act(agent => agent.get(AccessControlServer));
170
- if (MaybePromise.is(accessControl)) {
171
- throw new InternalError("AccessControlServer should already be initialized.");
172
- }
173
- const accessLevels = accessControl.accessLevelsFor(context, location, aclEndpointContextFor(location));
202
+ const accessLevels = aclManager.accessLevelsFor(context, location, aclEndpointContextFor(location));
174
203
 
175
204
  if (accessLevelCache === undefined) {
176
205
  accessLevelCache = new Map();
@@ -238,6 +267,7 @@ export namespace OnlineContext {
238
267
  timed?: boolean;
239
268
  fabricFiltered?: boolean;
240
269
  message?: Message;
270
+ aclManager?: FabricAccessControl;
241
271
  } & (
242
272
  | { exchange: MessageExchange; fabric?: undefined; subject?: undefined }
243
273
  | { exchange?: undefined; fabric: FabricIndex; subject: NodeId }
@@ -26,7 +26,7 @@ const logger = Logger.get("Datasource");
26
26
 
27
27
  const FEATURES_KEY = "__features__";
28
28
 
29
- const stateChanged = Symbol("stateChanged");
29
+ const changed = Symbol("changed");
30
30
 
31
31
  const viewTx = Transaction.open("offline-view", "ro");
32
32
 
@@ -66,7 +66,7 @@ export interface Datasource<T extends StateType = StateType> extends Transaction
66
66
  /**
67
67
  * Event that gets emitted when the state changes.
68
68
  */
69
- stateChanged: Observable<[changes: string[], version: number], MaybePromise>;
69
+ changed: Observable<[changes: string[], version: number], MaybePromise>;
70
70
 
71
71
  /**
72
72
  * Events registered for this Datasource
@@ -103,8 +103,8 @@ export function Datasource<const T extends StateType = StateType>(options: Datas
103
103
  return internals.location;
104
104
  },
105
105
 
106
- get stateChanged() {
107
- return internals.events[stateChanged];
106
+ get changed() {
107
+ return internals.events[changed];
108
108
  },
109
109
 
110
110
  get events() {
@@ -142,14 +142,15 @@ export namespace Datasource {
142
142
  * Datasource events.
143
143
  */
144
144
  export type Events = {
145
- interactionBegin?: Observable<[]>;
146
- interactionEnd?: Observable<[], MaybePromise>;
145
+ interactionBegin?: Observable<[context?: ValueSupervisor.Session], MaybePromise>;
146
+ interactionEnd?: Observable<[context?: ValueSupervisor.Session], MaybePromise>;
147
+ stateChanged?: Observable<[context?: ValueSupervisor.Session], MaybePromise>;
147
148
  } & {
148
149
  [K in `${string}$Changing` | `${string}$Changed`]: Observable<Parameters<ValueObserver>, MaybePromise>;
149
150
  };
150
151
 
151
152
  export type InternalEvents = Events & {
152
- [stateChanged]: Observable<[changes: string[], version: number], MaybePromise>;
153
+ [changed]: Observable<[changes: string[], version: number], MaybePromise>;
153
154
  };
154
155
 
155
156
  /**
@@ -235,7 +236,7 @@ interface Internals extends Datasource.Options {
235
236
  version: number;
236
237
  sessions?: Map<ValueSupervisor.Session, SessionContext>;
237
238
  featuresKey?: string;
238
- interactionObserver(): MaybePromise<void>;
239
+ interactionObserver(session?: AccessControl.Session): MaybePromise<void>;
239
240
  events: Datasource.InternalEvents;
240
241
  }
241
242
 
@@ -246,7 +247,7 @@ interface CommitChanges {
246
247
  persistent?: Val.Struct;
247
248
  notifications: Array<{
248
249
  event: Observable<any[], MaybePromise>;
249
- params: Parameters<Datasource.ValueObserver>;
250
+ params: Parameters<Datasource.ValueObserver> | [context?: ValueSupervisor.Session];
250
251
  }>;
251
252
  changeList: Set<string>;
252
253
  }
@@ -285,7 +286,7 @@ function configure(options: Datasource.Options): Internals {
285
286
  Object.freeze(options.location);
286
287
 
287
288
  const events = (options.events ?? {}) as Datasource.InternalEvents;
288
- events[stateChanged] = new Observable();
289
+ events[changed] = new Observable();
289
290
 
290
291
  return {
291
292
  ...options,
@@ -294,18 +295,20 @@ function configure(options: Datasource.Options): Internals {
294
295
  values: values,
295
296
  featuresKey,
296
297
 
297
- interactionObserver() {
298
+ interactionObserver(session?: ValueSupervisor.Session) {
298
299
  function handleObserverError(error: any) {
299
300
  logger.error(`Error in ${options.location.path} observer:`, error);
300
301
  }
301
302
 
302
- try {
303
- const result = options.events?.interactionEnd?.emit();
304
- if (MaybePromise.is(result)) {
305
- return MaybePromise.then(result, undefined, handleObserverError);
303
+ if (options.events?.interactionEnd?.isObserved) {
304
+ try {
305
+ const result = options.events?.interactionEnd?.emit(session);
306
+ if (MaybePromise.is(result)) {
307
+ return MaybePromise.then(result, undefined, handleObserverError);
308
+ }
309
+ } catch (e) {
310
+ handleObserverError(e);
306
311
  }
307
- } catch (e) {
308
- handleObserverError(e);
309
312
  }
310
313
  },
311
314
  };
@@ -468,8 +471,15 @@ function createReference(resource: Transaction.Resource, internals: Internals, s
468
471
  // Enter exclusive mode. This will throw if my lock is unavailable
469
472
  transaction.beginSync();
470
473
 
471
- if (session.interactionComplete && !session.interactionComplete.isObservedBy(internals.interactionObserver)) {
472
- internals.events?.interactionBegin?.emit();
474
+ if (
475
+ !session.interactionStarted &&
476
+ session.interactionComplete &&
477
+ !session.interactionComplete.isObservedBy(internals.interactionObserver)
478
+ ) {
479
+ session.interactionStarted = true;
480
+ if (internals.events?.interactionBegin?.isObserved) {
481
+ internals.events?.interactionBegin?.emit(session);
482
+ }
473
483
  session.interactionComplete.on(internals.interactionObserver);
474
484
  }
475
485
  }
@@ -613,6 +623,13 @@ function createReference(resource: Transaction.Resource, internals: Internals, s
613
623
  if (changes) {
614
624
  // We don't revert the version number on rollback. Should be OK
615
625
  incrementVersion();
626
+
627
+ if (internals.events.stateChanged?.isObserved) {
628
+ changes.notifications.push({
629
+ event: internals.events.stateChanged,
630
+ params: [session],
631
+ });
632
+ }
616
633
  }
617
634
  }
618
635
 
@@ -678,7 +695,7 @@ function createReference(resource: Transaction.Resource, internals: Internals, s
678
695
  }
679
696
  }
680
697
 
681
- const changeSetResult = internals.events[stateChanged]?.emit(
698
+ const changeSetResult = internals.events[changed]?.emit(
682
699
  Array.from(changes.changeList.values()),
683
700
  internals.version,
684
701
  );
@@ -4,8 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import type { Transaction } from "#general";
8
- import { AsyncObservable } from "#general";
7
+ import { ActionContext } from "#behavior/context/ActionContext.js";
8
+ import type { AsyncObservable, Transaction } from "#general";
9
9
  import { DataModelPath, Schema } from "#model";
10
10
  import type { AccessControl, Val } from "#protocol";
11
11
  import type { ValidationLocation } from "../state/validation/location.js";
@@ -84,7 +84,12 @@ export namespace ValueSupervisor {
84
84
  /**
85
85
  * If present the session is associated with an online interaction. Emits when the interaction ends.
86
86
  */
87
- interactionComplete?: AsyncObservable<[]>;
87
+ interactionComplete?: AsyncObservable<[session?: ActionContext]>;
88
+
89
+ /**
90
+ * Set to true when the interaction has started and the interactionBegin event was emitted for this session
91
+ */
92
+ interactionStarted?: boolean;
88
93
 
89
94
  /**
90
95
  * If true, structs initialize without named properties which are more expensive to install. This is useful