@openmrs/esm-react-utils 10.0.1-pre.5723 → 10.0.1-pre.5741
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/.turbo/turbo-build.log +1 -1
- package/dist/useConfig.d.ts.map +1 -1
- package/dist/useConfig.js +32 -24
- package/package.json +23 -23
- package/src/useConfig.suspense.test.tsx +226 -0
- package/src/useConfig.ts +43 -27
- package/src/useLayoutType.test.tsx +1 -3
package/.turbo/turbo-build.log
CHANGED
package/dist/useConfig.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useConfig.d.ts","sourceRoot":"","sources":["../src/useConfig.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useConfig.d.ts","sourceRoot":"","sources":["../src/useConfig.ts"],"names":[],"mappings":"AAqJA,MAAM,WAAW,gBAAgB;IAC/B;gGAC4F;IAC5F,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAqB1D,CAAC,CACnB"}
|
package/dist/useConfig.js
CHANGED
|
@@ -26,26 +26,39 @@ function readInitialExtensionConfig(store, extension) {
|
|
|
26
26
|
}
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The promise a suspended `useConfig()` throws, so every component waiting on the same config waits on
|
|
31
|
+
* one promise and a re-render throws the same one rather than starting again.
|
|
32
|
+
*
|
|
33
|
+
* The entry is dropped once it settles: an extension's config exists only while that extension is
|
|
34
|
+
* mounted, so the same id can have to wait a second time and a settled promise cannot describe that.
|
|
35
|
+
*/ function whenConfigReady(cacheId, store, read) {
|
|
36
|
+
const cached = promises[cacheId];
|
|
37
|
+
if (cached) {
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
const promise = new Promise((resolve)=>{
|
|
41
|
+
const current = read(store.getState());
|
|
42
|
+
if (current) {
|
|
43
|
+
resolve(current);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
31
46
|
const unsubscribe = store.subscribe((state)=>{
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
const config = read(state);
|
|
48
|
+
if (config) {
|
|
34
49
|
unsubscribe();
|
|
50
|
+
resolve(config);
|
|
35
51
|
}
|
|
36
52
|
});
|
|
37
53
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
resolve(extConfig.config);
|
|
45
|
-
unsubscribe();
|
|
46
|
-
}
|
|
47
|
-
});
|
|
54
|
+
promises[cacheId] = promise;
|
|
55
|
+
void promise.then(()=>{
|
|
56
|
+
// Guarded so that a later wait under the same id, started after this one settled, is left alone.
|
|
57
|
+
if (promises[cacheId] === promise) {
|
|
58
|
+
delete promises[cacheId];
|
|
59
|
+
}
|
|
48
60
|
});
|
|
61
|
+
return promise;
|
|
49
62
|
}
|
|
50
63
|
function useConfigStore(store) {
|
|
51
64
|
const [state, setState] = useState(readInitialConfig(store));
|
|
@@ -82,12 +95,11 @@ function useExtensionConfig(extension) {
|
|
|
82
95
|
const store = useMemo(getExtensionsConfigStore, []);
|
|
83
96
|
const state = useExtensionConfigStore(store, extension);
|
|
84
97
|
if (!state && extension) {
|
|
85
|
-
const cacheId = `${extension.extensionSlotName}-${extension.extensionId}`;
|
|
86
|
-
if (!promises[cacheId] && store) {
|
|
87
|
-
promises[cacheId] = createExtensionConfigPromise(store, extension);
|
|
88
|
-
}
|
|
89
98
|
// React will prevent the client component from rendering until the promise resolves
|
|
90
|
-
throw
|
|
99
|
+
throw whenConfigReady(`${extension.extensionSlotName}-${extension.extensionId}`, store, (storeState)=>{
|
|
100
|
+
const extConfig = getExtensionConfigFromStore(storeState, extension.extensionSlotName, extension.extensionId);
|
|
101
|
+
return extConfig.loaded ? extConfig.config : null;
|
|
102
|
+
});
|
|
91
103
|
}
|
|
92
104
|
return state || {};
|
|
93
105
|
}
|
|
@@ -95,14 +107,10 @@ function useNormalConfig(moduleName) {
|
|
|
95
107
|
const store = useMemo(()=>getConfigStore(moduleName), [
|
|
96
108
|
moduleName
|
|
97
109
|
]);
|
|
98
|
-
const cacheId = moduleName;
|
|
99
110
|
const state = useConfigStore(store);
|
|
100
111
|
if (!state) {
|
|
101
|
-
if (!promises[cacheId]) {
|
|
102
|
-
promises[cacheId] = createConfigPromise(store);
|
|
103
|
-
}
|
|
104
112
|
// React will prevent the client component from rendering until the promise resolves
|
|
105
|
-
throw
|
|
113
|
+
throw whenConfigReady(moduleName, store, (storeState)=>storeState.loaded ? storeState.config : null);
|
|
106
114
|
}
|
|
107
115
|
return state;
|
|
108
116
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5741",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"type": "module",
|
|
@@ -60,17 +60,17 @@
|
|
|
60
60
|
"single-spa-react": "^6.0.2"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@openmrs/esm-api": "^10.0.1-pre.
|
|
64
|
-
"@openmrs/esm-config": "^10.0.1-pre.
|
|
65
|
-
"@openmrs/esm-context": "^10.0.1-pre.
|
|
66
|
-
"@openmrs/esm-emr-api": "^10.0.1-pre.
|
|
67
|
-
"@openmrs/esm-error-handling": "^10.0.1-pre.
|
|
68
|
-
"@openmrs/esm-extensions": "^10.0.1-pre.
|
|
69
|
-
"@openmrs/esm-feature-flags": "^10.0.1-pre.
|
|
70
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
71
|
-
"@openmrs/esm-navigation": "^10.0.1-pre.
|
|
72
|
-
"@openmrs/esm-state": "^10.0.1-pre.
|
|
73
|
-
"@openmrs/esm-utils": "^10.0.1-pre.
|
|
63
|
+
"@openmrs/esm-api": "^10.0.1-pre.5741",
|
|
64
|
+
"@openmrs/esm-config": "^10.0.1-pre.5741",
|
|
65
|
+
"@openmrs/esm-context": "^10.0.1-pre.5741",
|
|
66
|
+
"@openmrs/esm-emr-api": "^10.0.1-pre.5741",
|
|
67
|
+
"@openmrs/esm-error-handling": "^10.0.1-pre.5741",
|
|
68
|
+
"@openmrs/esm-extensions": "^10.0.1-pre.5741",
|
|
69
|
+
"@openmrs/esm-feature-flags": "^10.0.1-pre.5741",
|
|
70
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5741",
|
|
71
|
+
"@openmrs/esm-navigation": "^10.0.1-pre.5741",
|
|
72
|
+
"@openmrs/esm-state": "^10.0.1-pre.5741",
|
|
73
|
+
"@openmrs/esm-utils": "^10.0.1-pre.5741",
|
|
74
74
|
"dayjs": "1.x",
|
|
75
75
|
"i18next": "25.x",
|
|
76
76
|
"react": "18.x",
|
|
@@ -79,17 +79,17 @@
|
|
|
79
79
|
"swr": "2.x"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@openmrs/esm-api": "10.0.1-pre.
|
|
83
|
-
"@openmrs/esm-config": "10.0.1-pre.
|
|
84
|
-
"@openmrs/esm-context": "10.0.1-pre.
|
|
85
|
-
"@openmrs/esm-emr-api": "10.0.1-pre.
|
|
86
|
-
"@openmrs/esm-error-handling": "10.0.1-pre.
|
|
87
|
-
"@openmrs/esm-extensions": "10.0.1-pre.
|
|
88
|
-
"@openmrs/esm-feature-flags": "10.0.1-pre.
|
|
89
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
90
|
-
"@openmrs/esm-navigation": "10.0.1-pre.
|
|
91
|
-
"@openmrs/esm-state": "10.0.1-pre.
|
|
92
|
-
"@openmrs/esm-utils": "10.0.1-pre.
|
|
82
|
+
"@openmrs/esm-api": "10.0.1-pre.5741",
|
|
83
|
+
"@openmrs/esm-config": "10.0.1-pre.5741",
|
|
84
|
+
"@openmrs/esm-context": "10.0.1-pre.5741",
|
|
85
|
+
"@openmrs/esm-emr-api": "10.0.1-pre.5741",
|
|
86
|
+
"@openmrs/esm-error-handling": "10.0.1-pre.5741",
|
|
87
|
+
"@openmrs/esm-extensions": "10.0.1-pre.5741",
|
|
88
|
+
"@openmrs/esm-feature-flags": "10.0.1-pre.5741",
|
|
89
|
+
"@openmrs/esm-globals": "10.0.1-pre.5741",
|
|
90
|
+
"@openmrs/esm-navigation": "10.0.1-pre.5741",
|
|
91
|
+
"@openmrs/esm-state": "10.0.1-pre.5741",
|
|
92
|
+
"@openmrs/esm-utils": "10.0.1-pre.5741",
|
|
93
93
|
"@swc/cli": "0.8.1",
|
|
94
94
|
"@swc/core": "1.15.21",
|
|
95
95
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import React, { Suspense } from 'react';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { act, render, screen } from '@testing-library/react';
|
|
4
|
+
import {
|
|
5
|
+
configExtensionStore,
|
|
6
|
+
configInternalStore,
|
|
7
|
+
defineConfigSchema,
|
|
8
|
+
getConfigStore,
|
|
9
|
+
getExtensionsConfigStore,
|
|
10
|
+
} from '@openmrs/esm-config';
|
|
11
|
+
import { ComponentContext } from './ComponentContext';
|
|
12
|
+
import { useConfig } from './useConfig';
|
|
13
|
+
|
|
14
|
+
const slotName = 'test-slot';
|
|
15
|
+
const slotModuleName = 'slot-module';
|
|
16
|
+
const extensionId = 'thing#instance';
|
|
17
|
+
const extensionModuleName = 'ext-module';
|
|
18
|
+
|
|
19
|
+
/** How many times `ShowConfig` has started rendering, including attempts that suspended. */
|
|
20
|
+
let renders = 0;
|
|
21
|
+
|
|
22
|
+
function ShowConfig() {
|
|
23
|
+
renders++;
|
|
24
|
+
const config = useConfig<{ thing: string }>();
|
|
25
|
+
return <div>{config.thing}</div>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderExtension() {
|
|
29
|
+
return render(
|
|
30
|
+
<Suspense fallback={<div>suspended</div>}>
|
|
31
|
+
<ComponentContext.Provider
|
|
32
|
+
value={{
|
|
33
|
+
moduleName: extensionModuleName,
|
|
34
|
+
featureName: 'ext-feature',
|
|
35
|
+
extension: { extensionSlotName: slotName, extensionSlotModuleName: slotModuleName, extensionId },
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<ShowConfig />
|
|
39
|
+
</ComponentContext.Provider>
|
|
40
|
+
</Suspense>,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lets a suspended component sit for a while and returns how many times it tried to render meanwhile.
|
|
46
|
+
* Re-throwing a promise that has already settled makes React retry at once, over and over, rather
|
|
47
|
+
* than wait, which is how a stale cached promise shows up.
|
|
48
|
+
*/
|
|
49
|
+
async function rendersWhileWaiting() {
|
|
50
|
+
renders = 0;
|
|
51
|
+
await act(() => new Promise((resolve) => setTimeout(resolve, 50)));
|
|
52
|
+
return renders;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderModule(moduleName: string) {
|
|
56
|
+
return render(
|
|
57
|
+
<Suspense fallback={<div>suspended</div>}>
|
|
58
|
+
<ComponentContext.Provider value={{ moduleName, featureName: 'f' }}>
|
|
59
|
+
<ShowConfig />
|
|
60
|
+
</ComponentContext.Provider>
|
|
61
|
+
</Suspense>,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Counts the subscriptions on `store` made from now on that are still live, so a leak shows up as a
|
|
67
|
+
* non-zero count. The config system's own subscriptions, made before this is called, are not counted.
|
|
68
|
+
*/
|
|
69
|
+
function countSubscriptions(store: { subscribe: (listener: any) => () => void }) {
|
|
70
|
+
const realSubscribe = store.subscribe.bind(store);
|
|
71
|
+
const counter = { live: 0 };
|
|
72
|
+
|
|
73
|
+
store.subscribe = (listener) => {
|
|
74
|
+
counter.live++;
|
|
75
|
+
const unsubscribe = realSubscribe(listener);
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
counter.live--;
|
|
79
|
+
unsubscribe();
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return counter;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Registers the extension as mounted, which is what drives the config system to derive its config. */
|
|
87
|
+
function mountExtension() {
|
|
88
|
+
act(() => {
|
|
89
|
+
configExtensionStore.setState({
|
|
90
|
+
mountedExtensions: [{ slotName, slotModuleName, extensionId, extensionModuleName }],
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Unregisters it, which drops its entry from the extensions config store. */
|
|
96
|
+
function unmountExtension() {
|
|
97
|
+
act(() => {
|
|
98
|
+
configExtensionStore.setState({ mountedExtensions: [] });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
configExtensionStore.setState({ mountedExtensions: [] });
|
|
104
|
+
configInternalStore.setState({ ...configInternalStore.getState(), providedConfigs: [] });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('useConfig suspense', () => {
|
|
108
|
+
it('renders an extension whose config arrives after the first render', async () => {
|
|
109
|
+
defineConfigSchema(extensionModuleName, { thing: { _default: 'the thing' } });
|
|
110
|
+
|
|
111
|
+
renderExtension();
|
|
112
|
+
expect(screen.getByText('suspended')).toBeInTheDocument();
|
|
113
|
+
|
|
114
|
+
mountExtension();
|
|
115
|
+
|
|
116
|
+
expect(await screen.findByText('the thing')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders an extension that mounts, unmounts and mounts again', async () => {
|
|
120
|
+
defineConfigSchema(extensionModuleName, { thing: { _default: 'the thing' } });
|
|
121
|
+
|
|
122
|
+
// First cycle: suspends, config arrives, renders.
|
|
123
|
+
const view = renderExtension();
|
|
124
|
+
mountExtension();
|
|
125
|
+
expect(await screen.findByText('the thing')).toBeInTheDocument();
|
|
126
|
+
|
|
127
|
+
// The extension goes away, taking its config store entry with it.
|
|
128
|
+
view.unmount();
|
|
129
|
+
unmountExtension();
|
|
130
|
+
|
|
131
|
+
// Second cycle: the same extension in the same slot waits under the same cache id as the first.
|
|
132
|
+
renderExtension();
|
|
133
|
+
expect(screen.getByText('suspended')).toBeInTheDocument();
|
|
134
|
+
expect(await rendersWhileWaiting()).toBeLessThanOrEqual(2);
|
|
135
|
+
|
|
136
|
+
mountExtension();
|
|
137
|
+
|
|
138
|
+
expect(await screen.findByText('the thing')).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('renders a module whose config store loads while it is suspended', async () => {
|
|
142
|
+
const moduleName = 'preloaded-module';
|
|
143
|
+
defineConfigSchema(moduleName, { thing: { _default: 'preloaded thing' } });
|
|
144
|
+
|
|
145
|
+
const store = getConfigStore(moduleName);
|
|
146
|
+
const loaded = store.getState();
|
|
147
|
+
|
|
148
|
+
act(() => store.setState({ ...loaded, loaded: false, config: null }));
|
|
149
|
+
|
|
150
|
+
renderModule(moduleName);
|
|
151
|
+
expect(screen.getByText('suspended')).toBeInTheDocument();
|
|
152
|
+
|
|
153
|
+
act(() => store.setState(loaded));
|
|
154
|
+
|
|
155
|
+
expect(await screen.findByText('preloaded thing')).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders a module whose config has already loaded without suspending', () => {
|
|
159
|
+
defineConfigSchema('loaded-module', { thing: { _default: 'already there' } });
|
|
160
|
+
|
|
161
|
+
renderModule('loaded-module');
|
|
162
|
+
|
|
163
|
+
expect(screen.getByText('already there')).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('renders an extension whose config has already loaded without suspending', () => {
|
|
167
|
+
defineConfigSchema(extensionModuleName, { thing: { _default: 'the thing' } });
|
|
168
|
+
mountExtension();
|
|
169
|
+
|
|
170
|
+
renderExtension();
|
|
171
|
+
|
|
172
|
+
expect(screen.getByText('the thing')).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('renders a module that suspends again after an earlier wait for it settled', async () => {
|
|
176
|
+
const moduleName = 'reloaded-module';
|
|
177
|
+
defineConfigSchema(moduleName, { thing: { _default: 'first' } });
|
|
178
|
+
|
|
179
|
+
const store = getConfigStore(moduleName);
|
|
180
|
+
const first = store.getState();
|
|
181
|
+
|
|
182
|
+
act(() => store.setState({ ...first, loaded: false, config: null }));
|
|
183
|
+
const view = renderModule(moduleName);
|
|
184
|
+
act(() => store.setState(first));
|
|
185
|
+
expect(await screen.findByText('first')).toBeInTheDocument();
|
|
186
|
+
view.unmount();
|
|
187
|
+
|
|
188
|
+
act(() => store.setState({ ...first, loaded: false, config: null }));
|
|
189
|
+
renderModule(moduleName);
|
|
190
|
+
expect(screen.getByText('suspended')).toBeInTheDocument();
|
|
191
|
+
expect(await rendersWhileWaiting()).toBeLessThanOrEqual(2);
|
|
192
|
+
|
|
193
|
+
act(() => store.setState({ ...first, config: { ...first.config, thing: 'second' } }));
|
|
194
|
+
|
|
195
|
+
expect(await screen.findByText('second')).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('leaves no subscription on a module store once its wait settles and the component unmounts', async () => {
|
|
199
|
+
const moduleName = 'counted-module';
|
|
200
|
+
defineConfigSchema(moduleName, { thing: { _default: 'counted' } });
|
|
201
|
+
|
|
202
|
+
const store = getConfigStore(moduleName);
|
|
203
|
+
const loaded = store.getState();
|
|
204
|
+
act(() => store.setState({ ...loaded, loaded: false, config: null }));
|
|
205
|
+
const subscriptions = countSubscriptions(store);
|
|
206
|
+
|
|
207
|
+
const view = renderModule(moduleName);
|
|
208
|
+
act(() => store.setState(loaded));
|
|
209
|
+
expect(await screen.findByText('counted')).toBeInTheDocument();
|
|
210
|
+
view.unmount();
|
|
211
|
+
|
|
212
|
+
expect(subscriptions.live).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('leaves no subscription on the extensions store once its wait settles and the component unmounts', async () => {
|
|
216
|
+
defineConfigSchema(extensionModuleName, { thing: { _default: 'the thing' } });
|
|
217
|
+
const subscriptions = countSubscriptions(getExtensionsConfigStore());
|
|
218
|
+
|
|
219
|
+
const view = renderExtension();
|
|
220
|
+
mountExtension();
|
|
221
|
+
expect(await screen.findByText('the thing')).toBeInTheDocument();
|
|
222
|
+
view.unmount();
|
|
223
|
+
|
|
224
|
+
expect(subscriptions.live).toBe(0);
|
|
225
|
+
});
|
|
226
|
+
});
|
package/src/useConfig.ts
CHANGED
|
@@ -42,27 +42,51 @@ function readInitialExtensionConfig(store: StoreApi<ExtensionsConfigStore>, exte
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The promise a suspended `useConfig()` throws, so every component waiting on the same config waits on
|
|
47
|
+
* one promise and a re-render throws the same one rather than starting again.
|
|
48
|
+
*
|
|
49
|
+
* The entry is dropped once it settles: an extension's config exists only while that extension is
|
|
50
|
+
* mounted, so the same id can have to wait a second time and a settled promise cannot describe that.
|
|
51
|
+
*/
|
|
52
|
+
function whenConfigReady<S>(
|
|
53
|
+
cacheId: string,
|
|
54
|
+
store: StoreApi<S>,
|
|
55
|
+
read: (state: S) => ConfigObject | null | undefined,
|
|
56
|
+
): Promise<ConfigObject> {
|
|
57
|
+
const cached = promises[cacheId];
|
|
58
|
+
|
|
59
|
+
if (cached) {
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const promise = new Promise<ConfigObject>((resolve) => {
|
|
64
|
+
const current = read(store.getState());
|
|
65
|
+
|
|
66
|
+
if (current) {
|
|
67
|
+
resolve(current);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
const unsubscribe = store.subscribe((state) => {
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
const config = read(state);
|
|
73
|
+
|
|
74
|
+
if (config) {
|
|
50
75
|
unsubscribe();
|
|
76
|
+
resolve(config);
|
|
51
77
|
}
|
|
52
78
|
});
|
|
53
79
|
});
|
|
54
|
-
}
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
unsubscribe();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
81
|
+
promises[cacheId] = promise;
|
|
82
|
+
void promise.then(() => {
|
|
83
|
+
// Guarded so that a later wait under the same id, started after this one settled, is left alone.
|
|
84
|
+
if (promises[cacheId] === promise) {
|
|
85
|
+
delete promises[cacheId];
|
|
86
|
+
}
|
|
65
87
|
});
|
|
88
|
+
|
|
89
|
+
return promise;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
function useConfigStore(store: StoreApi<ConfigStore>) {
|
|
@@ -101,14 +125,11 @@ function useExtensionConfig(extension: ExtensionData | undefined) {
|
|
|
101
125
|
const state = useExtensionConfigStore(store, extension);
|
|
102
126
|
|
|
103
127
|
if (!state && extension) {
|
|
104
|
-
const cacheId = `${extension.extensionSlotName}-${extension.extensionId}`;
|
|
105
|
-
|
|
106
|
-
if (!promises[cacheId] && store) {
|
|
107
|
-
promises[cacheId] = createExtensionConfigPromise(store, extension);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
128
|
// React will prevent the client component from rendering until the promise resolves
|
|
111
|
-
throw
|
|
129
|
+
throw whenConfigReady(`${extension.extensionSlotName}-${extension.extensionId}`, store, (storeState) => {
|
|
130
|
+
const extConfig = getExtensionConfigFromStore(storeState, extension.extensionSlotName, extension.extensionId);
|
|
131
|
+
return extConfig.loaded ? extConfig.config : null;
|
|
132
|
+
});
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
return state || {};
|
|
@@ -116,16 +137,11 @@ function useExtensionConfig(extension: ExtensionData | undefined) {
|
|
|
116
137
|
|
|
117
138
|
function useNormalConfig(moduleName: string) {
|
|
118
139
|
const store = useMemo(() => getConfigStore(moduleName), [moduleName]);
|
|
119
|
-
const cacheId = moduleName;
|
|
120
140
|
const state = useConfigStore(store);
|
|
121
141
|
|
|
122
142
|
if (!state) {
|
|
123
|
-
if (!promises[cacheId]) {
|
|
124
|
-
promises[cacheId] = createConfigPromise(store);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
143
|
// React will prevent the client component from rendering until the promise resolves
|
|
128
|
-
throw
|
|
144
|
+
throw whenConfigReady(moduleName, store, (storeState) => (storeState.loaded ? storeState.config : null));
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
return state;
|
|
@@ -99,9 +99,7 @@ describe('isDesktop', () => {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it('identifies non-desktop layouts correctly', () => {
|
|
102
|
-
const nonDesktopLayouts = LAYOUT_CASES.map(([, layout]) => layout).filter(
|
|
103
|
-
(layout) => !isDesktop(layout),
|
|
104
|
-
);
|
|
102
|
+
const nonDesktopLayouts = LAYOUT_CASES.map(([, layout]) => layout).filter((layout) => !isDesktop(layout));
|
|
105
103
|
expect(nonDesktopLayouts).toEqual(['phone']);
|
|
106
104
|
});
|
|
107
105
|
});
|