@blac/core 2.0.0-rc-2 → 2.0.0-rc-4

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/src/BlocBase.ts CHANGED
@@ -1,6 +1,4 @@
1
- import { Blac, BlacLifecycleEvent } from './Blac';
2
1
  import { BlacObservable } from './BlacObserver';
3
- import BlacAddon from './addons/BlacAddon';
4
2
  import { BlocConstructor } from './types';
5
3
 
6
4
  export type BlocInstanceId = string | number | undefined;
@@ -10,7 +8,6 @@ type DependencySelector<S> = (newState: S) => unknown[][];
10
8
  interface BlocStaticProperties {
11
9
  isolated: boolean;
12
10
  keepAlive: boolean;
13
- addons?: BlacAddon[];
14
11
  }
15
12
 
16
13
  /**
@@ -25,6 +22,7 @@ export abstract class BlocBase<
25
22
  S,
26
23
  P = unknown
27
24
  > {
25
+ public uid = crypto.randomUUID();
28
26
  /**
29
27
  * When true, every consumer will receive its own unique instance of this Bloc.
30
28
  * Use this when state should not be shared between components.
@@ -50,12 +48,6 @@ export abstract class BlocBase<
50
48
  * When provided, observers will only be notified when selected dependencies change.
51
49
  */
52
50
  defaultDependencySelector: DependencySelector<S> | undefined;
53
-
54
- /**
55
- * @internal
56
- * Optional array of addons to extend the functionality of this Bloc.
57
- */
58
- public _addons?: BlacAddon[];
59
51
 
60
52
  /**
61
53
  * @internal
@@ -69,12 +61,6 @@ export abstract class BlocBase<
69
61
  */
70
62
  public _observer: BlacObservable<S>;
71
63
 
72
- /**
73
- * @internal
74
- * Reference to the global Blac manager instance.
75
- */
76
- public _blac = Blac.getInstance();
77
-
78
64
  /**
79
65
  * The unique identifier for this Bloc instance.
80
66
  * Defaults to the class name, but can be customized.
@@ -126,17 +112,14 @@ export abstract class BlocBase<
126
112
  constructor(initialState: S) {
127
113
  this._state = initialState;
128
114
  this._observer = new BlacObservable(this);
129
- this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CREATED, this);
130
115
  this._id = this.constructor.name;
131
116
 
132
- // Use a type assertion for the constructor to access static properties safely
133
- const constructorWithStaticProps = this.constructor as BlocConstructor<this> & BlocStaticProperties;
134
-
135
- this._keepAlive = constructorWithStaticProps.keepAlive;
136
- this._isolated = constructorWithStaticProps.isolated;
137
- this._addons = constructorWithStaticProps.addons;
138
-
139
- this._connectAddons();
117
+ // Access static properties safely with proper type checking
118
+ const Constructor = this.constructor as typeof BlocBase & BlocStaticProperties;
119
+
120
+ // Validate that the static properties exist and are boolean
121
+ this._keepAlive = typeof Constructor.keepAlive === 'boolean' ? Constructor.keepAlive : false;
122
+ this._isolated = typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
140
123
  }
141
124
 
142
125
  /**
@@ -174,9 +157,17 @@ export abstract class BlocBase<
174
157
  * Notifies the Blac manager and clears all observers.
175
158
  */
176
159
  _dispose() {
177
- this._observer.dispose();
178
- this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_DISPOSED, this);
160
+ // Clear all consumers
161
+ this._consumers.clear();
162
+
163
+ // Clear observer subscriptions
164
+ this._observer.clear();
165
+
166
+ // Call user-defined disposal hook
179
167
  this.onDispose?.();
168
+
169
+ // The Blac manager will handle removal from registry
170
+ // This method is called by Blac.disposeBloc, so we don't need to call it again
180
171
  }
181
172
 
182
173
  /**
@@ -191,16 +182,29 @@ export abstract class BlocBase<
191
182
  */
192
183
  _consumers = new Set<string>();
193
184
 
185
+ /**
186
+ * @internal
187
+ * WeakSet to track consumer references for cleanup validation
188
+ */
189
+ private _consumerRefs = new WeakSet<object>();
190
+
194
191
  /**
195
192
  * @internal
196
193
  * Registers a new consumer to this Bloc instance.
197
194
  * Notifies the Blac manager that a consumer has been added.
198
195
  *
199
196
  * @param consumerId The unique ID of the consumer being added
197
+ * @param consumerRef Optional reference to the consumer object for cleanup validation
200
198
  */
