@blac/core 2.0.0-rc-7 → 2.0.0-rc-10
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/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +194 -7
- package/dist/index.es.js +1096 -563
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +204 -99
- package/src/BlacObserver.ts +62 -25
- package/src/Bloc.ts +166 -149
- package/src/BlocBase.ts +269 -85
- package/src/Cubit.ts +1 -1
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/adapter/BlacAdapter.ts +262 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/index.ts +2 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +21 -6
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Blac } from '../Blac';
|
|
2
|
+
import { BlocBase } from '../BlocBase';
|
|
3
|
+
import { BlocConstructor, BlocState, InferPropsFromGeneric } from '../types';
|
|
4
|
+
import { generateUUID } from '../utils/uuid';
|
|
5
|
+
import { ConsumerTracker, DependencyArray } from './ConsumerTracker';
|
|
6
|
+
import { ProxyFactory } from './ProxyFactory';
|
|
7
|
+
|
|
8
|
+
export interface AdapterOptions<B extends BlocBase<any>> {
|
|
9
|
+
id?: string;
|
|
10
|
+
dependencies?: (bloc: B) => unknown[];
|
|
11
|
+
props?: InferPropsFromGeneric<B>;
|
|
12
|
+
onMount?: (bloc: B) => void;
|
|
13
|
+
onUnmount?: (bloc: B) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* BlacAdapter orchestrates the connection between Bloc instances and React components.
|
|
18
|
+
* It manages dependency tracking, lifecycle hooks, and proxy creation.
|
|
19
|
+
*/
|
|
20
|
+
export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
21
|
+
public readonly id = `consumer-${generateUUID()}`;
|
|
22
|
+
public readonly blocConstructor: B;
|
|
23
|
+
public readonly componentRef: { current: object } = { current: {} };
|
|
24
|
+
public blocInstance: InstanceType<B>;
|
|
25
|
+
|
|
26
|
+
// Core components
|
|
27
|
+
private consumerTracker: ConsumerTracker;
|
|
28
|
+
|
|
29
|
+
unmountTime: number = 0;
|
|
30
|
+
mountTime: number = 0;
|
|
31
|
+
|
|
32
|
+
// Dependency tracking
|
|
33
|
+
private dependencyValues?: unknown[];
|
|
34
|
+
private isUsingDependencies: boolean = false;
|
|
35
|
+
|
|
36
|
+
// Lifecycle state
|
|
37
|
+
private hasMounted = false;
|
|
38
|
+
private mountCount = 0;
|
|
39
|
+
|
|
40
|
+
options?: AdapterOptions<InstanceType<B>>;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
instanceProps: { componentRef: { current: object }; blocConstructor: B },
|
|
44
|
+
options?: typeof this.options,
|
|
45
|
+
) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.blocConstructor = instanceProps.blocConstructor;
|
|
48
|
+
this.componentRef = instanceProps.componentRef;
|
|
49
|
+
this.isUsingDependencies = !!options?.dependencies;
|
|
50
|
+
|
|
51
|
+
// Initialize consumer tracker
|
|
52
|
+
this.consumerTracker = new ConsumerTracker();
|
|
53
|
+
|
|
54
|
+
// Initialize bloc instance and register consumer
|
|
55
|
+
this.blocInstance = this.updateBlocInstance();
|
|
56
|
+
this.consumerTracker.register(instanceProps.componentRef.current, this.id);
|
|
57
|
+
|
|
58
|
+
// Initialize dependency values if using dependencies
|
|
59
|
+
if (this.isUsingDependencies && options?.dependencies) {
|
|
60
|
+
this.dependencyValues = options.dependencies(this.blocInstance);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
trackAccess(
|
|
65
|
+
consumerRef: object,
|
|
66
|
+
type: 'state' | 'class',
|
|
67
|
+
path: string,
|
|
68
|
+
value?: any,
|
|
69
|
+
): void {
|
|
70
|
+
this.consumerTracker.trackAccess(consumerRef, type, path, value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getConsumerDependencies(consumerRef: object): DependencyArray | null {
|
|
74
|
+
return this.consumerTracker.getDependencies(consumerRef);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
shouldNotifyConsumer(
|
|
78
|
+
consumerRef: object,
|
|
79
|
+
changedPaths: Set<string>,
|
|
80
|
+
): boolean {
|
|
81
|
+
const consumerInfo = this.consumerTracker.getConsumerInfo(consumerRef);
|
|
82
|
+
if (!consumerInfo) {
|
|
83
|
+
return true; // If consumer not registered yet, notify by default
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// First render - always notify to establish baseline
|
|
87
|
+
if (!consumerInfo.hasRendered) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Use built-in method from ConsumerTracker
|
|
92
|
+
return this.consumerTracker.shouldNotifyConsumer(consumerRef, changedPaths);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
updateLastNotified(consumerRef: object): void {
|
|
96
|
+
this.consumerTracker.updateLastNotified(consumerRef);
|
|
97
|
+
this.consumerTracker.setHasRendered(consumerRef, true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resetConsumerTracking(): void {
|
|
101
|
+
this.consumerTracker.resetTracking(this.componentRef.current);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createStateProxy = <T extends object>(props: { target: T }): T => {
|
|
105
|
+
return ProxyFactory.createStateProxy({
|
|
106
|
+
target: props.target,
|
|
107
|
+
consumerRef: this.componentRef.current,
|
|
108
|
+
consumerTracker: this as any,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
createClassProxy = <T extends object>(props: { target: T }): T => {
|
|
113
|
+
return ProxyFactory.createClassProxy({
|
|
114
|
+
target: props.target,
|
|
115
|
+
consumerRef: this.componentRef.current,
|
|
116
|
+
consumerTracker: this as any,
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
updateBlocInstance(): InstanceType<B> {
|
|
121
|
+
this.blocInstance = Blac.instance.getBloc<B>(this.blocConstructor, {
|
|
122
|
+
props: this.options?.props,
|
|
123
|
+
id: this.options?.id,
|
|
124
|
+
instanceRef: this.id,
|
|
125
|
+
});
|
|
126
|
+
return this.blocInstance;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
createSubscription = (options: { onChange: () => void }) => {
|
|
130
|
+
const unsubscribe = this.blocInstance._observer.subscribe({
|
|
131
|
+
id: this.id,
|
|
132
|
+
fn: (
|
|
133
|
+
newState: BlocState<InstanceType<B>>,
|
|
134
|
+
oldState: BlocState<InstanceType<B>>,
|
|
135
|
+
) => {
|
|
136
|
+
// Handle dependency-based change detection
|
|
137
|
+
if (this.isUsingDependencies && this.options?.dependencies) {
|
|
138
|
+
const newValues = this.options.dependencies(this.blocInstance);
|
|
139
|
+
const hasChanged = this.hasDependencyValuesChanged(
|
|
140
|
+
this.dependencyValues,
|
|
141
|
+
newValues,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!hasChanged) {
|
|
145
|
+
return; // Don't trigger re-render
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.dependencyValues = newValues;
|
|
149
|
+
} else {
|
|
150
|
+
// Check if any tracked values have changed (proxy-based tracking)
|
|
151
|
+
const consumerInfo = this.consumerTracker.getConsumerInfo(
|
|
152
|
+
this.componentRef.current,
|
|
153
|
+
);
|
|
154
|
+
if (consumerInfo && consumerInfo.hasRendered) {
|
|
155
|
+
// Only check dependencies if component has rendered at least once
|
|
156
|
+
const hasChanged = this.consumerTracker.hasValuesChanged(
|
|
157
|
+
this.componentRef.current,
|
|
158
|
+
newState,
|
|
159
|
+
this.blocInstance,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!hasChanged) {
|
|
163
|
+
return; // Don't trigger re-render
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
options.onChange();
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return unsubscribe;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
mount = (): void => {
|
|
176
|
+
// Re-run dependencies on mount to ensure fresh values
|
|
177
|
+
if (this.isUsingDependencies && this.options?.dependencies) {
|
|
178
|
+
this.dependencyValues = this.options.dependencies(this.blocInstance);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Lifecycle management
|
|
182
|
+
this.mountCount++;
|
|
183
|
+
this.blocInstance._addConsumer(this.id, this.componentRef.current);
|
|
184
|
+
|
|
185
|
+
// Call onMount callback if provided and not already called
|
|
186
|
+
if (!this.hasMounted) {
|
|
187
|
+
this.hasMounted = true;
|
|
188
|
+
this.mountTime = Date.now();
|
|
189
|
+
|
|
190
|
+
if (this.options?.onMount) {
|
|
191
|
+
try {
|
|
192
|
+
this.options.onMount(this.blocInstance);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
unmount = (): void => {
|
|
201
|
+
this.unmountTime = Date.now();
|
|
202
|
+
this.consumerTracker.unregister(this.componentRef.current);
|
|
203
|
+
this.blocInstance._removeConsumer(this.id);
|
|
204
|
+
|
|
205
|
+
// Call onUnmount callback
|
|
206
|
+
if (this.options?.onUnmount) {
|
|
207
|
+
try {
|
|
208
|
+
this.options.onUnmount(this.blocInstance);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// Don't re-throw on unmount to allow cleanup to continue
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
getProxyState = (
|
|
216
|
+
state: BlocState<InstanceType<B>>,
|
|
217
|
+
): BlocState<InstanceType<B>> => {
|
|
218
|
+
if (this.isUsingDependencies) {
|
|
219
|
+
return state; // Return raw state when using dependencies
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return ProxyFactory.getProxyState({
|
|
223
|
+
state,
|
|
224
|
+
consumerRef: this.componentRef.current,
|
|
225
|
+
consumerTracker: this as any,
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
getProxyBlocInstance = (): InstanceType<B> => {
|
|
230
|
+
if (this.isUsingDependencies) {
|
|
231
|
+
return this.blocInstance; // Return raw instance when using dependencies
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return ProxyFactory.getProxyBlocInstance({
|
|
235
|
+
blocInstance: this.blocInstance,
|
|
236
|
+
consumerRef: this.componentRef.current,
|
|
237
|
+
consumerTracker: this as any,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Expose calledOnMount for backward compatibility
|
|
242
|
+
get calledOnMount(): boolean {
|
|
243
|
+
return this.hasMounted;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private hasDependencyValuesChanged(
|
|
247
|
+
prev: unknown[] | undefined,
|
|
248
|
+
next: unknown[],
|
|
249
|
+
): boolean {
|
|
250
|
+
if (!prev) return true; // First run, always trigger
|
|
251
|
+
if (prev.length !== next.length) return true;
|
|
252
|
+
|
|
253
|
+
// Use Object.is for comparison (handles NaN, +0/-0 correctly)
|
|
254
|
+
for (let i = 0; i < prev.length; i++) {
|
|
255
|
+
if (!Object.is(prev[i], next[i])) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export interface DependencyArray {
|
|
2
|
+
statePaths: string[];
|
|
3
|
+
classPaths: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TrackedValue {
|
|
7
|
+
value: any;
|
|
8
|
+
lastAccessTime: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConsumerInfo {
|
|
12
|
+
id: string;
|
|
13
|
+
lastNotified: number;
|
|
14
|
+
hasRendered: boolean;
|
|
15
|
+
// Dependency tracking
|
|
16
|
+
stateAccesses: Set<string>;
|
|
17
|
+
classAccesses: Set<string>;
|
|
18
|
+
stateValues: Map<string, TrackedValue>;
|
|
19
|
+
classValues: Map<string, TrackedValue>;
|
|
20
|
+
accessCount: number;
|
|
21
|
+
lastAccessTime: number;
|
|
22
|
+
firstAccessTime: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ConsumerTracker manages both consumer registration and dependency tracking.
|
|
27
|
+
* It uses a WeakMap for automatic garbage collection when consumers are no longer referenced.
|
|
28
|
+
*/
|
|
29
|
+
export class ConsumerTracker {
|
|
30
|
+
private consumers = new WeakMap<object, ConsumerInfo>();
|
|
31
|
+
private registrationCount = 0;
|
|
32
|
+
private activeConsumers = 0;
|
|
33
|
+
|
|
34
|
+
register(consumerRef: object, consumerId: string): void {
|
|
35
|
+
this.registrationCount++;
|
|
36
|
+
|
|
37
|
+
const existingConsumer = this.consumers.get(consumerRef);
|
|
38
|
+
if (!existingConsumer) {
|
|
39
|
+
this.activeConsumers++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.consumers.set(consumerRef, {
|
|
43
|
+
id: consumerId,
|
|
44
|
+
lastNotified: Date.now(),
|
|
45
|
+
hasRendered: false,
|
|
46
|
+
stateAccesses: new Set(),
|
|
47
|
+
classAccesses: new Set(),
|
|
48
|
+
stateValues: new Map(),
|
|
49
|
+
classValues: new Map(),
|
|
50
|
+
accessCount: 0,
|
|
51
|
+
lastAccessTime: 0,
|
|
52
|
+
firstAccessTime: 0,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
unregister(consumerRef: object): void {
|
|
57
|
+
if (consumerRef) {
|
|
58
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
59
|
+
if (consumerInfo) {
|
|
60
|
+
this.consumers.delete(consumerRef);
|
|
61
|
+
this.activeConsumers = Math.max(0, this.activeConsumers - 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
trackAccess(
|
|
67
|
+
consumerRef: object,
|
|
68
|
+
type: 'state' | 'class',
|
|
69
|
+
path: string,
|
|
70
|
+
value?: any,
|
|
71
|
+
): void {
|
|
72
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
73
|
+
if (!consumerInfo) return;
|
|
74
|
+
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
if (!consumerInfo.firstAccessTime) {
|
|
78
|
+
consumerInfo.firstAccessTime = now;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
consumerInfo.accessCount++;
|
|
82
|
+
consumerInfo.lastAccessTime = now;
|
|
83
|
+
|
|
84
|
+
if (type === 'state') {
|
|
85
|
+
consumerInfo.stateAccesses.add(path);
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
consumerInfo.stateValues.set(path, { value, lastAccessTime: now });
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
consumerInfo.classAccesses.add(path);
|
|
91
|
+
if (value !== undefined) {
|
|
92
|
+
consumerInfo.classValues.set(path, { value, lastAccessTime: now });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resetTracking(consumerRef: object): void {
|
|
98
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
99
|
+
if (!consumerInfo) return;
|
|
100
|
+
|
|
101
|
+
consumerInfo.stateAccesses.clear();
|
|
102
|
+
consumerInfo.classAccesses.clear();
|
|
103
|
+
consumerInfo.stateValues.clear();
|
|
104
|
+
consumerInfo.classValues.clear();
|
|
105
|
+
consumerInfo.accessCount = 0;
|
|
106
|
+
consumerInfo.firstAccessTime = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getDependencies(consumerRef: object): DependencyArray | null {
|
|
110
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
111
|
+
if (!consumerInfo) return null;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
statePaths: Array.from(consumerInfo.stateAccesses),
|
|
115
|
+
classPaths: Array.from(consumerInfo.classAccesses),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
hasValuesChanged(
|
|
120
|
+
consumerRef: object,
|
|
121
|
+
newState: any,
|
|
122
|
+
newBlocInstance: any,
|
|
123
|
+
): boolean {
|
|
124
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
125
|
+
if (!consumerInfo) return true;
|
|
126
|
+
|
|
127
|
+
let hasChanged = false;
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
|
|
130
|
+
// If we haven't tracked any values yet, consider it changed to establish baseline
|
|
131
|
+
if (
|
|
132
|
+
consumerInfo.stateValues.size === 0 &&
|
|
133
|
+
consumerInfo.classValues.size === 0 &&
|
|
134
|
+
(consumerInfo.stateAccesses.size > 0 ||
|
|
135
|
+
consumerInfo.classAccesses.size > 0)
|
|
136
|
+
) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check state values
|
|
141
|
+
for (const [path, trackedValue] of consumerInfo.stateValues) {
|
|
142
|
+
try {
|
|
143
|
+
const currentValue = this.getValueAtPath(newState, path);
|
|
144
|
+
if (currentValue !== trackedValue.value) {
|
|
145
|
+
consumerInfo.stateValues.set(path, {
|
|
146
|
+
value: currentValue,
|
|
147
|
+
lastAccessTime: now,
|
|
148
|
+
});
|
|
149
|
+
hasChanged = true;
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
hasChanged = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check class getter values
|
|
157
|
+
for (const [path, trackedValue] of consumerInfo.classValues) {
|
|
158
|
+
try {
|
|
159
|
+
const currentValue = this.getValueAtPath(newBlocInstance, path);
|
|
160
|
+
if (currentValue !== trackedValue.value) {
|
|
161
|
+
consumerInfo.classValues.set(path, {
|
|
162
|
+
value: currentValue,
|
|
163
|
+
lastAccessTime: now,
|
|
164
|
+
});
|
|
165
|
+
hasChanged = true;
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
hasChanged = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return hasChanged;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
shouldNotifyConsumer(
|
|
176
|
+
consumerRef: object,
|
|
177
|
+
changedPaths: Set<string>,
|
|
178
|
+
): boolean {
|
|
179
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
180
|
+
if (!consumerInfo) return false;
|
|
181
|
+
|
|
182
|
+
// Check if any changed paths match tracked dependencies
|
|
183
|
+
for (const changedPath of changedPaths) {
|
|
184
|
+
if (consumerInfo.stateAccesses.has(changedPath)) return true;
|
|
185
|
+
if (consumerInfo.classAccesses.has(changedPath)) return true;
|
|
186
|
+
|
|
187
|
+
// Check for nested path changes
|
|
188
|
+
for (const trackedPath of consumerInfo.stateAccesses) {
|
|
189
|
+
if (
|
|
190
|
+
changedPath.startsWith(trackedPath + '.') ||
|
|
191
|
+
trackedPath.startsWith(changedPath + '.')
|
|
192
|
+
) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
updateLastNotified(consumerRef: object): void {
|
|
202
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
203
|
+
if (consumerInfo) {
|
|
204
|
+
consumerInfo.lastNotified = Date.now();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getConsumerInfo(consumerRef: object): ConsumerInfo | undefined {
|
|
209
|
+
return this.consumers.get(consumerRef);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setHasRendered(consumerRef: object, hasRendered: boolean): void {
|
|
213
|
+
const consumerInfo = this.consumers.get(consumerRef);
|
|
214
|
+
if (consumerInfo) {
|
|
215
|
+
consumerInfo.hasRendered = hasRendered;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
hasConsumer(consumerRef: object): boolean {
|
|
220
|
+
return this.consumers.has(consumerRef);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getStats() {
|
|
224
|
+
return {
|
|
225
|
+
totalRegistrations: this.registrationCount,
|
|
226
|
+
activeConsumers: this.activeConsumers,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private getValueAtPath(obj: any, path: string): any {
|
|
231
|
+
const parts = path.split('.');
|
|
232
|
+
let current = obj;
|
|
233
|
+
|
|
234
|
+
for (const part of parts) {
|
|
235
|
+
if (current == null) return undefined;
|
|
236
|
+
current = current[part];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return current;
|
|
240
|
+
}
|
|
241
|
+
}
|