@openmrs/esm-react-utils 8.0.1-pre.3935 → 8.0.1-pre.3942
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
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDefineAppContext.d.ts","sourceRoot":"","sources":["../src/useDefineAppContext.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useDefineAppContext.d.ts","sourceRoot":"","sources":["../src/useDefineAppContext.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,YAE5E,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,UA2CvD"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** @module @category Context */ import { useEffect, useRef } from "react";
|
|
2
|
-
import { registerContext, unregisterContext, updateContext } from "@openmrs/esm-context";
|
|
2
|
+
import { getContext, registerContext, unregisterContext, updateContext } from "@openmrs/esm-context";
|
|
3
3
|
import { shallowEqual } from "@openmrs/esm-utils";
|
|
4
|
+
/**
|
|
5
|
+
* Tracks the current owner of each namespace so that a stale cleanup from a
|
|
6
|
+
* previous instance does not unregister a namespace that a newer instance now owns.
|
|
7
|
+
*/ const namespaceOwners = new Map();
|
|
4
8
|
/**
|
|
5
9
|
* This hook is used to register a namespace in the AppContext. The component that registers the
|
|
6
10
|
* namespace is responsible for updating the value associated with the namespace. The namespace
|
|
@@ -34,11 +38,28 @@ import { shallowEqual } from "@openmrs/esm-utils";
|
|
|
34
38
|
*/ export function useDefineAppContext(namespace, value) {
|
|
35
39
|
const previousValue = useRef(value ?? {});
|
|
36
40
|
const updateFunction = useRef((update)=>updateContext(namespace, update));
|
|
41
|
+
const ownerToken = useRef(Symbol());
|
|
37
42
|
// effect hook for registration and unregistration
|
|
38
43
|
useEffect(()=>{
|
|
39
|
-
|
|
44
|
+
const initialValue = value ?? {};
|
|
45
|
+
const token = ownerToken.current;
|
|
46
|
+
if (getContext(namespace) === null) {
|
|
47
|
+
registerContext(namespace, initialValue);
|
|
48
|
+
} else {
|
|
49
|
+
// The previous instance's cleanup effect hasn't run before this new instance
|
|
50
|
+
// mounts (e.g., during navigation when extensions unmount/remount across
|
|
51
|
+
// separate single-spa lifecycles). Update the existing context instead.
|
|
52
|
+
console.warn(`Namespace ${namespace} is already registered in the app context. ` + `This is likely a race condition during navigation, but may indicate two components ` + `are trying to own the same namespace. Updating the existing context.`);
|
|
53
|
+
updateContext(namespace, ()=>initialValue);
|
|
54
|
+
}
|
|
55
|
+
namespaceOwners.set(namespace, token);
|
|
40
56
|
return ()=>{
|
|
41
|
-
|
|
57
|
+
// Only unregister if this instance is still the current owner.
|
|
58
|
+
// A newer instance may have taken ownership during a lifecycle race.
|
|
59
|
+
if (namespaceOwners.get(namespace) === token) {
|
|
60
|
+
unregisterContext(namespace);
|
|
61
|
+
namespaceOwners.delete(namespace);
|
|
62
|
+
}
|
|
42
63
|
};
|
|
43
64
|
}, [
|
|
44
65
|
namespace
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "8.0.1-pre.
|
|
3
|
+
"version": "8.0.1-pre.3942",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"type": "module",
|
|
@@ -79,17 +79,17 @@
|
|
|
79
79
|
"swr": "2.x"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@openmrs/esm-api": "8.0.1-pre.
|
|
83
|
-
"@openmrs/esm-config": "8.0.1-pre.
|
|
84
|
-
"@openmrs/esm-context": "8.0.1-pre.
|
|
85
|
-
"@openmrs/esm-emr-api": "8.0.1-pre.
|
|
86
|
-
"@openmrs/esm-error-handling": "8.0.1-pre.
|
|
87
|
-
"@openmrs/esm-extensions": "8.0.1-pre.
|
|
88
|
-
"@openmrs/esm-feature-flags": "8.0.1-pre.
|
|
89
|
-
"@openmrs/esm-globals": "8.0.1-pre.
|
|
90
|
-
"@openmrs/esm-navigation": "8.0.1-pre.
|
|
91
|
-
"@openmrs/esm-state": "8.0.1-pre.
|
|
92
|
-
"@openmrs/esm-utils": "8.0.1-pre.
|
|
82
|
+
"@openmrs/esm-api": "8.0.1-pre.3942",
|
|
83
|
+
"@openmrs/esm-config": "8.0.1-pre.3942",
|
|
84
|
+
"@openmrs/esm-context": "8.0.1-pre.3942",
|
|
85
|
+
"@openmrs/esm-emr-api": "8.0.1-pre.3942",
|
|
86
|
+
"@openmrs/esm-error-handling": "8.0.1-pre.3942",
|
|
87
|
+
"@openmrs/esm-extensions": "8.0.1-pre.3942",
|
|
88
|
+
"@openmrs/esm-feature-flags": "8.0.1-pre.3942",
|
|
89
|
+
"@openmrs/esm-globals": "8.0.1-pre.3942",
|
|
90
|
+
"@openmrs/esm-navigation": "8.0.1-pre.3942",
|
|
91
|
+
"@openmrs/esm-state": "8.0.1-pre.3942",
|
|
92
|
+
"@openmrs/esm-utils": "8.0.1-pre.3942",
|
|
93
93
|
"@swc/cli": "^0.7.7",
|
|
94
94
|
"@swc/core": "^1.11.29",
|
|
95
95
|
"@vitest/coverage-v8": "^4.0.18",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { getContext, unregisterContext } from '@openmrs/esm-context';
|
|
4
|
+
import { useDefineAppContext } from './useDefineAppContext';
|
|
5
|
+
|
|
6
|
+
const namespace = 'test-context';
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
// Clean up any registered context between tests
|
|
10
|
+
unregisterContext(namespace);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('useDefineAppContext', () => {
|
|
14
|
+
it('registers the namespace on mount and unregisters on unmount', () => {
|
|
15
|
+
const { unmount } = renderHook(() => useDefineAppContext(namespace, { value: 1 }));
|
|
16
|
+
|
|
17
|
+
expect(getContext(namespace)).toEqual({ value: 1 });
|
|
18
|
+
|
|
19
|
+
unmount();
|
|
20
|
+
|
|
21
|
+
expect(getContext(namespace)).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('updates the context when the value changes', () => {
|
|
25
|
+
const { rerender } = renderHook(({ value }) => useDefineAppContext(namespace, value), {
|
|
26
|
+
initialProps: { value: { count: 1 } },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(getContext(namespace)).toEqual({ count: 1 });
|
|
30
|
+
|
|
31
|
+
rerender({ value: { count: 2 } });
|
|
32
|
+
|
|
33
|
+
expect(getContext(namespace)).toEqual({ count: 2 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('recovers when a new instance mounts before the previous cleanup runs', () => {
|
|
37
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
38
|
+
|
|
39
|
+
// First instance registers the context
|
|
40
|
+
const { unmount: unmountFirst } = renderHook(() => useDefineAppContext(namespace, { version: 'old' }));
|
|
41
|
+
|
|
42
|
+
expect(getContext(namespace)).toEqual({ version: 'old' });
|
|
43
|
+
|
|
44
|
+
// Simulate the race: mount a second instance WITHOUT unmounting the first.
|
|
45
|
+
// This is what happens when a new single-spa lifecycle mounts before the
|
|
46
|
+
// old one's cleanup effect has run.
|
|
47
|
+
const { unmount: unmountSecond } = renderHook(() => useDefineAppContext(namespace, { version: 'new' }));
|
|
48
|
+
|
|
49
|
+
// The second instance should have updated the context, not crashed
|
|
50
|
+
expect(getContext(namespace)).toEqual({ version: 'new' });
|
|
51
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
52
|
+
expect.stringContaining(`Namespace ${namespace} is already registered`),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// The stale cleanup from the old instance must NOT remove the new instance's context
|
|
56
|
+
unmountFirst();
|
|
57
|
+
|
|
58
|
+
expect(getContext(namespace)).toEqual({ version: 'new' });
|
|
59
|
+
|
|
60
|
+
// Only the current owner's cleanup should unregister the context
|
|
61
|
+
unmountSecond();
|
|
62
|
+
|
|
63
|
+
expect(getContext(namespace)).toBeNull();
|
|
64
|
+
|
|
65
|
+
consoleWarnSpy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/** @module @category Context */
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { registerContext, unregisterContext, updateContext } from '@openmrs/esm-context';
|
|
3
|
+
import { getContext, registerContext, unregisterContext, updateContext } from '@openmrs/esm-context';
|
|
4
4
|
import { shallowEqual } from '@openmrs/esm-utils';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Tracks the current owner of each namespace so that a stale cleanup from a
|
|
8
|
+
* previous instance does not unregister a namespace that a newer instance now owns.
|
|
9
|
+
*/
|
|
10
|
+
const namespaceOwners = new Map<string, symbol>();
|
|
11
|
+
|
|
6
12
|
/**
|
|
7
13
|
* This hook is used to register a namespace in the AppContext. The component that registers the
|
|
8
14
|
* namespace is responsible for updating the value associated with the namespace. The namespace
|
|
@@ -37,12 +43,36 @@ import { shallowEqual } from '@openmrs/esm-utils';
|
|
|
37
43
|
export function useDefineAppContext<T extends NonNullable<object> = NonNullable<object>>(namespace: string, value?: T) {
|
|
38
44
|
const previousValue = useRef<T>(value ?? ({} as T));
|
|
39
45
|
const updateFunction = useRef((update: (state: T) => T) => updateContext<T>(namespace, update));
|
|
46
|
+
const ownerToken = useRef(Symbol());
|
|
40
47
|
|
|
41
48
|
// effect hook for registration and unregistration
|
|
42
49
|
useEffect(() => {
|
|
43
|
-
|
|
50
|
+
const initialValue = value ?? ({} as T);
|
|
51
|
+
const token = ownerToken.current;
|
|
52
|
+
|
|
53
|
+
if (getContext(namespace) === null) {
|
|
54
|
+
registerContext(namespace, initialValue);
|
|
55
|
+
} else {
|
|
56
|
+
// The previous instance's cleanup effect hasn't run before this new instance
|
|
57
|
+
// mounts (e.g., during navigation when extensions unmount/remount across
|
|
58
|
+
// separate single-spa lifecycles). Update the existing context instead.
|
|
59
|
+
console.warn(
|
|
60
|
+
`Namespace ${namespace} is already registered in the app context. ` +
|
|
61
|
+
`This is likely a race condition during navigation, but may indicate two components ` +
|
|
62
|
+
`are trying to own the same namespace. Updating the existing context.`,
|
|
63
|
+
);
|
|
64
|
+
updateContext<T>(namespace, () => initialValue);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
namespaceOwners.set(namespace, token);
|
|
68
|
+
|
|
44
69
|
return () => {
|
|
45
|
-
|
|
70
|
+
// Only unregister if this instance is still the current owner.
|
|
71
|
+
// A newer instance may have taken ownership during a lifecycle race.
|
|
72
|
+
if (namespaceOwners.get(namespace) === token) {
|
|
73
|
+
unregisterContext(namespace);
|
|
74
|
+
namespaceOwners.delete(namespace);
|
|
75
|
+
}
|
|
46
76
|
};
|
|
47
77
|
}, [namespace]);
|
|
48
78
|
|