201
- _addConsumer = (consumerId: string) => {
199
+ _addConsumer = (consumerId: string, consumerRef?: object) => {
200
+ if (this._consumers.has(consumerId)) return;
202
201
  this._consumers.add(consumerId);
203
- this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
202
+
203
+ if (consumerRef) {
204
+ this._consumerRefs.add(consumerRef);
205
+ }
206
+
207
+ // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
204
208
  };
205
209
 
206
210
  /**
@@ -211,27 +215,61 @@ export abstract class BlocBase<
211
215
  * @param consumerId The unique ID of the consumer being removed
212
216
  */
213
217
  _removeConsumer = (consumerId: string) => {
214
- this._blac.log(`[${this._name}:${String(this._id ?? 'default_id')}] Removing consumer: ${consumerId}`);
218
+ if (!this._consumers.has(consumerId)) return;
215
219
  this._consumers.delete(consumerId);
216
- this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
220
+ // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
221
+
222
+ // If no consumers remain and not keep-alive, schedule disposal
223
+ if (this._consumers.size === 0 && !this._keepAlive) {
224
+ this._scheduleDisposal();
225
+ }
217
226
  };
218
227
 
219
228
  /**
220
229
  * @internal
221
- * Initializes all registered addons for this Bloc instance.
222
- * Calls the onInit lifecycle method on each addon if defined.
230
+ * Handler function for disposal (can be set by Blac manager)
223
231
  */
224
- _connectAddons = () => {
225
- const { _addons: addons } = this;
226
- if (addons) {
227
- addons.forEach(addon => {
228
- addon.onInit?.(this);
229
- });
230
- }
231
- };
232
+ private _disposalHandler?: (bloc: BlocBase<unknown>) => void;
233
+
234
+ /**
235
+ * @internal
236
+ * Sets the disposal handler for this bloc
237
+ */
238
+ _setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void) {
239
+ this._disposalHandler = handler;
240
+ }
241
+
242
+ /**
243
+ * @internal
244
+ * Schedules disposal of this bloc instance if it has no consumers
245
+ */
246
+ private _scheduleDisposal() {
247
+ // Use setTimeout to avoid disposal during render cycles
248
+ setTimeout(() => {
249
+ if (this._consumers.size === 0 && !this._keepAlive) {
250
+ if (this._disposalHandler) {
251
+ this._disposalHandler(this as any);
252
+ } else {
253
+ this._dispose();
254
+ }
255
+ }
256
+ }, 0);
257
+ }
232
258
 
233
259
  lastUpdate = Date.now();
234
260
 
261
+ /**
262
+ * @internal
263
+ * Flag to indicate if batching is enabled for this bloc
264
+ */
265
+ private _batchingEnabled = false;
266
+
267
+ /**
268
+ * @internal
269
+ * Pending state updates when batching is enabled
270
+ */
271
+ private _pendingUpdates: Array<{newState: S, oldState: S, action?: unknown}> = [];
272
+
235
273
  /**
236
274
  * @internal
237
275
  * Updates the state and notifies all observers of the change.
@@ -241,8 +279,59 @@ export abstract class BlocBase<
241
279
  * @param action Optional metadata about what caused the state change
242
280
  */
243
281
  _pushState = (newState: S, oldState: S, action?: unknown): void => {
282
+ // Runtime validation for state changes
283
+ if (newState === undefined) {
284
+ console.warn('BlocBase._pushState: newState is undefined', this);
285
+ return;
286
+ }
287
+
288
+ // Validate action type if provided
289
+ if (action !== undefined && action !== null) {
290
+ const actionType = typeof action;
291
+ if (!(['string', 'object', 'number'].includes(actionType))) {
292
+ console.warn('BlocBase._pushState: Invalid action type', this, action);
293
+ }
294
+ }
295
+
296
+ // If batching is enabled, queue the update
297
+ if (this._batchingEnabled) {
298
+ this._pendingUpdates.push({ newState, oldState, action });
299
+ return;
300
+ }
301
+
302
+ this._oldState = oldState;
244
303
  this._state = newState;
245
304
  this._observer.notify(newState, oldState, action);
246
305
  this.lastUpdate = Date.now();
247
306
  };
