@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,315 +1,140 @@
1
- import { Blac, BlacObserver, BlocBase, BlocBaseAbstract, BlocConstructor, BlocHookDependencyArrayFn, BlocState, generateUUID } from '@blac/core';
1
+ import {
2
+ Blac,
3
+ BlocBase,
4
+ BlocConstructor,
5
+ BlocState,
6
+ InferPropsFromGeneric,
7
+ generateUUID,
8
+ } from '@blac/core';
2
9
  import { useCallback, useMemo, useRef } from 'react';
3
- import { BlocHookOptions } from './useBloc';
4
10
 
5
- export interface ExternalStore<
6
- B extends BlocConstructor<BlocBase<any>>
7
- > {
8
- /**
9
- * Subscribes to changes in the store and returns an unsubscribe function.
10
- * @param onStoreChange - Callback function that will be called whenever the store changes
11
- * @returns A function that can be called to unsubscribe from store changes
12
- */
13
- subscribe: (onStoreChange: (state: BlocState<InstanceType<B>>) => void) => () => void;
14
-
15
- /**
16
- * Gets the current snapshot of the store state.
17
- * @returns The current state of the store
18
- */
19
- getSnapshot: () => BlocState<InstanceType<B>> | undefined;
11
+ interface ExternalStoreOptions<B extends BlocBase<any>> {
12
+ id?: string;
13
+ props?: InferPropsFromGeneric<B>;
14
+ selector?: (
15
+ currentState: BlocState<B>,
16
+ previousState: BlocState<B>,
17
+ instance: B,
18
+ ) => any[];
19
+ }
20
20
 
21
- /**
22
- * Gets the server snapshot of the store state.
23
- * This is optional and defaults to the same value as getSnapshot.
24
- * @returns The server state of the store
25
- */
26
- getServerSnapshot?: () => BlocState<InstanceType<B>> | undefined;
21
+ interface ExternalStore<T> {
22
+ getSnapshot: () => T | undefined;
23
+ subscribe: (listener: () => void) => () => void;
24
+ getServerSnapshot?: () => T | undefined;
27
25
  }
28
26
 
29
- export interface ExternalBlacStore<
30
- B extends BlocConstructor<BlocBase<any>>
31
- > {
32
- usedKeys: React.RefObject<Set<string>>;
33
- usedClassPropKeys: React.RefObject<Set<string>>;
34
- externalStore: ExternalStore<B>;
35
- instance: React.RefObject<InstanceType<B>>;
27
+ interface ExternalBlocStoreResult<B extends BlocBase<any>> {
28
+ externalStore: ExternalStore<BlocState<B>>;
29
+ instance: { current: B | null };
30
+ usedKeys: { current: Set<string> };
31
+ usedClassPropKeys: { current: Set<string> };
36
32
  rid: string;
37
- hasProxyTracking: React.RefObject<boolean>;
38
33
  }
39
34
 
40
35
  /**
41
- * Creates an external store that wraps a Bloc instance, providing a React-compatible interface
42
- * for subscribing to and accessing bloc state.
36
+ * Hook for using an external bloc store
37
+ * Provides low-level access to bloc instances for use with React's useSyncExternalStore
43
38
  */
44
- const useExternalBlocStore = <
45
- B extends BlocConstructor<BlocBase<any>>
39
+ export default function useExternalBlocStore<
40
+ B extends BlocConstructor<BlocBase<any>>,
46
41
  >(
47
- bloc: B,
48
- options: BlocHookOptions<InstanceType<B>> | undefined,
49
- ): ExternalBlacStore<B> => {
50
- const { id: blocId, props, selector } = options ?? {};
51
-
52
- const rid = useMemo(() => {
53
- return generateUUID();
54
- }, []);
55
-
56
- const base = bloc as unknown as BlocBaseAbstract;
57
-
58
- const isIsolated = base.isolated;
59
- const effectiveBlocId = isIsolated ? rid : blocId;
60
-
61
- const usedKeys = useRef<Set<string>>(new Set());
62
- const usedClassPropKeys = useRef<Set<string>>(new Set());
63
-
64
- // Track whether proxy-based dependency tracking has been initialized
65
- // This helps distinguish between direct external store usage and useBloc proxy usage
66
- const hasProxyTracking = useRef<boolean>(false);
42
+ blocConstructor: B,
43
+ options: ExternalStoreOptions<InstanceType<B>> = {},
44
+ ): ExternalBlocStoreResult<InstanceType<B>> {
45
+ const ridRef = useRef<string>(`external-${generateUUID()}`);
46
+ const usedKeysRef = useRef<Set<string>>(new Set());
47
+ const usedClassPropKeysRef = useRef<Set<string>>(new Set());
48
+ const instanceRef = useRef<InstanceType<B> | null>(null);
49
+
50
+ // Get or create bloc instance
51
+ const bloc = useMemo(() => {
52
+ const blac = Blac.getInstance();
53
+ const base = blocConstructor as unknown as BlocBase<any>;
54
+
55
+ // For isolated blocs, always create a new instance
56
+ if (
57
+ (base.constructor as any).isolated ||
58
+ (blocConstructor as any).isolated
59
+ ) {
60
+ const newBloc = new blocConstructor(options?.props) as InstanceType<B>;
61
+ const uniqueId =
62
+ options?.id || `${blocConstructor.name}_${generateUUID()}`;
63
+ newBloc._updateId(uniqueId);
64
+ blac.activateBloc(newBloc);
65
+ return newBloc;
66
+ }
67
67
 
68
- const getBloc = useCallback(() => {
69
- return Blac.getBloc(bloc, {
70
- id: effectiveBlocId,
71
- props,
72
- instanceRef: rid
68
+ // For shared blocs, use the existing getBloc logic
69
+ return blac.getBloc(blocConstructor, {
70
+ id: options?.id,
71
+ props: options?.props,
73
72
  });
74
- }, [bloc, effectiveBlocId, props, rid]);
75
-
76
- const blocInstance = useRef<InstanceType<B>>(getBloc());
77
-
78
- // Update the instance when dependencies change
79
- useMemo(() => {
80
- blocInstance.current = getBloc();
81
- }, [getBloc]);
82
-
83
- // Track previous state and dependencies for selector
84
- const previousStateRef = useRef<BlocState<InstanceType<B>> | undefined>(undefined);
85
- const lastDependenciesRef = useRef<unknown[][] | undefined>(undefined);
86
- const lastStableSnapshot = useRef<BlocState<InstanceType<B>> | undefined>(undefined);
87
-
88
- // Track bloc instance uid to prevent unnecessary store recreation
89
- const blocUidRef = useRef<string | undefined>(undefined);
90
- if (blocUidRef.current !== blocInstance.current?.uid) {
91
- blocUidRef.current = blocInstance.current?.uid;
92
- }
93
-
94
- const dependencyArray = useMemo(
95
- () =>
96
- (newState: BlocState<InstanceType<B>>, oldState?: BlocState<InstanceType<B>>): unknown[][] => {
97
- const instance = blocInstance.current;
98
-
99
- if (!instance) {
100
- return [[], []]; // [stateArray, classArray]
101
- }
102
-
103
- // Use the provided oldState or fall back to our tracked previous state
104
- const previousState = oldState ?? previousStateRef.current;
105
-
106
- let currentDependencies: unknown[][];
107
-
108
- // Use custom dependency selector if provided
109
- if (selector) {
110
- const flatDeps = selector(newState, previousState, instance);
111
- // Wrap flat custom selector result in the two-array structure for consistency
112
- currentDependencies = [flatDeps, []]; // [customSelectorDeps, classArray]
113
- }
114
- // Fall back to bloc's default dependency selector if available
115
- else if (instance.defaultDependencySelector) {
116
- const flatDeps = instance.defaultDependencySelector(newState, previousState, instance);
117
- // Wrap flat default selector result in the two-array structure for consistency
118
- currentDependencies = [flatDeps, []]; // [defaultSelectorDeps, classArray]
119
- }
120
- // For primitive states, use default selector
121
- else if (typeof newState !== 'object') {
122
- // Default behavior for primitive states: re-render if the state itself changes.
123
- currentDependencies = [[newState], []]; // [primitiveStateArray, classArray]
73
+ }, [blocConstructor, options?.id]);
74
+
75
+ // Create external store interface
76
+ const externalStore = useMemo<ExternalStore<BlocState<InstanceType<B>>>>(
77
+ () => ({
78
+ getSnapshot: () => {
79
+ const currentInstance = instanceRef.current;
80
+ return currentInstance ? currentInstance.state : undefined;
81
+ },
82
+ subscribe: (listener: () => void) => {
83
+ const currentInstance = instanceRef.current;
84
+ if (!currentInstance) {
85
+ // Return no-op unsubscribe if no instance
86
+ return () => {};
124
87
  }
125
- else {
126
- // For object states, track which properties were actually used
127
- const stateDependencies: unknown[] = [];
128
- const classDependencies: unknown[] = [];
129
-
130
- // Add state property values that were accessed
131
- for (const key of usedKeys.current) {
132
- if (key in newState) {
133
- stateDependencies.push(newState[key as keyof typeof newState]);
134
- }
135
- }
136
88
 
137
- // Add class property values that were accessed
138
- for (const key of usedClassPropKeys.current) {
139
- if (key in instance) {
140
- try {
141
- const value = instance[key as keyof InstanceType<B>];
142
- if (typeof value !== 'function') {
143
- classDependencies.push(value);
144
- }
145
- } catch (error) {
146
- Blac.instance.log('useBloc Error', error);
147
- }
89
+ // Wrap listener to handle errors gracefully
90
+ const safeListener = (
91
+ newState: BlocState<InstanceType<B>>,
92
+ oldState: BlocState<InstanceType<B>>,
93
+ ) => {
94
+ try {
95
+ // Reset tracking keys on each listener call
96
+ usedKeysRef.current.clear();
97
+ usedClassPropKeysRef.current.clear();
98
+
99
+ // Call selector if provided
100
+ if (options.selector && currentInstance) {
101
+ options.selector(newState, oldState, currentInstance);
148
102
  }
149
- }
150
103
 
151
- // If no properties have been accessed through proxy
152
- if (usedKeys.current.size === 0 && usedClassPropKeys.current.size === 0) {
153
- // If proxy tracking has never been initialized, this is direct external store usage
154
- // In this case, always track the entire state to ensure notifications
155
- if (!hasProxyTracking.current) {
156
- stateDependencies.push(newState);
104
+ // Call listener with state if it expects it
105
+ if (listener.length > 0) {
106
+ (listener as any)(newState);
107
+ } else {
108
+ listener();
157
109
  }
158
- // If proxy tracking was initialized but no properties accessed,
159
- // return empty dependencies to prevent unnecessary re-renders
110
+ } catch (error) {
111
+ console.error('Listener error in useExternalBlocStore:', error);
112
+ // Don't rethrow to prevent breaking other listeners
160
113
  }
161
-
162
- currentDependencies = [stateDependencies, classDependencies];
163
- }
164
-
165
- // Update tracked state
166
- previousStateRef.current = newState;
167
-
168
-
169
- // Return the dependencies for BlacObserver to compare
170
- return currentDependencies;
171
- },
172
- [],
173
- );
174
-
175
- // Store active subscriptions to reuse observers
176
- const activeObservers = useRef<Map<Function, { observer: BlacObserver<BlocState<InstanceType<B>>>, unsubscribe: () => void }>>(new Map());
177
-
178
- const state: ExternalStore<B> = useMemo(() => {
179
- return {
180
- subscribe: (listener: (state: BlocState<InstanceType<B>>) => void) => {
181
-
182
- const currentInstance = blocInstance.current;
183
- if (!currentInstance) {
184
- return () => {}; // Return no-op if no instance
185
- }
186
-
187
- // Check if we already have an observer for this listener
188
- const existing = activeObservers.current.get(listener);
189
- if (existing) {
190
- return existing.unsubscribe;
191
- }
192
- const observer: BlacObserver<BlocState<InstanceType<B>>> = {
193
- fn: () => {
194
- try {
195
-
196
- // Only reset dependency tracking if we're not using a custom selector
197
- // Custom selectors override proxy-based tracking entirely
198
- if (!selector && !currentInstance.defaultDependencySelector) {
199
- usedKeys.current = new Set();
200
- usedClassPropKeys.current = new Set();
201
- }
202
-
203
- // Only trigger listener if there are actual subscriptions
204
- listener(currentInstance.state);
205
- } catch (e) {
206
- // Log any errors that occur during the listener callback
207
- // This ensures errors in listeners don't break the entire application
208
- console.error({
209
- e,
210
- blocInstance: currentInstance,
211
- dependencyArray,
212
- });
213
- }
214
- },
215
- // Pass the dependency array to control when the subscription is updated
216
- dependencyArray,
217
- // Use the provided id to identify this subscription
218
- id: rid,
219
- }
220
-
221
- Blac.activateBloc(currentInstance);
222
-
223
- // Subscribe to the bloc's observer with the provided listener function
224
- // This will trigger the callback whenever the bloc's state changes
225
- const unSub = currentInstance._observer.subscribe(observer);
226
-
227
- const unsubscribe = () => {
228
- activeObservers.current.delete(listener);
229
- unSub();
230
114
  };
231
115
 
232
- // Store the observer and unsubscribe function
233
- activeObservers.current.set(listener, { observer, unsubscribe });
234
-
235
- // Return an unsubscribe function that can be called to clean up the subscription
116
+ const unsubscribe = currentInstance._observer.subscribe({
117
+ id: ridRef.current,
118
+ fn: safeListener,
119
+ });
236
120
  return unsubscribe;
237
121
  },
238
- // Return an immutable snapshot of the current bloc state
239
- getSnapshot: (): BlocState<InstanceType<B>> | undefined => {
240
- const instance = blocInstance.current;
241
- if (!instance) {
242
- return {} as BlocState<InstanceType<B>>;
243
- }
244
-
245
- const currentState = instance.state;
246
- const currentDependencies = dependencyArray(currentState, previousStateRef.current);
247
-
248
- // Check if dependencies have changed using the two-array comparison logic
249
- const lastDeps = lastDependenciesRef.current;
250
- let dependenciesChanged = false;
251
-
252
- if (!lastDeps) {
253
- // First time - dependencies changed
254
- dependenciesChanged = true;
255
- } else if (lastDeps.length !== currentDependencies.length) {
256
- // Array structure changed
257
- dependenciesChanged = true;
258
- } else {
259
- // Compare each array (state and class dependencies)
260
- for (let arrayIndex = 0; arrayIndex < currentDependencies.length; arrayIndex++) {
261
- const lastArray = lastDeps[arrayIndex] || [];
262
- const newArray = currentDependencies[arrayIndex] || [];
263
-
264
- if (lastArray.length !== newArray.length) {
265
- dependenciesChanged = true;
266
- break;
267
- }
268
-
269
- // Compare each dependency value using Object.is
270
- for (let i = 0; i < newArray.length; i++) {
271
- if (!Object.is(lastArray[i], newArray[i])) {
272
- dependenciesChanged = true;
273
- break;
274
- }
275
- }
276
-
277
- if (dependenciesChanged) break;
278
- }
279
- }
280
-
281
- // Update dependency tracking
282
- lastDependenciesRef.current = currentDependencies;
283
-
284
- // If dependencies haven't changed, return the same snapshot reference
285
- // This prevents React from re-rendering when dependencies are stable
286
- if (!dependenciesChanged && lastStableSnapshot.current) {
287
- return lastStableSnapshot.current;
288
- }
289
-
290
- // Dependencies changed - update and return new snapshot
291
- lastStableSnapshot.current = currentState;
292
- return currentState;
122
+ getServerSnapshot: () => {
123
+ const currentInstance = instanceRef.current;
124
+ return currentInstance ? currentInstance.state : undefined;
293
125
  },
294
- // Server snapshot mirrors the client snapshot in this implementation
295
- getServerSnapshot: (): BlocState<InstanceType<B>> | undefined => {
296
- const instance = blocInstance.current;
297
- if (!instance) {
298
- return {} as BlocState<InstanceType<B>>;
299
- }
300
- return instance.state;
301
- },
302
- }
303
- }, []); // Store is stable - individual methods handle instance changes
126
+ }),
127
+ [],
128
+ );
129
+
130
+ // Update instance ref
131
+ instanceRef.current = bloc;
304
132
 
305
133
  return {
306
- usedKeys,
307
- usedClassPropKeys,
308
- externalStore: state,
309
- instance: blocInstance,
310
- rid,
311
- hasProxyTracking,
134
+ externalStore,
135
+ instance: instanceRef,
136
+ usedKeys: usedKeysRef,
137
+ usedClassPropKeys: usedClassPropKeysRef,
138
+ rid: ridRef.current,
312
139
  };
313
- };
314
-
315
- export default useExternalBlocStore;
140
+ }