@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.
- package/dist/cjs/behavior/Events.d.ts +8 -3
- package/dist/cjs/behavior/Events.d.ts.map +1 -1
- package/dist/cjs/behavior/Events.js +5 -1
- package/dist/cjs/behavior/Events.js.map +1 -1
- package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js +3 -3
- package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
- package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts +16 -0
- package/dist/cjs/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -0
- package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js +119 -0
- package/dist/cjs/behavior/cluster/FabricScopedDataHandler.js.map +6 -0
- package/dist/cjs/behavior/cluster/index.d.ts +1 -0
- package/dist/cjs/behavior/cluster/index.d.ts.map +1 -1
- package/dist/cjs/behavior/cluster/index.js +1 -0
- package/dist/cjs/behavior/cluster/index.js.map +1 -1
- package/dist/cjs/behavior/context/server/OnlineContext.d.ts +2 -1
- package/dist/cjs/behavior/context/server/OnlineContext.d.ts.map +1 -1
- package/dist/cjs/behavior/context/server/OnlineContext.js +22 -7
- package/dist/cjs/behavior/context/server/OnlineContext.js.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.d.ts +6 -5
- package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.js +25 -14
- package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts +7 -3
- package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
- package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts +20 -36
- package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/access-control/AccessControlServer.js +153 -87
- package/dist/cjs/behaviors/access-control/AccessControlServer.js.map +1 -1
- package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
- package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js.map +2 -2
- package/dist/cjs/endpoint/properties/Behaviors.d.ts.map +1 -1
- package/dist/cjs/endpoint/properties/Behaviors.js +10 -0
- package/dist/cjs/endpoint/properties/Behaviors.js.map +1 -1
- package/dist/cjs/node/ServerNode.d.ts +2 -2
- package/dist/cjs/node/ServerNode.d.ts.map +1 -1
- package/dist/cjs/node/ServerNode.js +2 -2
- package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -1
- package/dist/cjs/node/server/InteractionServer.js +10 -44
- package/dist/cjs/node/server/InteractionServer.js.map +2 -2
- package/dist/cjs/node/server/ProtocolService.js +1 -1
- package/dist/cjs/node/server/ProtocolService.js.map +1 -1
- package/dist/cjs/node/server/ServerEnvironment.d.ts +3 -0
- package/dist/cjs/node/server/ServerEnvironment.d.ts.map +1 -1
- package/dist/cjs/node/server/ServerEnvironment.js +12 -2
- package/dist/cjs/node/server/ServerEnvironment.js.map +1 -1
- package/dist/esm/behavior/Events.d.ts +8 -3
- package/dist/esm/behavior/Events.d.ts.map +1 -1
- package/dist/esm/behavior/Events.js +5 -2
- package/dist/esm/behavior/Events.js.map +1 -1
- package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js +4 -4
- package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
- package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts +16 -0
- package/dist/esm/behavior/cluster/FabricScopedDataHandler.d.ts.map +1 -0
- package/dist/esm/behavior/cluster/FabricScopedDataHandler.js +99 -0
- package/dist/esm/behavior/cluster/FabricScopedDataHandler.js.map +6 -0
- package/dist/esm/behavior/cluster/index.d.ts +1 -0
- package/dist/esm/behavior/cluster/index.d.ts.map +1 -1
- package/dist/esm/behavior/cluster/index.js +1 -0
- package/dist/esm/behavior/cluster/index.js.map +1 -1
- package/dist/esm/behavior/context/server/OnlineContext.d.ts +2 -1
- package/dist/esm/behavior/context/server/OnlineContext.d.ts.map +1 -1
- package/dist/esm/behavior/context/server/OnlineContext.js +29 -9
- package/dist/esm/behavior/context/server/OnlineContext.js.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.d.ts +6 -5
- package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.js +25 -14
- package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/esm/behavior/supervision/ValueSupervisor.d.ts +7 -3
- package/dist/esm/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
- package/dist/esm/behaviors/access-control/AccessControlServer.d.ts +20 -36
- package/dist/esm/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
- package/dist/esm/behaviors/access-control/AccessControlServer.js +153 -88
- package/dist/esm/behaviors/access-control/AccessControlServer.js.map +1 -1
- package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
- package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
- package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js.map +1 -1
- package/dist/esm/endpoint/properties/Behaviors.d.ts.map +1 -1
- package/dist/esm/endpoint/properties/Behaviors.js +10 -0
- package/dist/esm/endpoint/properties/Behaviors.js.map +1 -1
- package/dist/esm/node/ServerNode.d.ts +2 -2
- package/dist/esm/node/ServerNode.d.ts.map +1 -1
- package/dist/esm/node/ServerNode.js +3 -3
- package/dist/esm/node/ServerNode.js.map +1 -1
- package/dist/esm/node/server/InteractionServer.d.ts.map +1 -1
- package/dist/esm/node/server/InteractionServer.js +10 -44
- package/dist/esm/node/server/InteractionServer.js.map +1 -1
- package/dist/esm/node/server/ProtocolService.js +1 -1
- package/dist/esm/node/server/ProtocolService.js.map +1 -1
- package/dist/esm/node/server/ServerEnvironment.d.ts +3 -0
- package/dist/esm/node/server/ServerEnvironment.d.ts.map +1 -1
- package/dist/esm/node/server/ServerEnvironment.js +12 -2
- package/dist/esm/node/server/ServerEnvironment.js.map +1 -1
- package/package.json +7 -7
- package/src/behavior/Events.ts +8 -3
- package/src/behavior/cluster/ClusterBehaviorUtil.ts +4 -4
- package/src/behavior/cluster/FabricScopedDataHandler.ts +142 -0
- package/src/behavior/cluster/index.ts +1 -0
- package/src/behavior/context/server/OnlineContext.ts +39 -9
- package/src/behavior/state/managed/Datasource.ts +37 -20
- package/src/behavior/supervision/ValueSupervisor.ts +8 -3
- package/src/behaviors/access-control/AccessControlServer.ts +210 -102
- package/src/behaviors/operational-credentials/OperationalCredentialsServer.ts +10 -18
- package/src/endpoint/properties/Behaviors.ts +12 -1
- package/src/node/ServerNode.ts +3 -3
- package/src/node/server/InteractionServer.ts +10 -63
- package/src/node/server/ProtocolService.ts +1 -1
- 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
|
+
}
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
107
|
-
return internals.events[
|
|
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
|
-
[
|
|
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[
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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 (
|
|
472
|
-
|
|
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[
|
|
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
|
|
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
|