307
+
308
+ /**
309
+ * Enables batching for multiple state updates
310
+ * @param batchFn Function to execute with batching enabled
311
+ */
312
+ batch = <T>(batchFn: () => T): T => {
313
+ const wasBatching = this._batchingEnabled;
314
+ this._batchingEnabled = true;
315
+
316
+ try {
317
+ const result = batchFn();
318
+
319
+ // Process all pending updates
320
+ if (this._pendingUpdates.length > 0) {
321
+ const lastUpdate = this._pendingUpdates[this._pendingUpdates.length - 1];
322
+ this._oldState = this._pendingUpdates[0].oldState;
323
+ this._state = lastUpdate.newState;
324
+
325
+ // Notify with the final state
326
+ this._observer.notify(lastUpdate.newState, this._oldState, lastUpdate.action);
327
+ this.lastUpdate = Date.now();
328
+
329
+ this._pendingUpdates = [];
330
+ }
331
+
332
+ return result;
333
+ } finally {
334
+ this._batchingEnabled = wasBatching;
335
+ }
336
+ };
248
337
  }
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export * from './Blac';
2
2
  export * from './BlacObserver';
3
+ export * from './Bloc';
3
4
  export * from './BlocBase';
4
5
  export * from './Cubit';
5
- export * from './Bloc';
6
6
  export * from './types';
