11/** @module @category Config */
2- import { clone , reduce , mergeDeepRight , equals , omit } from 'ramda' ;
2+ import { clone , equals , reduce , mergeDeepRight , omit } from 'ramda' ;
33import type { Config , ConfigObject , ConfigSchema , ExtensionSlotConfig } from '../types' ;
44import { Type } from '../types' ;
55import { isArray , isBoolean , isUuid , isNumber , isObject , isString } from '../validators/type-validators' ;
66import { validator } from '../validators/validator' ;
77import { type ConfigExtensionStore , type ConfigInternalStore , type ConfigStore } from './state' ;
88import {
9- configInternalStore ,
109 configExtensionStore ,
10+ configInternalStore ,
1111 getConfigStore ,
1212 getExtensionConfig ,
13+ getExtensionSlotsConfigStore ,
1314 getExtensionsConfigStore ,
1415 implementerToolsConfigStore ,
1516 temporaryConfigStore ,
16- getExtensionSlotsConfigStore ,
1717} from './state' ;
1818import { type TemporaryConfigStore } from '..' ;
1919
@@ -37,42 +37,37 @@ import { type TemporaryConfigStore } from '..';
3737 * store values at the end. `computeExtensionConfigs` calls `getGlobalStore`,
3838 * which creates stores.
3939 */
40- computeModuleConfig ( configInternalStore . getState ( ) , temporaryConfigStore . getState ( ) ) ;
41- configInternalStore . subscribe ( ( configState ) => computeModuleConfig ( configState , temporaryConfigStore . getState ( ) ) ) ;
42- temporaryConfigStore . subscribe ( ( tempConfigState ) =>
43- computeModuleConfig ( configInternalStore . getState ( ) , tempConfigState ) ,
44- ) ;
45-
46- computeImplementerToolsConfig ( configInternalStore . getState ( ) , temporaryConfigStore . getState ( ) ) ;
47- configInternalStore . subscribe ( ( configState ) =>
48- computeImplementerToolsConfig ( configState , temporaryConfigStore . getState ( ) ) ,
49- ) ;
50- temporaryConfigStore . subscribe ( ( tempConfigState ) =>
51- computeImplementerToolsConfig ( configInternalStore . getState ( ) , tempConfigState ) ,
52- ) ;
53-
54- computeExtensionSlotConfigs ( configInternalStore . getState ( ) , temporaryConfigStore . getState ( ) ) ;
55- configInternalStore . subscribe ( ( configState ) =>
56- computeExtensionSlotConfigs ( configState , temporaryConfigStore . getState ( ) ) ,
57- ) ;
58- temporaryConfigStore . subscribe ( ( tempConfigState ) =>
59- computeExtensionSlotConfigs ( configInternalStore . getState ( ) , tempConfigState ) ,
60- ) ;
61-
62- computeExtensionConfigs (
63- configInternalStore . getState ( ) ,
64- configExtensionStore . getState ( ) ,
65- temporaryConfigStore . getState ( ) ,
66- ) ;
67- configInternalStore . subscribe ( ( configState ) => {
68- computeExtensionConfigs ( configState , configExtensionStore . getState ( ) , temporaryConfigStore . getState ( ) ) ;
69- } ) ;
70- configExtensionStore . subscribe ( ( extensionState ) => {
71- computeExtensionConfigs ( configInternalStore . getState ( ) , extensionState , temporaryConfigStore . getState ( ) ) ;
72- } ) ;
73- temporaryConfigStore . subscribe ( ( tempConfigState ) => {
74- computeExtensionConfigs ( configInternalStore . getState ( ) , configExtensionStore . getState ( ) , tempConfigState ) ;
75- } ) ;
40+ // Store unsubscribe functions to allow cleanup (e.g., in tests or hot module reloading)
41+ const configSubscriptions : Array < ( ) => void > = [ ] ;
42+
43+ /**
44+ * Recomputes all configuration derived stores based on current state of input stores.
45+ * Called whenever any input store (configInternalStore, temporaryConfigStore, configExtensionStore) changes.
46+ */
47+ function recomputeAllConfigs ( ) {
48+ const configState = configInternalStore . getState ( ) ;
49+ const tempConfigState = temporaryConfigStore . getState ( ) ;
50+ const extensionState = configExtensionStore . getState ( ) ;
51+
52+ computeModuleConfig ( configState , tempConfigState ) ;
53+ computeImplementerToolsConfig ( configState , tempConfigState ) ;
54+ computeExtensionSlotConfigs ( configState , tempConfigState ) ;
55+ computeExtensionConfigs ( configState , extensionState , tempConfigState ) ;
56+ }
57+
58+ function setupConfigSubscriptions ( ) {
59+ // Initial computation
60+ recomputeAllConfigs ( ) ;
61+
62+ // Subscribe to all input stores with a single handler
63+ // This ensures we only recompute once even if multiple stores change simultaneously
64+ configSubscriptions . push ( configInternalStore . subscribe ( recomputeAllConfigs ) ) ;
65+ configSubscriptions . push ( temporaryConfigStore . subscribe ( recomputeAllConfigs ) ) ;
66+ configSubscriptions . push ( configExtensionStore . subscribe ( recomputeAllConfigs ) ) ;
67+ }
68+
69+ // Set up subscriptions at module load time
70+ setupConfigSubscriptions ( ) ;
7671
7772function computeModuleConfig ( state : ConfigInternalStore , tempState : TemporaryConfigStore ) {
7873 for ( let moduleName of Object . keys ( state . schemas ) ) {
@@ -83,21 +78,24 @@ function computeModuleConfig(state: ConfigInternalStore, tempState: TemporaryCon
8378 // available, which as of this writing blocks the schema definition from occurring
8479 // for modules loaded based on their extensions.
8580 const moduleStore = getConfigStore ( moduleName ) ;
81+ let newState ;
8682 if ( state . moduleLoaded [ moduleName ] ) {
8783 const config = getConfigForModule ( moduleName , state , tempState ) ;
88- moduleStore . setState ( {
84+ newState = {
8985 translationOverridesLoaded : true ,
9086 loaded : true ,
9187 config,
92- } ) ;
88+ } ;
9389 } else {
9490 const config = getConfigForModuleImplicitSchema ( moduleName , state , tempState ) ;
95- moduleStore . setState ( {
91+ newState = {
9692 translationOverridesLoaded : true ,
9793 loaded : false ,
9894 config,
99- } ) ;
95+ } ;
10096 }
97+
98+ moduleStore . setState ( newState ) ;
10199 }
102100}
103101
@@ -109,14 +107,21 @@ function computeExtensionSlotConfigs(state: ConfigInternalStore, tempState: Temp
109107 const slotStore = getExtensionSlotsConfigStore ( ) ;
110108 const oldState = slotStore . getState ( ) ;
111109 const newState = { slots : { ...oldState . slots , ...newSlotStoreEntries } } ;
112- if ( ! equals ( oldState , newState ) ) {
110+
111+ if ( ! equals ( oldState . slots , newState . slots ) ) {
113112 slotStore . setState ( newState ) ;
114113 }
115114}
116115
117116function computeImplementerToolsConfig ( state : ConfigInternalStore , tempConfigState : TemporaryConfigStore ) {
117+ const oldState = implementerToolsConfigStore . getState ( ) ;
118118 const config = getImplementerToolsConfig ( state , tempConfigState ) ;
119- implementerToolsConfigStore . setState ( { config } ) ;
119+ const newState = { config } ;
120+
121+ // Use deep equality on the actual config content, not the wrapper object
122+ if ( ! equals ( oldState . config , newState . config ) ) {
123+ implementerToolsConfigStore . setState ( newState ) ;
124+ }
120125}
121126
122127function computeExtensionConfigs (
@@ -137,12 +142,19 @@ function computeExtensionConfigs(
137142 tempConfigState ,
138143 ) ;
139144
140- configs [ extension . slotName ] = {
141- ...configs [ extension . slotName ] ,
142- [ extension . extensionId ] : { config, loaded : true } ,
143- } ;
145+ if ( ! configs [ extension . slotName ] ) {
146+ configs [ extension . slotName ] = { } ;
147+ }
148+ configs [ extension . slotName ] [ extension . extensionId ] = { config, loaded : true } ;
149+ }
150+ const extensionsConfigStore = getExtensionsConfigStore ( ) ;
151+ const oldState = extensionsConfigStore . getState ( ) ;
152+ const newState = { configs } ;
153+
154+ // Use deep equality to only update if configs actually changed
155+ if ( ! equals ( oldState . configs , newState . configs ) ) {
156+ extensionsConfigStore . setState ( newState ) ;
144157 }
145- getExtensionsConfigStore ( ) . setState ( { configs } ) ;
146158}
147159
148160/*
@@ -862,6 +874,20 @@ export function clearConfigErrors(keyPath?: string) {
862874 }
863875}
864876
877+ /**
878+ * Cleans up all config store subscriptions and re-establishes them. This is primarily
879+ * useful for testing, where subscriptions set up at module load time need to be cleared
880+ * between tests to prevent infinite update loops. After clearing, subscriptions are
881+ * re-established so the config system continues to work normally.
882+ *
883+ * @internal
884+ */
885+ export function resetConfigSystem ( ) {
886+ configSubscriptions . forEach ( ( unsubscribe ) => unsubscribe ( ) ) ;
887+ configSubscriptions . length = 0 ;
888+ setupConfigSubscriptions ( ) ;
889+ }
890+
865891/**
866892 * Copied over from esm-extensions. It rightly belongs to that module, but esm-config
867893 * cannot depend on esm-extensions.
@@ -905,6 +931,11 @@ const implicitConfigSchema: ConfigSchema = {
905931 _type : Type . Array ,
906932 _default : [ ] ,
907933 } ,
934+ expression : {
935+ _description : 'The expression that determines whether the extension is displayed' ,
936+ _type : Type . String ,
937+ _default : undefined ,
938+ } ,
908939 } ,
909940 ...translationOverridesSchema ,
910941} ;
0 commit comments