@blac/core 2.0.0-rc-11 → 2.0.0-rc-14

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.
Files changed (55) hide show
  1. package/dist/index.cjs.js +2 -2
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +739 -260
  4. package/dist/index.es.js +1924 -1237
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +6 -7
  7. package/src/Blac.ts +82 -69
  8. package/src/Bloc.ts +45 -3
  9. package/src/BlocBase.ts +263 -454
  10. package/src/Cubit.ts +1 -16
  11. package/src/__tests__/Blac.config.test.ts +10 -17
  12. package/src/__tests__/Bloc.event.test.ts +27 -52
  13. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  14. package/src/__tests__/Cubit.test.ts +39 -50
  15. package/src/__tests__/memory-leaks.test.ts +383 -0
  16. package/src/__tests__/plugins.test.ts +464 -0
  17. package/src/adapter/BlacAdapter.ts +347 -207
  18. package/src/adapter/ProxyFactory.ts +196 -255
  19. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  20. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  21. package/src/adapter/index.ts +0 -1
  22. package/src/errors/BlacError.ts +69 -0
  23. package/src/errors/ErrorManager.ts +101 -0
  24. package/src/errors/handleError.ts +114 -0
  25. package/src/errors/index.ts +13 -0
  26. package/src/events.ts +7 -1
  27. package/src/index.ts +10 -1
  28. package/src/lifecycle/BlocLifecycle.ts +156 -0
  29. package/src/lifecycle/index.ts +1 -0
  30. package/src/plugins/BlocPluginRegistry.ts +240 -0
  31. package/src/plugins/SystemPluginRegistry.ts +242 -0
  32. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  33. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  34. package/src/plugins/index.ts +8 -0
  35. package/src/plugins/types.ts +129 -0
  36. package/src/subscription/README.md +132 -0
  37. package/src/subscription/SubscriptionManager.ts +395 -0
  38. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  39. package/src/subscription/index.ts +2 -0
  40. package/src/subscription/types.ts +90 -0
  41. package/src/testing.ts +31 -37
  42. package/src/types.ts +4 -21
  43. package/src/utils/BatchingManager.ts +59 -0
  44. package/src/utils/RerenderLogger.ts +265 -0
  45. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  46. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  47. package/src/utils/__tests__/uuid.test.ts +144 -0
  48. package/src/utils/generateInstanceId.ts +54 -0
  49. package/src/utils/shallowEqual.ts +1 -1
  50. package/README.md +0 -329
  51. package/src/BlacObserver.ts +0 -186
  52. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  53. package/src/__tests__/props.test.ts +0 -256
  54. package/src/adapter/ConsumerTracker.ts +0 -241
  55. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blac/core",
3
- "version": "2.0.0-rc-11",
3
+ "version": "2.0.0-rc-14",
4
4
  "license": "MIT",
5
5
  "author": "Brendan Mullins <jsnanigans@gmail.com>",
6
6
  "main": "dist/index.cjs.js",
@@ -27,16 +27,15 @@
27
27
  "@testing-library/jest-dom": "^6.6.4",
28
28
  "@testing-library/user-event": "^14.6.1",
29
29
  "@vitest/browser": "^3.2.4",
30
- "jsdom": "^24.1.1",
30
+ "jsdom": "^26.1.0",
31
31
  "prettier": "^3.6.2",
32
- "typescript": "^5.8.3",
33
- "vite": "^5.3.1",
32
+ "typescript": "^5.9.2",
33
+ "vite": "^7.0.6",
34
34
  "vite-plugin-dts": "^4.5.4",
35
- "vitest": "^1.6.0"
35
+ "vitest": "^3.2.4"
36
36
  },
