@blac/react 2.0.0-rc-8 → 2.0.0-rc-11

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.
@@ -1,350 +0,0 @@
1
-
2
- export interface DependencyMetrics {
3
- stateAccessCount: number;
4
- classAccessCount: number;
5
- proxyCreationCount: number;
6
- batchFlushCount: number;
7
- averageResolutionTime: number;
8
- memoryUsageKB: number;
9
- }
10
-
11
- export interface DependencyTrackerConfig {
12
- enableBatching: boolean;
13
- batchTimeout: number;
14
- enableMetrics: boolean;
15
- maxCacheSize: number;
16
- enableDeepTracking: boolean;
17
- }
18
-
19
- export type DependencyChangeCallback = (changedKeys: Set<string>) => void;
20
-
21
- export class DependencyTracker {
22
- private stateKeys = new Set<string>();
23
- private classKeys = new Set<string>();
24
- private batchedCallbacks = new Set<DependencyChangeCallback>();
25
- private flushScheduled = false;
26
- private flushTimeoutId: ReturnType<typeof setTimeout> | undefined;
27
-
28
- private metrics: DependencyMetrics = {
29
- stateAccessCount: 0,
30
- classAccessCount: 0,
31
- proxyCreationCount: 0,
32
- batchFlushCount: 0,
33
- averageResolutionTime: 0,
34
- memoryUsageKB: 0,
35
- };
36
-
37
- private resolutionTimes: number[] = [];
38
-
39
- private config: DependencyTrackerConfig;
40
-
41
- private stateProxyCache = new WeakMap<object, object>();
42
- private classProxyCache = new WeakMap<object, object>();
43
-
44
- private lastStateSnapshot: unknown = null;
45
- private lastClassSnapshot: unknown = null;
46
-
47
- constructor(config: Partial<DependencyTrackerConfig> = {}) {
48
- this.config = {
49
- enableBatching: true,
50
- batchTimeout: 0, // Use React's scheduler
51
- enableMetrics: process.env.NODE_ENV === 'development',
52
- maxCacheSize: 1000,
53
- enableDeepTracking: false,
54
- ...config,
55
- };
56
- }
57
-
58
- public trackStateAccess(key: string): void {
59
- if (this.config.enableMetrics) {
60
- this.metrics.stateAccessCount++;
61
- }
62
-
63
- this.stateKeys.add(key);
64
-
65
- if (this.config.enableBatching) {
66
- this.scheduleFlush();
67
- }
68
- }
69
-
70
- public trackClassAccess(key: string): void {
71
- if (this.config.enableMetrics) {
72
- this.metrics.classAccessCount++;
73
- }
74
-
75
- this.classKeys.add(key);
76
-
77
- if (this.config.enableBatching) {
78
- this.scheduleFlush();
79
- }
80
- }
81
-
82
- public createStateProxy<T extends object>(target: T, onAccess?: (prop: string) => void): T {
83
- const cachedProxy = this.stateProxyCache.get(target);
84
- if (cachedProxy) {
85
- return cachedProxy as T;
86
- }
87
-
88
- const startTime = this.config.enableMetrics ? performance.now() : 0;
89
-
90
- const proxy = new Proxy(target, {
91
- get: (obj: T, prop: string | symbol) => {
92
- if (typeof prop === 'string') {
93
- this.trackStateAccess(prop);
94
- onAccess?.(prop);
95
- }
96
-
97
- const value = obj[prop as keyof T];
98
-
99
- if (
100
- this.config.enableDeepTracking &&
101
- value &&
102
- typeof value === 'object' &&
103
- !Array.isArray(value)
104
- ) {
105
- return this.createStateProxy(value as object);
106
- }
107
-
108
- return value;
109
- },
110
-
111
- has: (obj: T, prop: string | symbol) => {
112
- return prop in obj;
113
- },
114
-
115
- ownKeys: (obj: T) => {
116
- return Reflect.ownKeys(obj);
117
- },
118
-
119
- getOwnPropertyDescriptor: (obj: T, prop: string | symbol) => {
120
- return Reflect.getOwnPropertyDescriptor(obj, prop);
121
- },
122
- });
123
-
124
- this.stateProxyCache.set(target, proxy);
125
-
126
- if (this.config.enableMetrics) {
127
- this.metrics.proxyCreationCount++;
128
- const endTime = performance.now();
129
- this.resolutionTimes.push(endTime - startTime);
130
- this.updateAverageResolutionTime();
131
- }
132
-
133
- return proxy;
134
- }
135
-
136
- public createClassProxy<T extends object>(target: T, onAccess?: (prop: string) => void): T {
137
- const cachedProxy = this.classProxyCache.get(target);
138
- if (cachedProxy) {
139
- return cachedProxy as T;
140
- }
141
-
142
- const startTime = this.config.enableMetrics ? performance.now() : 0;
143
-
144
- const proxy = new Proxy(target, {
145
- get: (obj: T, prop: string | symbol) => {
146
- const value = obj[prop as keyof T];
147
-
148
- if (typeof prop === 'string' && typeof value !== 'function') {
149
- this.trackClassAccess(prop);
150
- onAccess?.(prop);
151
- }
152
-
153
- return value;
154
- },
155
- });
156
-
157
- this.classProxyCache.set(target, proxy);
158
-
159
- if (this.config.enableMetrics) {
160
- this.metrics.proxyCreationCount++;
161
- const endTime = performance.now();
162
- this.resolutionTimes.push(endTime - startTime);
163
- this.updateAverageResolutionTime();
164
- }
165
-
166
- return proxy;
167
- }
168
-
169
- public getStateKeys(): Set<string> {
170
- return new Set(this.stateKeys);
171
- }
172
-
173
- public getClassKeys(): Set<string> {
174
- return new Set(this.classKeys);
175
- }
176
-
177
- public reset(): void {
178
- this.stateKeys.clear();
179
- this.classKeys.clear();
180
- this.cancelScheduledFlush();
181
- }
182
-
183
- public subscribe(callback: DependencyChangeCallback): () => void {
184
- this.batchedCallbacks.add(callback);
185
-
186
- return () => {
187
- this.batchedCallbacks.delete(callback);
188
- };
189
- }
190
-
191
- public computeDependencyArray<TState, TClass>(
192
- state: TState,
193
- classInstance: TClass,
194
- ): unknown[] {
195
- const startTime = this.config.enableMetrics ? performance.now() : 0;
196
-
197
- if (typeof state !== 'object' || state === null) {
198
- return [[state]];
199
- }
200
-
201
- const stateValues: unknown[] = [];
202
- for (const key of this.stateKeys) {
203
- if (key in (state as object)) {
204
- stateValues.push((state as any)[key]);
205
- }
206
- }
207
-
208
- const classValues: unknown[] = [];
209
- for (const key of this.classKeys) {
210
- if (key in (classInstance as object)) {
211
- try {
212
- const value = (classInstance as any)[key];
213
- if (typeof value !== 'function') {
214
- classValues.push(value);
215
- }
216
- } catch (error) {
217
- }
218
- }
219
- }
220
-
221
- if (this.config.enableMetrics) {
222
- const endTime = performance.now();
223
- this.resolutionTimes.push(endTime - startTime);
224
- this.updateAverageResolutionTime();
225
- }
226
-
227
- if (stateValues.length === 0 && classValues.length === 0) {
228
- return [[]];
229
- }
230
-
231
- if (classValues.length === 0) {
232
- return [stateValues];
233
- }
234
-
235
- if (stateValues.length === 0) {
236
- return [classValues];
237
- }
238
-
239
- return [stateValues, classValues];
240
- }
241
-
242
- public getMetrics(): DependencyMetrics {
243
- if (!this.config.enableMetrics) {
244
- return {
245
- stateAccessCount: 0,
246
- classAccessCount: 0,
247
- proxyCreationCount: 0,
248
- batchFlushCount: 0,
249
- averageResolutionTime: 0,
250
- memoryUsageKB: 0,
251
- };
252
- }
253
-
254
- const estimatedMemory =
255
- (this.stateKeys.size * 50) +
256
- (this.classKeys.size * 50) +
257
- (this.stateProxyCache instanceof WeakMap ? 100 : 0) +
258
- (this.classProxyCache instanceof WeakMap ? 100 : 0);
259
-
260
- return {
261
- ...this.metrics,
262
- memoryUsageKB: Math.round(estimatedMemory / 1024),
263
- };
264
- }
265
-
266
- public clearCaches(): void {
267
- this.stateProxyCache = new WeakMap();
268
- this.classProxyCache = new WeakMap();
269
- this.resolutionTimes = [];
270
-
271
- if (this.config.enableMetrics) {
272
- this.metrics = {
273
- stateAccessCount: 0,
274
- classAccessCount: 0,
275
- proxyCreationCount: 0,
276
- batchFlushCount: 0,
277
- averageResolutionTime: 0,
278
- memoryUsageKB: 0,
279
- };
280
- }
281
- }
282
-
283
- private scheduleFlush(): void {
284
- if (this.flushScheduled) {
285
- return;
286
- }
287
-
288
- this.flushScheduled = true;
289
-
290
- if (this.config.batchTimeout > 0) {
291
- this.flushTimeoutId = setTimeout(() => {
292
- this.flushBatchedChanges();
293
- }, this.config.batchTimeout);
294
- } else {
295
- Promise.resolve().then(() => this.flushBatchedChanges());
296
- }
297
- }
298
-
299
- private cancelScheduledFlush(): void {
300
- if (this.flushTimeoutId) {
301
- clearTimeout(this.flushTimeoutId);
302
- this.flushTimeoutId = undefined;
303
- }
304
- this.flushScheduled = false;
305
- }
306
-
307
- private flushBatchedChanges(): void {
308
- if (!this.flushScheduled) {
309
- return;
310
- }
311
-
312
- this.flushScheduled = false;
313
- this.flushTimeoutId = undefined;
314
-
315
- if (this.config.enableMetrics) {
316
- this.metrics.batchFlushCount++;
317
- }
318
-
319
- const allChangedKeys = new Set([...this.stateKeys, ...this.classKeys]);
320
-
321
- for (const callback of this.batchedCallbacks) {
322
- try {
323
- callback(allChangedKeys);
324
- } catch (error) {
325
- console.error('Error in dependency change callback:', error);
326
- }
327
- }
328
- }
329
-
330
- private updateAverageResolutionTime(): void {
331
- if (this.resolutionTimes.length === 0) {
332
- return;
333
- }
334
-
335
- if (this.resolutionTimes.length > 100) {
336
- this.resolutionTimes = this.resolutionTimes.slice(-100);
337
- }
338
-
339
- const sum = this.resolutionTimes.reduce((a, b) => a + b, 0);
340
- this.metrics.averageResolutionTime = sum / this.resolutionTimes.length;
341
- }
342
- }
343
-
344
- export function createDependencyTracker(
345
- config?: Partial<DependencyTrackerConfig>,
346
- ): DependencyTracker {
347
- return new DependencyTracker(config);
348
- }
349
-
350
- export const defaultDependencyTracker = createDependencyTracker();
package/src/useBloc.tsx DELETED
@@ -1,218 +0,0 @@
1
- import {
2
- BlocBase,
3
- BlocConstructor,
4
- BlocHookDependencyArrayFn,
5
- BlocState,
6
- InferPropsFromGeneric,
7
- } from '@blac/core';
8
- import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
9
- import useExternalBlocStore from './useExternalBlocStore';
10
- import { DependencyTracker } from './DependencyTracker';
11
-
12
- /**
13
- * Type definition for the return type of the useBloc hook
14
- * @template B - Bloc constructor type
15
- */
16
- type HookTypes<B extends BlocConstructor<BlocBase<any>>> = [
17
- BlocState<InstanceType<B>>,
18
- InstanceType<B>,
19
- ];
20
-
21
- /**
22
- * Configuration options for the useBloc hook
23
- */
24
- export interface BlocHookOptions<B extends BlocBase<any>> {
25
- id?: string;
26
- selector?: BlocHookDependencyArrayFn<BlocState<B>>;
27
- props?: InferPropsFromGeneric<B>;
28
- onMount?: (bloc: B) => void;
29
- onUnmount?: (bloc: B) => void;
30
- }
31
-
32
- /**
33
- * Default dependency selector that wraps the entire state in an array
34
- * @template T - State type
35
- * @returns {Array<Array<T>>} Dependency array containing the entire state
36
- */
37
-
38
- /**
39
- * React hook for integrating with Blac state management
40
- *
41
- * This hook connects a React component to a Bloc state container, providing
42
- * automatic re-rendering when relevant state changes, dependency tracking,
43
- * and proper lifecycle management.
44
- *
45
- * @template B - Bloc constructor type
46
- * @template O - BlocHookOptions type
47
- * @param {B} bloc - Bloc constructor class
48
- * @param {O} [options] - Configuration options for the hook
49
- * @returns {HookTypes<B>} Tuple containing [state, bloc instance]
50
- *
51
- * @example
52
- * const [state, counterBloc] = useBloc(CounterBloc);
53
- * // Access state
54
- * console.log(state.count);
55
- * // Call bloc methods
56
- * counterBloc.increment();
57
- */
58
- export default function useBloc<B extends BlocConstructor<BlocBase<any>>>(
59
- bloc: B,
60
- options?: BlocHookOptions<InstanceType<B>>,
61
- ): HookTypes<B> {
62
- const {
63
- externalStore,
64
- usedKeys,
65
- usedClassPropKeys,
66
- instance,
67
- rid,
68
- hasProxyTracking,
69
- } = useExternalBlocStore(bloc, options);
70
-
71
- const state = useSyncExternalStore<BlocState<InstanceType<B>>>(
72
- externalStore.subscribe,
73
- () => {
74
- const snapshot = externalStore.getSnapshot();
75
- if (snapshot === undefined) {
76
- throw new Error(
77
- `[useBloc] State snapshot is undefined for bloc ${bloc.name}. Bloc may not be properly initialized.`,
78
- );
79
- }
80
- return snapshot;
81
- },
82
- externalStore.getServerSnapshot
83
- ? () => {
84
- const serverSnapshot = externalStore.getServerSnapshot!();
85
- if (serverSnapshot === undefined) {
86
- throw new Error(
87
- `[useBloc] Server state snapshot is undefined for bloc ${bloc.name}. Bloc may not be properly initialized.`,
88
- );
89
- }
90
- return serverSnapshot;
91
- }
92
- : undefined,
93
- );
94
-
95
- const dependencyTracker = useRef<DependencyTracker | null>(null);
96
- if (!dependencyTracker.current) {
97
- dependencyTracker.current = new DependencyTracker({
98
- enableBatching: true,
99
- enableMetrics: process.env.NODE_ENV === 'development',
100
- enableDeepTracking: false,
101
- });
102
- }
103
-
104
- const stateProxyCache = useRef<WeakMap<object, object>>(new WeakMap());
105
- const classProxyCache = useRef<WeakMap<object, object>>(new WeakMap());
106
-
107
- const returnState = useMemo(() => {
108
- // If a custom selector is provided, don't use proxy tracking
109
- if (options?.selector) {
110
- return state;
111
- }
112
-
113
- hasProxyTracking.current = true;
114
-
115
- if (typeof state !== 'object' || state === null) {
116
- return state;
117
- }
118
-
119
- let proxy = stateProxyCache.current.get(state);
120
- if (!proxy) {
121
- proxy = new Proxy(state, {
122
- get(target, prop) {
123
- if (typeof prop === 'string') {
124
- usedKeys.current.add(prop);
125
- dependencyTracker.current?.trackStateAccess(prop);
126
- }
127
- const value = target[prop as keyof typeof target];
128
- return value;
129
- },
130
- has(target, prop) {
131
- return prop in target;
132
- },
133
- ownKeys(target) {
134
- return Reflect.ownKeys(target);
135
- },
136
- getOwnPropertyDescriptor(target, prop) {
137
- return Reflect.getOwnPropertyDescriptor(target, prop);
138
- },
139
- });
140
- stateProxyCache.current.set(state as object, proxy as object);
141
- }
142
- return proxy;
143
- }, [state]);
144
-
145
- const returnClass = useMemo(() => {
146
- if (!instance.current) {
147
- throw new Error(
148
- `[useBloc] Bloc instance is null for ${bloc.name}. This should never happen - bloc instance must be defined.`,
149
- );
150
- }
151
-
152
- let proxy = classProxyCache.current.get(instance.current);
153
- if (!proxy) {
154
- proxy = new Proxy(instance.current, {
155
- get(target, prop) {
156
- if (!target) {
157
- throw new Error(
158
- `[useBloc] Bloc target is null for ${bloc.name}. This should never happen - bloc target must be defined.`,
159
- );
160
- }
161
- const value = target[prop as keyof InstanceType<B>];
162
- if (typeof value !== 'function' && typeof prop === 'string') {
163
- usedClassPropKeys.current.add(prop);
164
- dependencyTracker.current?.trackClassAccess(prop);
165
- }
166
- return value;
167
- },
168
- });
169
- classProxyCache.current.set(instance.current, proxy);
170
- }
171
- return proxy;
172
- }, [instance.current?.uid]);
173
-
174
- const componentRef = useRef({});
175
-
176
- useEffect(() => {
177
- const currentInstance = instance.current;
178
- if (!currentInstance) return;
179
-
180
- currentInstance._addConsumer(rid, componentRef.current);
181
-
182
- options?.onMount?.(currentInstance);
183
-
184
- return () => {
185
- if (!currentInstance) {
186
- return;
187
- }
188
- options?.onUnmount?.(currentInstance);
189
- currentInstance._removeConsumer(rid);
190
-
191
- dependencyTracker.current?.reset();
192
- };
193
- }, [instance.current?.uid, rid]);
194
- useEffect(() => {
195
- if (process.env.NODE_ENV === 'development' && dependencyTracker.current) {
196
- const metrics = dependencyTracker.current.getMetrics();
197
- if (metrics.stateAccessCount > 0 || metrics.classAccessCount > 0) {
198
- console.debug(`[useBloc] ${bloc.name} Performance Metrics:`, metrics);
199
- }
200
- }
201
- });
202
-
203
- if (returnState === undefined) {
204
- throw new Error(
205
- `[useBloc] State is undefined for ${bloc.name}. This should never happen - state must be defined.`,
206
- );
207
- }
208
- if (!returnClass) {
209
- throw new Error(
210
- `[useBloc] Instance is null for ${bloc.name}. This should never happen - instance must be defined.`,
211
- );
212
- }
213
-
214
- return [returnState, returnClass] as [
215
- BlocState<InstanceType<B>>,
216
- InstanceType<B>,
217
- ];
218
- }