7
- export * from './BlacPlugin';
8
- export * from './addons';
7
+
@@ -1,85 +0,0 @@
1
- import { Blac, BlacLifecycleEvent } from './Blac';
2
- import { BlocBase } from './BlocBase';
3
- import { BlocHookDependencyArrayFn } from './types';
4
-
5
- /**
6
- * Represents an observer that can subscribe to state changes in a Bloc
7
- * @template S - The type of state being observed
8
- */
9
- export type BlacObserver<S> = {
10
- /** Function to be called when state changes */
11
- fn: (newState: S, oldState: S, action?: unknown) => void | Promise<void>;
12
- /** Optional function to determine if the observer should be notified of state changes */
13
- dependencyArray?: BlocHookDependencyArrayFn<S>;
14
- /** Cached state values used for dependency comparison */
15
- lastState?: unknown[][];
16
- /** Unique identifier for the observer */
17
- id: string;
18
- };
19
-
20
- /**
21
- * A class that manages observers for a Bloc's state changes
22
- * @template S - The type of state being observed
23
- */
24
- export class BlacObservable<S = unknown> {
25
- /** The Bloc instance this observable is associated with */
26
- bloc: BlocBase<S>;
27
-
28
- /**
29
- * Creates a new BlacObservable instance
30
- * @param bloc - The Bloc instance to observe
31
- */
32
- constructor(bloc: BlocBase<S>) {
33
- this.bloc = bloc;
34
- }
35
-
36
- private _observers = new Set<BlacObserver<S>>();
37
-
38
- /**
39
- * Gets the number of active observers
40
- * @returns The number of observers currently subscribed
41
- */
42
- get size(): number {
43
- return this._observers.size;
44
- }
45
-
46
- /**
47
- * Gets the set of all observers
48
- * @returns The Set of all BlacObserver instances
49
- */
50
- get observers() {
51
- return this._observers;
52
- }
53
-
54
- /**
55
- * Subscribes an observer to state changes
56
- * @param observer - The observer to subscribe
57
- * @returns A function that can be called to unsubscribe the observer
58
- */
59
- subscribe(observer: BlacObserver<S>): () => void {
60
- this._observers.add(observer);
61
- Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_ADDED, this.bloc, { listenerId: observer.id });
62
-
63
- // Immediately notify the new observer with the current state
64
- // Pass current state as both newState and oldState for initial notification context
65
- void observer.fn(this.bloc.state, this.bloc.state, { initialSubscription: true });
66
-
67
- if (!observer.lastState) {
68
- observer.lastState = observer.dependencyArray
69
- ? observer.dependencyArray(this.bloc.state)
70
- : [];
71
- }
72
- return () => {
73
- this.unsubscribe(observer);
74
- }
75
- }
76
-
77
- /**
78
- * Unsubscribes an observer from state changes
79
- * @param observer - The observer to unsubscribe
80
- */
81
- unsubscribe(observer: BlacObserver<S>) {
82
- this._observers.delete(observer);
83
- Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_REMOVED, this.bloc, { listenerId: observer.id });
84
- }
85
- }
package/src/BlacPlugin.ts DELETED
@@ -1,13 +0,0 @@
1
- import { BlacLifecycleEvent } from './Blac';
2
- import { BlocBase } from './BlocBase';
3
-
4
- export interface BlacPlugin {
5
- name: string;
6
-
7
- onEvent(
8
- event: BlacLifecycleEvent,
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- bloc: BlocBase<any>,
11
- params?: unknown,
12
- ): void;
13
- }
@@ -1,17 +0,0 @@
1
- import { BlocBase } from '../BlocBase';
2
-
3
- export type BlacAddonInit = (bloc: BlocBase<any>) => void;
4
-
5
- export type BlacAddonEmit = (params: {
6
- oldState: unknown;
7
- newState: unknown;
8
- cubit: BlocBase<any>;
9
- }) => void;
10
-
11
- type BlacAddon = {
12
- name: string;
13
- onInit?: BlacAddonInit;
14
- onEmit?: BlacAddonEmit;
15
- };
16
-
17
- export default BlacAddon;
@@ -1,105 +0,0 @@
1
- import { BlocBase, BlocInstanceId } from '../BlocBase';
2
- import BlacAddon, { BlacAddonEmit, BlacAddonInit } from './BlacAddon';
3
-
4
- type StorageType = 'localStorage' | 'sessionStorage';
5
-
6
- function getStorage(type: StorageType): Storage {
7
- switch (type) {
8
- case 'localStorage':
9
- return localStorage;
10
- case 'sessionStorage':
11
- return sessionStorage;
12
- default:
13
- return localStorage;
14
- }
15
- }
16
-
17
- /**
18
- * Persist addon
19
- *
20
- * @param options
21
- * @returns BlacAddon
22
- */
23
- export function Persist(
24
- options: {
25
- /**
26
- * @default 'blac'
27
- */
28
- keyPrefix?: string;
29
- /**
30
- * @default the bloc's id
31
- */
32
- keyName?: string;
33
- /**
34
- * Used when the value is not found in storage
35
- */
36
- defaultValue?: unknown;
37
-
38
- /**
39
- * @default 'localStorage'
40
- * @see StorageType
41
- */
42
- storageType?: StorageType;
43
-
44
- /**
45
- * @default false
46
- */
47
- onError?: (e: unknown) => void;
48
- } = {},
49
- ): BlacAddon {
50
- const {
51
- keyPrefix = 'blac',
52
- keyName,
53
- defaultValue,
54
- storageType = 'localStorage',
55
- } = options;
56
-
57
- const fullKey = (id: string | BlocInstanceId) => `${keyPrefix}:${String(id)}`;
58
-
59
- const getFromLocalStorage = (id: string | BlocInstanceId): unknown => {
60
- try {
61
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
62
- const value = getStorage(storageType).getItem(fullKey(id));
63
- if (typeof value !== 'string') {
64
- return defaultValue;
65
- }
66
-
67
- const p = JSON.parse(value) as { v: unknown };
68
- if (typeof p.v !== 'undefined') {
69
- return p.v;
70
- } else {
71
- return defaultValue;
72
- }
73
- } catch (e) {
74
- options.onError?.(e);
75
- return defaultValue;
76
- }
77
- };
78
-
79
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
- const onInit: BlacAddonInit = (e: BlocBase<any>) => {
81
- const id = keyName ?? e._id;
82
-
83
- const value = getFromLocalStorage(id);
84
- e._pushState(value, null);
85
- };
86
-
87
- let currentCachedValue = '';
88
- const onEmit: BlacAddonEmit = ({ newState, cubit }) => {
89
- const id = keyName ?? cubit._id;
90
-
91
- const newValue = JSON.stringify({ v: newState });
92
-
93
- if (newValue !== currentCachedValue) {
94
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
95
- getStorage(storageType).setItem(fullKey(id), newValue);
96
- currentCachedValue = newValue;
97
- }
98
- };
99
-
100
- return {
101
- name: 'Persist',
102
- onInit,
103
- onEmit,
104
- };
105
- }
@@ -1 +0,0 @@
1
- export * from './Persist';