37
37
  "scripts": {
38
- "prettier": "prettier --write ./src",
39
- "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\"",
38
+ "format": "prettier --write \".\"",
40
39
  "test": "vitest run",
41
40
  "test:watch": "vitest --watch",
42
41
  "coverage": "vitest run --coverage",
package/src/Blac.ts CHANGED
@@ -1,18 +1,19 @@
1
- import { BlocBase, BlocInstanceId, BlocLifecycleState } from './BlocBase';
1
+ import { BlocBase, BlocInstanceId } from './BlocBase';
2
+ import { BlocLifecycleState } from './lifecycle/BlocLifecycle';
2
3
  import {
3
4
  BlocBaseAbstract,
4
5
  BlocConstructor,
5
6
  BlocHookDependencyArrayFn,
6
7
  BlocState,
7
- InferPropsFromGeneric,
8
8
  } from './types';
9
+ import { SystemPluginRegistry } from './plugins/SystemPluginRegistry';
10
+ import { BlacError, ErrorCategory, ErrorSeverity } from './errors/BlacError';
11
+ import { ErrorManager } from './errors/ErrorManager';
9
12
 
10
13
  /**
11
14
  * Configuration options for the Blac instance
12
15
  */
13
16
  export interface BlacConfig {
14
- /** Whether to expose the Blac instance globally */
15
- exposeBlacInstance?: boolean;
16
17
  /**
17
18
  * Whether to enable proxy dependency tracking for automatic re-render optimization.
18
19
  * When false, state changes always cause re-renders unless dependencies are manually specified.
@@ -24,7 +25,7 @@ export interface BlacConfig {
24
25
  export interface GetBlocOptions<B extends BlocBase<unknown>> {
25
26
  id?: string;
26
27
  selector?: BlocHookDependencyArrayFn<BlocState<B>>;
27
- props?: InferPropsFromGeneric<B>;
28
+ constructorParams?: ConstructorParameters<BlocConstructor<B>>[];
28
29
  onMount?: (bloc: B) => void;
29
30
  instanceRef?: string;
30
31
  throwIfNotFound?: boolean;
@@ -104,13 +105,13 @@ export class Blac {
104
105
  }
105
106
  /** Timestamp when the instance was created */
106
107
  createdAt = Date.now();
108
+ private errorManager = ErrorManager.getInstance();
107
109
  static get getAllBlocs() {
108
110
  return Blac.instance.getAllBlocs;
109
111
  }
110
112
 
111
113
  /** Private static configuration */
112
114
  private static _config: BlacConfig = {
113
- exposeBlacInstance: false,
114
115
  proxyDependencyTracking: true,
115
116
  };
116
117
 
@@ -126,14 +127,16 @@ export class Blac {
126
127
  */
127
128
  static setConfig(config: Partial<BlacConfig>): void {
128
129
  // Validate config
129
- if (config.proxyDependencyTracking !== undefined &&
130
- typeof config.proxyDependencyTracking !== 'boolean') {
131
- throw new Error('BlacConfig.proxyDependencyTracking must be a boolean');
132
- }
133
-
134
- if (config.exposeBlacInstance !== undefined &&
135
- typeof config.exposeBlacInstance !== 'boolean') {
136
- throw new Error('BlacConfig.exposeBlacInstance must be a boolean');
130
+ if (
131
+ config.proxyDependencyTracking !== undefined &&
132
+ typeof config.proxyDependencyTracking !== 'boolean'
133
+ ) {
134
+ const error = new BlacError(
135
+ 'BlacConfig.proxyDependencyTracking must be a boolean',
136
+ ErrorCategory.VALIDATION,
137
+ ErrorSeverity.FATAL,
138
+ );
139
+ this.instance.errorManager.handle(error);
137
140
  }
138
141
 
139
142
  // Merge with existing config
@@ -162,6 +165,8 @@ export class Blac {
162
165
  keepAliveBlocs: Set<BlocBase<unknown>> = new Set();
163
166
  /** Flag to control whether changes should be posted to document */
164
167
  postChangesToDocument = false;
168
+ /** System plugin registry */
169
+ readonly plugins = new SystemPluginRegistry();
165
170
 
166
171
  /**
167
172
  * Creates a new Blac instance.
@@ -173,6 +178,9 @@ export class Blac {
173
178
  return Blac.instance;
174
179
  }
175
180
  instanceManager.setInstance(this);
181
+
182
+ // Bootstrap plugins on creation
183
+ this.plugins.bootstrap();
176
184
  }
177
185
 
178
186
  /** Flag to enable/disable logging */
@@ -213,9 +221,7 @@ export class Blac {
213
221
  * @param args - Additional arguments
214
222
  */
215
223
  warn = (message: string, ...args: unknown[]) => {
216
- if (Blac.enableLog) {
217
- // Logging disabled - console.warn removed
218
- }
224
+ console.warn(message, ...args);
219
225
  };
220
226
  static get warn() {
221
227
  return Blac.instance.warn;
@@ -227,7 +233,7 @@ export class Blac {
227
233
  */
228
234
  error = (message: string, ...args: unknown[]) => {
229
235
  if (Blac.enableLog) {
230
- // Logging disabled - console.error removed
236
+ console.error(message, ...args);
231
237
  }
232
238
  };
233
239
  static get error() {
@@ -311,7 +317,10 @@ export class Blac {
311
317
  );
312
318
 
313
319
  // First dispose the bloc to prevent further operations
314
- bloc._dispose();
320
+ bloc.dispose();
321
+
322
+ // Notify plugins of bloc disposal
323
+ this.plugins.notifyBlocDisposed(bloc);
315
324
 
316
325
  // Then clean up from registries
317
326
  if (base.isolated) {
@@ -384,6 +393,12 @@ export class Blac {
384
393
 
385
394
  const key = this.createBlocInstanceMapKey(blocClass.name, id);
386
395
  const found = this.blocInstanceMap.get(key) as InstanceType<B> | undefined;
396
+
397
+ // Don't return disposed blocs
398
+ if (found && (found as any).isDisposed) {
399
+ return undefined;
400
+ }
401
+
387
402
  return found;
388
403
  }
389
404
 
@@ -468,7 +483,7 @@ export class Blac {
468
483
  return undefined;
469
484
  }
470
485
  // Find the specific bloc by ID within the isolated array
471
- const found = blocs.find((b) => b._id === id) as
486
+ const found = blocs.find((b) => b._id === id && !(b as any).isDisposed) as
472
487
  | InstanceType<B>
473
488
  | undefined;
474
489
  return found;
@@ -496,21 +511,26 @@ export class Blac {
496
511
  id: BlocInstanceId,
497
512
  options: GetBlocOptions<InstanceType<B>> = {},
498
513
  ): InstanceType<B> {
499
- const { props, instanceRef } = options;
500
- const newBloc = new blocClass(props as never) as InstanceType<B>;
514
+ const { constructorParams, instanceRef } = options;
515
+ const newBloc = new blocClass(constructorParams) as InstanceType<B>;
516
+ newBloc.blacInstance = this;
501
517
  newBloc._instanceRef = instanceRef;
502
- newBloc.props = props || null;
503
- newBloc._updateId(id);
518
+ newBloc._id = id;
504
519
 
505
520
  // Set up disposal handler to break circular dependency
506
- newBloc._setDisposalHandler((bloc) => this.disposeBloc(bloc));
521
+ newBloc.setDisposalHandler((bloc) => this.disposeBloc(bloc));
507
522
 
508
523
  if (newBloc.isIsolated) {
509
524
  this.registerIsolatedBlocInstance(newBloc);
510
- return newBloc;
525
+ } else {
526
+ this.registerBlocInstance(newBloc);
511
527
  }
512
528
 
513
- this.registerBlocInstance(newBloc);
529
+ // Plugins are activated in constructor
530
+
531
+ // Notify system plugins of bloc creation
532
+ this.plugins.notifyBlocCreated(newBloc);
533
+
514
534
  return newBloc;
515
535
  }
516
536
 
@@ -617,38 +637,6 @@ export class Blac {
617
637
  return Blac.instance.getBloc;
618
638
  }
619
639
 
620
- /**
621
- * Gets a bloc instance or throws an error if it doesn't exist
622
- * @param blocClass - The bloc class to get
623
- * @param options - Options including:
624
- * - id: The instance ID (defaults to class name if not provided)
625
- * - props: Properties to pass to the bloc constructor
626
- * - instanceRef: Optional reference string for the instance
627
- */
628
- getBlocOrThrow = <B extends BlocConstructor<any>>(
629
- blocClass: B,
630
- options: {
631
- id?: BlocInstanceId;
632
- props?: InferPropsFromGeneric<B>;
633
- instanceRef?: string;
634
- } = {},
635
- ): InstanceType<B> => {
636
- const isIsolated = (blocClass as unknown as BlocBaseAbstract).isolated;
637
- const id = options.id || blocClass.name;
638
-
639
- const registered = isIsolated
640
- ? this.findIsolatedBlocInstance(blocClass, id)
641
- : this.findRegisteredBlocInstance(blocClass, id);
642
-
643
- if (registered) {
644
- return registered;
645
- }
646
- throw new Error(`Bloc ${blocClass.name} not found`);
647
- };
648
- static get getBlocOrThrow() {
649
- return Blac.instance.getBlocOrThrow;
650
- }
651
-
652
640
  /**
653
641
  * Gets all instances of a specific bloc class
654
642
  * @param blocClass - The bloc class to search for
@@ -663,7 +651,6 @@ export class Blac {
663
651
  } = {},
664
652
  ): InstanceType<B>[] => {
665
653
  const results: InstanceType<B>[] = [];
666
- // const blocClassName = (blocClass as any).name; // Temporarily removed for debugging
667
654
 
668
655
  // Search non-isolated blocs
669
656
  this.blocInstanceMap.forEach((blocInstance) => {
@@ -699,7 +686,7 @@ export class Blac {
699
686
  }
700
687
  }
701
688
 
702
- toDispose.forEach((bloc) => bloc._dispose());
689
+ toDispose.forEach((bloc) => bloc.dispose());
703
690
  };
704
691
  static get disposeKeepAliveBlocs() {
705
692
  return Blac.instance.disposeKeepAliveBlocs;
@@ -728,7 +715,7 @@ export class Blac {
728
715
  }
729
716
  }
730
717
 
731
- toDispose.forEach((bloc) => bloc._dispose());
718
+ toDispose.forEach((bloc) => bloc.dispose());
732
719
  };
733
720
  static get disposeBlocs() {
734
721
  return Blac.instance.disposeBlocs;
@@ -754,26 +741,23 @@ export class Blac {
754
741
  }
755
742
 
756
743
  /**
757
- * Validates consumer references and cleans up orphaned consumers
744
+ * Validates subscription references and cleans up orphaned blocs
758
745
  */
759
746
  validateConsumers = (): void => {
760
747
  for (const bloc of this.uidRegistry.values()) {
761
- // Validate consumers using the bloc's own validation method
762
- bloc._validateConsumers();
763
-
764
748
  // Check if bloc should be disposed after validation
765
749
  // TODO: Type assertion for private property access (see explanation above)
766
750
  if (
767
- bloc._consumers.size === 0 &&
751
+ bloc.subscriptionCount === 0 &&
768
752
  !bloc._keepAlive &&
769
753
  (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
770
754
  ) {
771
- // Schedule disposal for blocs with no consumers
755
+ // Schedule disposal for blocs with no subscriptions
772
756
  setTimeout(() => {
773
757
  // Double-check conditions before disposal
774
758
  // TODO: Type assertion for private property access (see explanation above)
775
759
  if (
776
- bloc._consumers.size === 0 &&
760
+ bloc.subscriptionCount === 0 &&
777
761
  !bloc._keepAlive &&
778
762
  (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
779
763
  ) {
@@ -786,4 +770,33 @@ export class Blac {
786
770
  static get validateConsumers() {
787
771
  return Blac.instance.validateConsumers;
788
772
  }
773
+
774
+ /**
775
+ * Bootstrap the Blac instance and all plugins
776
+ */
777
+ bootstrap(): void {
778
+ this.plugins.bootstrap();
779
+ }
780
+
781
+ /**
782
+ * Shutdown the Blac instance and all plugins
783
+ */
784
+ shutdown(): void {
785
+ this.plugins.shutdown();
786
+
787
+ // Dispose all non-keepAlive blocs
788
+ for (const bloc of this.blocInstanceMap.values()) {
789
+ if (!bloc._keepAlive) {
790
+ this.disposeBloc(bloc);
791
+ }
792
+ }
793
+
794
+ for (const blocs of this.isolatedBlocMap.values()) {
795
+ for (const bloc of blocs) {
796
+ if (!bloc._keepAlive) {
797
+ this.disposeBloc(bloc);
798
+ }
799
+ }
800
+ }
801
+ }
789
802
  }
package/src/Bloc.ts CHANGED
@@ -5,8 +5,7 @@ import { BlocEventConstraint } from './types';
5
5
  export abstract class Bloc<
6
6
  S, // State type
7
7
  A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
8
- P = unknown, // Props type
9
- > extends BlocBase<S, P> {
8
+ > extends BlocBase<S> {
10
9
  readonly eventHandlers: Map<
11
10
  new (...args: any[]) => A,
12
11
  (event: A, emit: (newState: S) => void) => void | Promise<void>
@@ -55,7 +54,31 @@ export abstract class Bloc<
55
54
  * @param action The action/event instance to be processed.
56
55
  */
57
56
  public add = async (action: A): Promise<void> => {
58
- this._eventQueue.push(action);
57
+ // Transform event through plugins
58
+ let transformedAction: A | null = action;
59
+ try {
60
+ transformedAction = (this._plugins as any).transformEvent(action);
61
+ } catch (error) {
62
+ console.error('Error transforming event:', error);
63
+ // Continue with original event if transformation fails
64
+ }
65
+
66
+ // If event was cancelled by plugin, don't process it
67
+ if (transformedAction === null) {
68
+ return;
69
+ }
70
+
71
+ // Notify bloc plugins of event
72
+ try {
73
+ (this._plugins as any).notifyEvent(transformedAction);
74
+ } catch (error) {
75
+ console.error('Error notifying plugins of event:', error);
76
+ }
77
+
78
+ // Notify system plugins of event
79
+ Blac.instance.plugins.notifyEventAdded(this as any, transformedAction);
80
+
81
+ this._eventQueue.push(transformedAction);
59
82
 
60
83
  if (!this._isProcessingEvent) {
61
84
  await this._processEventQueue();
@@ -119,6 +142,25 @@ export abstract class Bloc<
119
142
  errorContext,
120
143
  );
121
144
 
145
+ // Notify plugins of the error
146
+ if (error instanceof Error) {
147
+ try {
148
+ this._plugins.notifyError(error, {
149
+ phase: 'event-processing',
150
+ operation: 'handler',
151
+ metadata: { event: action },
152
+ });
153
+
154
+ Blac.instance.plugins.notifyError(error, this as any, {
155
+ phase: 'event-processing',
156
+ operation: 'handler',
157
+ metadata: { event: action },
158
+ });
159
+ } catch (pluginError) {
160
+ console.error('Error notifying plugins:', pluginError);
161
+ }
162
+ }
163
+
122
164
  if (error instanceof Error && error.name === 'CriticalError') {
123
165
  throw error;
124
166
  }