@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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 53 files with swc (142.49ms)
1
+ [0] Successfully compiled: 53 files with swc (163.64ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -1 +1 @@
1
- {"version":3,"file":"useDefineAppContext.d.ts","sourceRoot":"","sources":["../src/useDefineAppContext.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,UAmBvD"}
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
- registerContext(namespace, value ?? {});
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
- unregisterContext(namespace);
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.3935",
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.3935",
83
- "@openmrs/esm-config": "8.0.1-pre.3935",
84
- "@openmrs/esm-context": "8.0.1-pre.3935",
85
- "@openmrs/esm-emr-api": "8.0.1-pre.3935",
86
- "@openmrs/esm-error-handling": "8.0.1-pre.3935",
87
- "@openmrs/esm-extensions": "8.0.1-pre.3935",
88
- "@openmrs/esm-feature-flags": "8.0.1-pre.3935",
89
- "@openmrs/esm-globals": "8.0.1-pre.3935",
90
- "@openmrs/esm-navigation": "8.0.1-pre.3935",
91
- "@openmrs/esm-state": "8.0.1-pre.3935",
92
- "@openmrs/esm-utils": "8.0.1-pre.3935",
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
- registerContext(namespace, value ?? ({} as T));
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
- unregisterContext(namespace);
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