@openmrs/esm-api 10.0.1-pre.5539 → 10.0.1-pre.5545
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/current-user.d.ts +30 -41
- package/dist/current-user.d.ts.map +1 -1
- package/dist/current-user.js +54 -32
- package/dist/openmrs-fetch.d.ts +0 -32
- package/dist/openmrs-fetch.d.ts.map +1 -1
- package/dist/openmrs-fetch.js +1 -53
- package/mock-jest.ts +1 -3
- package/mock.ts +1 -3
- package/package.json +9 -10
- package/src/current-user.test.ts +120 -154
- package/src/current-user.ts +89 -66
- package/src/openmrs-fetch.test.ts +1 -60
- package/src/openmrs-fetch.ts +0 -62
package/.turbo/turbo-build.log
CHANGED
package/dist/current-user.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
2
1
|
import type { LoggedInUser, SessionLocation, Privilege, Role, Session } from './types';
|
|
3
2
|
export type SessionStore = LoadedSessionStore | UnloadedSessionStore;
|
|
4
3
|
export type LoadedSessionStore = {
|
|
@@ -12,62 +11,52 @@ export type UnloadedSessionStore = {
|
|
|
12
11
|
/** @internal */
|
|
13
12
|
export declare const sessionStore: import("zustand").StoreApi<SessionStore>;
|
|
14
13
|
/**
|
|
15
|
-
* The getCurrentUser function returns
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* (if `includeAuthStatus` is
|
|
14
|
+
* The getCurrentUser function returns a Promise that resolves once with the
|
|
15
|
+
* current user's session. If the session hasn't been loaded, was loaded more than
|
|
16
|
+
* a minute ago, or is in the middle of being refetched, the Promise waits for the
|
|
17
|
+
* fetch in question rather than resolving with data that may be out of date. The
|
|
18
|
+
* session it resolves with is therefore never more than a minute old.
|
|
19
|
+
*
|
|
20
|
+
* The function accepts an optional `opts` object with an `includeAuthStatus` boolean
|
|
21
|
+
* property that defaults to `true`. When `true`, the entire {@link Session} object
|
|
22
|
+
* from the API is provided. When `false`, only the {@link LoggedInUser} property of
|
|
23
|
+
* the response is provided.
|
|
24
|
+
*
|
|
25
|
+
* To react to subsequent session changes (login, logout, user-property updates),
|
|
26
|
+
* use {@link getSessionStore} (`getState()` / `subscribe()`) or the `useSession`
|
|
27
|
+
* React hook rather than calling this repeatedly.
|
|
28
|
+
*
|
|
29
|
+
* @returns A Promise resolving to a {@link LoggedInUser} object (if `includeAuthStatus`
|
|
30
|
+
* is `false`) or a {@link Session} object (if `includeAuthStatus` is `true` or not
|
|
31
|
+
* provided).
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
*
|
|
35
35
|
* ```js
|
|
36
36
|
* import { getCurrentUser } from '@openmrs/esm-api'
|
|
37
|
-
* const
|
|
38
|
-
*
|
|
39
|
-
* )
|
|
40
|
-
* subscription.unsubscribe()
|
|
41
|
-
* getCurrentUser({includeAuthStatus: true}).subscribe(
|
|
42
|
-
* data => console.log(data.authenticated)
|
|
43
|
-
* )
|
|
37
|
+
* const session = await getCurrentUser({ includeAuthStatus: true })
|
|
38
|
+
* console.log(session.authenticated)
|
|
44
39
|
* ```
|
|
45
|
-
*
|
|
46
|
-
* #### Be sure to unsubscribe when your component unmounts
|
|
47
|
-
*
|
|
48
|
-
* Otherwise your code will continue getting updates to the user object
|
|
49
|
-
* even after the UI component is gone from the screen. This is a memory
|
|
50
|
-
* leak and source of bugs.
|
|
51
40
|
*/
|
|
52
|
-
declare function getCurrentUser():
|
|
41
|
+
declare function getCurrentUser(): Promise<Session>;
|
|
53
42
|
/**
|
|
54
43
|
* @param opts Options for controlling the response format.
|
|
55
|
-
* @param opts.includeAuthStatus When `true`,
|
|
44
|
+
* @param opts.includeAuthStatus When `true`, resolves with the full {@link Session} object
|
|
56
45
|
* including authentication status.
|
|
57
|
-
* @returns
|
|
46
|
+
* @returns A Promise resolving to a {@link Session} object.
|
|
58
47
|
*/
|
|
59
48
|
declare function getCurrentUser(opts: {
|
|
60
49
|
includeAuthStatus: true;
|
|
61
|
-
}):
|
|
50
|
+
}): Promise<Session>;
|
|
62
51
|
/**
|
|
63
52
|
* @param opts Options for controlling the response format.
|
|
64
|
-
* @param opts.includeAuthStatus When `false`,
|
|
53
|
+
* @param opts.includeAuthStatus When `false`, resolves with only the {@link LoggedInUser} object
|
|
65
54
|
* without the surrounding session information.
|
|
66
|
-
* @returns
|
|
55
|
+
* @returns A Promise resolving to a {@link LoggedInUser} object.
|
|
67
56
|
*/
|
|
68
57
|
declare function getCurrentUser(opts: {
|
|
69
58
|
includeAuthStatus: false;
|
|
70
|
-
}):
|
|
59
|
+
}): Promise<LoggedInUser>;
|
|
71
60
|
export { getCurrentUser };
|
|
72
61
|
/**
|
|
73
62
|
* Returns the global session store containing the current user's session information.
|
|
@@ -102,10 +91,10 @@ export declare function getSessionStore(): import("zustand").StoreApi<SessionSto
|
|
|
102
91
|
export declare function setUserLanguage(data: Session): void;
|
|
103
92
|
/**
|
|
104
93
|
* The `refetchCurrentUser` function causes a network request to redownload
|
|
105
|
-
* the user. All subscribers to the
|
|
106
|
-
* new
|
|
94
|
+
* the user. All subscribers to the session store will be notified of the
|
|
95
|
+
* new user once the new version of the user object is downloaded.
|
|
107
96
|
*
|
|
108
|
-
* @returns
|
|
97
|
+
* @returns A Promise resolving to the updated session store state.
|
|
109
98
|
*
|
|
110
99
|
* @example
|
|
111
100
|
* ```js
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-user.d.ts","sourceRoot":"","sources":["../src/current-user.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"current-user.d.ts","sourceRoot":"","sources":["../src/current-user.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAiB,MAAM,SAAS,CAAC;AAEtG,MAAM,MAAM,YAAY,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,KAAK,CAAC;IACd,OAAO,EAAE,IAAI,CAAC;CACf,CAAC;AAEF,gBAAgB;AAChB,eAAO,MAAM,YAAY,0CAGvB,CAAC;AAoCH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,iBAAS,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;AAC5C;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAC7E;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,KAAK,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAmBnF,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,6CAG9B;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,QAc5C;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,yBAwBtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,SAK/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EACzC,IAAI,EAAE;IAAE,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;CAAE,WAa3D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,0BAmB9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAG/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,CAW7G;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE;IACd,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,EACD,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,YAAY,CAAC,CAcvB"}
|
package/dist/current-user.js
CHANGED
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
/** @module @category API */ import { reportError } from "@openmrs/esm-error-handling";
|
|
2
2
|
import { createGlobalStore } from "@openmrs/esm-state";
|
|
3
3
|
import { isUndefined } from "lodash-es";
|
|
4
|
-
import { Observable } from "rxjs";
|
|
5
4
|
import { openmrsFetch, restBaseUrl, sessionEndpoint } from "./openmrs-fetch.js";
|
|
6
5
|
/** @internal */ export const sessionStore = createGlobalStore('session', {
|
|
7
6
|
loaded: false,
|
|
8
7
|
session: null
|
|
9
8
|
});
|
|
9
|
+
/**
|
|
10
|
+
* The upper bound on how old the session handed to a reader may be. It is measured from the moment a
|
|
11
|
+
* fetch is *started*, not from the moment its response lands, so the session a reader sees is always
|
|
12
|
+
* strictly newer than this.
|
|
13
|
+
*/ const sessionMaxAgeMillis = 60 * 1000;
|
|
10
14
|
let lastFetchTimeMillis = 0;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
let inFlightRefresh = null;
|
|
16
|
+
/**
|
|
17
|
+
* Fetches the session if the store is unloaded or its session is older than `sessionMaxAgeMillis`.
|
|
18
|
+
* Returns `null` only when the store already holds a session fresh enough to be read as-is, so a
|
|
19
|
+
* caller that gets a promise back must wait for it rather than read the store.
|
|
20
|
+
*
|
|
21
|
+
* A fetch already in flight is joined rather than duplicated. This is what keeps `sessionMaxAgeMillis`
|
|
22
|
+
* an upper bound: while a fetch is running, the store still holds the previous session, and returning
|
|
23
|
+
* that would hand back data older than the bound allows.
|
|
24
|
+
*
|
|
25
|
+
* The returned promise rejects if the fetch fails, but the rejection is already handled and reported,
|
|
26
|
+
* so a caller that only needs the fetch started can ignore it.
|
|
27
|
+
*/ function refreshSessionIfStale() {
|
|
28
|
+
if (inFlightRefresh) {
|
|
29
|
+
return inFlightRefresh;
|
|
16
30
|
}
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
if (sessionStore.getState().loaded && lastFetchTimeMillis >= Date.now() - sessionMaxAgeMillis) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return refetchCurrentUser();
|
|
35
|
+
}
|
|
36
|
+
function getCurrentUser(opts) {
|
|
37
|
+
const includeAuthStatus = opts?.includeAuthStatus ?? true;
|
|
38
|
+
const select = (session)=>includeAuthStatus ? session : session.user;
|
|
39
|
+
if (!refreshSessionIfStale()) {
|
|
40
|
+
return Promise.resolve(select(sessionStore.getState().session));
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve)=>{
|
|
43
|
+
const unsubscribe = sessionStore.subscribe((state)=>{
|
|
19
44
|
if (state.loaded) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} else {
|
|
23
|
-
subscriber.next(state.session?.user);
|
|
24
|
-
}
|
|
45
|
+
unsubscribe();
|
|
46
|
+
resolve(select(state.session));
|
|
25
47
|
}
|
|
26
|
-
};
|
|
27
|
-
handler(sessionStore.getState());
|
|
28
|
-
// The observable subscribe function should return an unsubscribe function,
|
|
29
|
-
// which happens to be exactly what `subscribe` returns.
|
|
30
|
-
return sessionStore.subscribe(handler);
|
|
48
|
+
});
|
|
31
49
|
});
|
|
32
50
|
}
|
|
33
51
|
export { getCurrentUser };
|
|
@@ -49,9 +67,7 @@ export { getCurrentUser };
|
|
|
49
67
|
* });
|
|
50
68
|
* ```
|
|
51
69
|
*/ export function getSessionStore() {
|
|
52
|
-
|
|
53
|
-
refetchCurrentUser();
|
|
54
|
-
}
|
|
70
|
+
refreshSessionIfStale();
|
|
55
71
|
return sessionStore;
|
|
56
72
|
}
|
|
57
73
|
// NB locale is string only if this returns true
|
|
@@ -109,10 +125,10 @@ function isSuperUser(user) {
|
|
|
109
125
|
}
|
|
110
126
|
/**
|
|
111
127
|
* The `refetchCurrentUser` function causes a network request to redownload
|
|
112
|
-
* the user. All subscribers to the
|
|
113
|
-
* new
|
|
128
|
+
* the user. All subscribers to the session store will be notified of the
|
|
129
|
+
* new user once the new version of the user object is downloaded.
|
|
114
130
|
*
|
|
115
|
-
* @returns
|
|
131
|
+
* @returns A Promise resolving to the updated session store state.
|
|
116
132
|
*
|
|
117
133
|
* @example
|
|
118
134
|
* ```js
|
|
@@ -125,9 +141,19 @@ function isSuperUser(user) {
|
|
|
125
141
|
if (username && password) {
|
|
126
142
|
headers['Authorization'] = `Basic ${window.btoa(`${username}:${password}`)}`;
|
|
127
143
|
}
|
|
128
|
-
|
|
144
|
+
const refresh = handleSessionResponse(openmrsFetch(sessionEndpoint, {
|
|
129
145
|
headers
|
|
130
146
|
}));
|
|
147
|
+
// Publish the request so that readers can wait on it instead of reading the session it is about to
|
|
148
|
+
// replace. Each call still issues its own request; this only tracks the most recent one.
|
|
149
|
+
const clear = ()=>{
|
|
150
|
+
if (inFlightRefresh === refresh) {
|
|
151
|
+
inFlightRefresh = null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
refresh.then(clear, clear);
|
|
155
|
+
inFlightRefresh = refresh;
|
|
156
|
+
return refresh;
|
|
131
157
|
}
|
|
132
158
|
/**
|
|
133
159
|
* Clears the current user session from the session store, setting the session
|
|
@@ -226,13 +252,9 @@ function isSuperUser(user) {
|
|
|
226
252
|
* console.log('Current location:', location.display);
|
|
227
253
|
* }
|
|
228
254
|
* ```
|
|
229
|
-
*/ export function getSessionLocation() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
res(session.sessionLocation);
|
|
233
|
-
}, rej);
|
|
234
|
-
sub.unsubscribe();
|
|
235
|
-
});
|
|
255
|
+
*/ export async function getSessionLocation() {
|
|
256
|
+
const session = await getCurrentUser();
|
|
257
|
+
return session.sessionLocation;
|
|
236
258
|
}
|
|
237
259
|
/**
|
|
238
260
|
* Sets the session location for the current user. The session location represents
|
package/dist/openmrs-fetch.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** @module @category API */
|
|
2
|
-
import { Observable } from 'rxjs';
|
|
3
1
|
import type { FetchResponse } from './types';
|
|
4
2
|
/** The base URL for the OpenMRS REST API (e.g., '/ws/rest/v1'). */
|
|
5
3
|
export declare const restBaseUrl = "/ws/rest/v1";
|
|
@@ -72,36 +70,6 @@ export declare function makeUrl(path: string): string;
|
|
|
72
70
|
* @category API
|
|
73
71
|
*/
|
|
74
72
|
export declare function openmrsFetch<T = any>(path: string, fetchInit?: FetchConfig): Promise<FetchResponse<T>>;
|
|
75
|
-
/**
|
|
76
|
-
* The openmrsObservableFetch function is a wrapper around openmrsFetch
|
|
77
|
-
* that returns an [Observable](https://rxjs-dev.firebaseapp.com/guide/observable)
|
|
78
|
-
* instead of a promise. It exists in case using an Observable is
|
|
79
|
-
* preferred or more convenient than a promise.
|
|
80
|
-
*
|
|
81
|
-
* @param url See [[openmrsFetch]]
|
|
82
|
-
* @param fetchInit See [[openmrsFetch]]
|
|
83
|
-
* @returns An Observable that produces exactly one Response object.
|
|
84
|
-
* The response object is exactly the same as for [[openmrsFetch]].
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
*
|
|
88
|
-
* ```js
|
|
89
|
-
* import { openmrsObservableFetch } from '@openmrs/esm-api'
|
|
90
|
-
* const subscription = openmrsObservableFetch(`${restBaseUrl}/session').subscribe(
|
|
91
|
-
* response => console.log(response.data),
|
|
92
|
-
* err => {throw err},
|
|
93
|
-
* () => console.log('finished')
|
|
94
|
-
* )
|
|
95
|
-
* subscription.unsubscribe()
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* #### Cancellation
|
|
99
|
-
*
|
|
100
|
-
* To cancel the network request, simply call `subscription.unsubscribe();`
|
|
101
|
-
*
|
|
102
|
-
* @category API
|
|
103
|
-
*/
|
|
104
|
-
export declare function openmrsObservableFetch<T>(url: string, fetchInit?: FetchConfig): Observable<FetchResponse<T>>;
|
|
105
73
|
export declare class OpenmrsFetchError extends Error implements FetchError {
|
|
106
74
|
constructor(url: string, response: Response, responseBody: ResponseBody | null, requestStacktrace: Error);
|
|
107
75
|
response: Response;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openmrs-fetch.d.ts","sourceRoot":"","sources":["../src/openmrs-fetch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openmrs-fetch.d.ts","sourceRoot":"","sources":["../src/openmrs-fetch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C,mEAAmE;AACnE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,uEAAuE;AACvE,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAC1C,sDAAsD;AACtD,eAAO,MAAM,eAAe,wBAA2B,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UASnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,YAAY,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,WAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAuK1G;AAED,qBAAa,iBAAkB,SAAQ,KAAM,YAAW,UAAU;gBACpD,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,EAAE,iBAAiB,EAAE,KAAK;IAQxG,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACxE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAC3B;AAED,KAAK,YAAY,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,UAAU,SAAS;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;CACnC"}
|
package/dist/openmrs-fetch.js
CHANGED
|
@@ -11,8 +11,7 @@ function _define_property(obj, key, value) {
|
|
|
11
11
|
}
|
|
12
12
|
return obj;
|
|
13
13
|
}
|
|
14
|
-
/** @module @category API */ import {
|
|
15
|
-
import { isPlainObject } from "lodash-es";
|
|
14
|
+
/** @module @category API */ import { isPlainObject } from "lodash-es";
|
|
16
15
|
import { getConfig } from "@openmrs/esm-config";
|
|
17
16
|
import { clearHistory, navigate } from "@openmrs/esm-navigation";
|
|
18
17
|
import { defaultRedirectAuthFailureUrl } from "./config-schema.js";
|
|
@@ -216,57 +215,6 @@ import { defaultRedirectAuthFailureUrl } from "./config-schema.js";
|
|
|
216
215
|
}
|
|
217
216
|
});
|
|
218
217
|
}
|
|
219
|
-
/**
|
|
220
|
-
* The openmrsObservableFetch function is a wrapper around openmrsFetch
|
|
221
|
-
* that returns an [Observable](https://rxjs-dev.firebaseapp.com/guide/observable)
|
|
222
|
-
* instead of a promise. It exists in case using an Observable is
|
|
223
|
-
* preferred or more convenient than a promise.
|
|
224
|
-
*
|
|
225
|
-
* @param url See [[openmrsFetch]]
|
|
226
|
-
* @param fetchInit See [[openmrsFetch]]
|
|
227
|
-
* @returns An Observable that produces exactly one Response object.
|
|
228
|
-
* The response object is exactly the same as for [[openmrsFetch]].
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
*
|
|
232
|
-
* ```js
|
|
233
|
-
* import { openmrsObservableFetch } from '@openmrs/esm-api'
|
|
234
|
-
* const subscription = openmrsObservableFetch(`${restBaseUrl}/session').subscribe(
|
|
235
|
-
* response => console.log(response.data),
|
|
236
|
-
* err => {throw err},
|
|
237
|
-
* () => console.log('finished')
|
|
238
|
-
* )
|
|
239
|
-
* subscription.unsubscribe()
|
|
240
|
-
* ```
|
|
241
|
-
*
|
|
242
|
-
* #### Cancellation
|
|
243
|
-
*
|
|
244
|
-
* To cancel the network request, simply call `subscription.unsubscribe();`
|
|
245
|
-
*
|
|
246
|
-
* @category API
|
|
247
|
-
*/ export function openmrsObservableFetch(url, fetchInit = {}) {
|
|
248
|
-
if (typeof fetchInit !== 'object') {
|
|
249
|
-
throw Error('The second argument to openmrsObservableFetch must be either omitted or an object');
|
|
250
|
-
}
|
|
251
|
-
const abortController = new AbortController();
|
|
252
|
-
fetchInit.signal = abortController.signal;
|
|
253
|
-
return new Observable((observer)=>{
|
|
254
|
-
let hasResponse = false;
|
|
255
|
-
openmrsFetch(url, fetchInit).then((response)=>{
|
|
256
|
-
hasResponse = true;
|
|
257
|
-
observer.next(response);
|
|
258
|
-
observer.complete();
|
|
259
|
-
}, (err)=>{
|
|
260
|
-
hasResponse = true;
|
|
261
|
-
observer.error(err);
|
|
262
|
-
});
|
|
263
|
-
return ()=>{
|
|
264
|
-
if (!hasResponse) {
|
|
265
|
-
abortController.abort();
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
218
|
export class OpenmrsFetchError extends Error {
|
|
271
219
|
constructor(url, response, responseBody, requestStacktrace){
|
|
272
220
|
super(), _define_property(this, "response", void 0), _define_property(this, "responseBody", void 0);
|
package/mock-jest.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { of } from 'rxjs';
|
|
2
1
|
import { createGlobalStore } from '@openmrs/esm-state/mock';
|
|
3
2
|
import { type SessionStore } from './src/current-user';
|
|
4
3
|
|
|
5
4
|
export const setSessionLocation = jest.fn(() => Promise.resolve());
|
|
6
5
|
export const openmrsFetch = jest.fn((url?: string) => new Promise(() => {}));
|
|
7
|
-
export const openmrsObservableFetch = jest.fn(() => of({ data: { entry: [] } }));
|
|
8
6
|
export function getCurrentUser() {
|
|
9
|
-
return
|
|
7
|
+
return Promise.resolve({ authenticated: false });
|
|
10
8
|
}
|
|
11
9
|
export const mockSessionStore = createGlobalStore<SessionStore>('mock-session-store', {
|
|
12
10
|
loaded: false,
|
package/mock.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { vi } from 'vitest';
|
|
2
|
-
import { of } from 'rxjs';
|
|
3
2
|
import { createGlobalStore } from '@openmrs/esm-state/mock';
|
|
4
3
|
import { type SessionStore } from './src/current-user';
|
|
5
4
|
|
|
6
5
|
export const setSessionLocation = vi.fn(() => Promise.resolve());
|
|
7
6
|
export const openmrsFetch = vi.fn((_url?: string) => new Promise(() => {}));
|
|
8
|
-
export const openmrsObservableFetch = vi.fn(() => of({ data: { entry: [] } }));
|
|
9
7
|
export function getCurrentUser() {
|
|
10
|
-
return
|
|
8
|
+
return Promise.resolve({ authenticated: false });
|
|
11
9
|
}
|
|
12
10
|
export const mockSessionStore = createGlobalStore<SessionStore>('mock-session-store', {
|
|
13
11
|
loaded: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-api",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5545",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The javascript module for interacting with the OpenMRS API",
|
|
6
6
|
"type": "module",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"lodash-es": "^4.17.21"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@openmrs/esm-config": "^10.0.1-pre.
|
|
61
|
-
"@openmrs/esm-error-handling": "^10.0.1-pre.
|
|
62
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
63
|
-
"@openmrs/esm-navigation": "^10.0.1-pre.
|
|
60
|
+
"@openmrs/esm-config": "^10.0.1-pre.5545",
|
|
61
|
+
"@openmrs/esm-error-handling": "^10.0.1-pre.5545",
|
|
62
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5545",
|
|
63
|
+
"@openmrs/esm-navigation": "^10.0.1-pre.5545"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@openmrs/esm-config": "10.0.1-pre.
|
|
67
|
-
"@openmrs/esm-error-handling": "10.0.1-pre.
|
|
68
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
69
|
-
"@openmrs/esm-navigation": "10.0.1-pre.
|
|
66
|
+
"@openmrs/esm-config": "10.0.1-pre.5545",
|
|
67
|
+
"@openmrs/esm-error-handling": "10.0.1-pre.5545",
|
|
68
|
+
"@openmrs/esm-globals": "10.0.1-pre.5545",
|
|
69
|
+
"@openmrs/esm-navigation": "10.0.1-pre.5545",
|
|
70
70
|
"@swc/cli": "0.8.1",
|
|
71
71
|
"@swc/core": "1.15.21",
|
|
72
72
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"cross-env": "^10.1.0",
|
|
75
75
|
"happy-dom": "^20.6.0",
|
|
76
76
|
"rimraf": "^6.1.3",
|
|
77
|
-
"rxjs": "^6.5.3",
|
|
78
77
|
"vitest": "^4.1.2"
|
|
79
78
|
},
|
|
80
79
|
"stableVersion": "10.0.0"
|
package/src/current-user.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import '@testing-library/jest-dom/vitest';
|
|
3
|
-
import { Observable } from 'rxjs';
|
|
4
3
|
import {
|
|
5
4
|
userHasAccess,
|
|
6
5
|
getCurrentUser,
|
|
@@ -314,11 +313,10 @@ describe('getLoggedInUser', () => {
|
|
|
314
313
|
});
|
|
315
314
|
|
|
316
315
|
describe('getCurrentUser', () => {
|
|
317
|
-
let subscriptions: Array<{ unsubscribe?: () => void }> = [];
|
|
318
|
-
|
|
319
316
|
beforeEach(() => {
|
|
320
317
|
sessionStore.setState({ loaded: false, session: null });
|
|
321
318
|
mockOpenmrsFetch.mockClear();
|
|
319
|
+
mockReportError.mockClear();
|
|
322
320
|
// Mock openmrsFetch to prevent unhandled promise rejections
|
|
323
321
|
mockOpenmrsFetch.mockResolvedValue(
|
|
324
322
|
createMockFetchResponse({
|
|
@@ -328,183 +326,151 @@ describe('getCurrentUser', () => {
|
|
|
328
326
|
);
|
|
329
327
|
});
|
|
330
328
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
329
|
+
const buildMockUser = (): LoggedInUser => ({
|
|
330
|
+
uuid: 'user-uuid',
|
|
331
|
+
display: 'Test User',
|
|
332
|
+
username: 'testuser',
|
|
333
|
+
systemId: 'test-sys-id',
|
|
334
|
+
userProperties: {},
|
|
335
|
+
person: {} as any,
|
|
336
|
+
privileges: [],
|
|
337
|
+
roles: [],
|
|
338
|
+
retired: false,
|
|
339
|
+
locale: 'en',
|
|
340
|
+
allowedLocales: ['en'],
|
|
335
341
|
});
|
|
336
342
|
|
|
337
|
-
it('should return
|
|
338
|
-
|
|
339
|
-
expect(result).toBeInstanceOf(Observable);
|
|
343
|
+
it('should return a Promise', () => {
|
|
344
|
+
expect(getCurrentUser()).toBeInstanceOf(Promise);
|
|
340
345
|
});
|
|
341
346
|
|
|
342
|
-
it('should
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
userProperties: {},
|
|
350
|
-
person: {} as any,
|
|
351
|
-
privileges: [],
|
|
352
|
-
roles: [],
|
|
353
|
-
retired: false,
|
|
354
|
-
locale: 'en',
|
|
355
|
-
allowedLocales: ['en'],
|
|
356
|
-
};
|
|
347
|
+
it('should resolve with the user when the session is fresh and includeAuthStatus is false', async () => {
|
|
348
|
+
const mockUser = buildMockUser();
|
|
349
|
+
mockOpenmrsFetch.mockResolvedValue(
|
|
350
|
+
createMockFetchResponse({ authenticated: true, sessionId: 'test-session', user: mockUser }),
|
|
351
|
+
);
|
|
352
|
+
await refetchCurrentUser();
|
|
353
|
+
mockOpenmrsFetch.mockClear();
|
|
357
354
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
authenticated: true,
|
|
362
|
-
sessionId: 'test-session',
|
|
363
|
-
user: mockUser,
|
|
364
|
-
},
|
|
365
|
-
});
|
|
355
|
+
await expect(getCurrentUser({ includeAuthStatus: false })).resolves.toEqual(mockUser);
|
|
356
|
+
expect(mockOpenmrsFetch).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
366
358
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
359
|
+
it('should resolve with the full session when includeAuthStatus is true', async () => {
|
|
360
|
+
const mockSession: Session = {
|
|
361
|
+
authenticated: true,
|
|
362
|
+
sessionId: 'test-session',
|
|
363
|
+
user: buildMockUser(),
|
|
364
|
+
};
|
|
365
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
366
|
+
await refetchCurrentUser();
|
|
367
|
+
mockOpenmrsFetch.mockClear();
|
|
368
|
+
|
|
369
|
+
await expect(getCurrentUser({ includeAuthStatus: true })).resolves.toEqual(mockSession);
|
|
370
|
+
expect(mockOpenmrsFetch).not.toHaveBeenCalled();
|
|
373
371
|
});
|
|
374
372
|
|
|
375
|
-
it('should
|
|
376
|
-
|
|
377
|
-
const mockUser: LoggedInUser = {
|
|
378
|
-
uuid: 'user-uuid',
|
|
379
|
-
display: 'Test User',
|
|
380
|
-
username: 'testuser',
|
|
381
|
-
systemId: 'test-sys-id',
|
|
382
|
-
userProperties: {},
|
|
383
|
-
person: {} as any,
|
|
384
|
-
privileges: [],
|
|
385
|
-
roles: [],
|
|
386
|
-
retired: false,
|
|
387
|
-
locale: 'en',
|
|
388
|
-
allowedLocales: ['en'],
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const mockSession: Session = {
|
|
392
|
-
authenticated: true,
|
|
393
|
-
sessionId: 'test-session',
|
|
394
|
-
user: mockUser,
|
|
395
|
-
};
|
|
373
|
+
it('should refetch and resolve with the fresh session when the loaded session is stale', async () => {
|
|
374
|
+
vi.useFakeTimers();
|
|
396
375
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
});
|
|
376
|
+
try {
|
|
377
|
+
const staleSession: Session = { authenticated: true, sessionId: 'stale-session', user: buildMockUser() };
|
|
378
|
+
const freshSession: Session = { authenticated: true, sessionId: 'fresh-session', user: buildMockUser() };
|
|
401
379
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
380
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(staleSession));
|
|
381
|
+
await refetchCurrentUser();
|
|
382
|
+
|
|
383
|
+
vi.setSystemTime(Date.now() + 61 * 1000);
|
|
384
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(freshSession));
|
|
385
|
+
|
|
386
|
+
await expect(getCurrentUser({ includeAuthStatus: true })).resolves.toEqual(freshSession);
|
|
387
|
+
} finally {
|
|
388
|
+
vi.useRealTimers();
|
|
389
|
+
}
|
|
408
390
|
});
|
|
409
391
|
|
|
410
|
-
it('should
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
subscriptions.push(sub);
|
|
414
|
-
|
|
415
|
-
expect(handler).not.toHaveBeenCalled();
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('should emit updates when session changes', () => {
|
|
419
|
-
return new Promise<void>((resolve) => {
|
|
420
|
-
const mockUser1: LoggedInUser = {
|
|
421
|
-
uuid: 'user-1',
|
|
422
|
-
display: 'User 1',
|
|
423
|
-
username: 'user1',
|
|
424
|
-
systemId: 'sys-1',
|
|
425
|
-
userProperties: {},
|
|
426
|
-
person: {} as any,
|
|
427
|
-
privileges: [],
|
|
428
|
-
roles: [],
|
|
429
|
-
retired: false,
|
|
430
|
-
locale: 'en',
|
|
431
|
-
allowedLocales: ['en'],
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const mockUser2: LoggedInUser = {
|
|
435
|
-
uuid: 'user-2',
|
|
436
|
-
display: 'User 2',
|
|
437
|
-
username: 'user2',
|
|
438
|
-
systemId: 'sys-2',
|
|
439
|
-
userProperties: {},
|
|
440
|
-
person: {} as any,
|
|
441
|
-
privileges: [],
|
|
442
|
-
roles: [],
|
|
443
|
-
retired: false,
|
|
444
|
-
locale: 'en',
|
|
445
|
-
allowedLocales: ['en'],
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const emittedUsers: LoggedInUser[] = [];
|
|
392
|
+
it('should wait for a refetch already in flight rather than resolving with the session it replaces', async () => {
|
|
393
|
+
const oldSession: Session = { authenticated: true, sessionId: 'old-session', user: buildMockUser() };
|
|
394
|
+
const newSession: Session = { authenticated: true, sessionId: 'new-session', user: buildMockUser() };
|
|
449
395
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
authenticated: true,
|
|
454
|
-
sessionId: 'session-1',
|
|
455
|
-
user: mockUser1,
|
|
456
|
-
},
|
|
457
|
-
});
|
|
396
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(oldSession));
|
|
397
|
+
await refetchCurrentUser();
|
|
398
|
+
mockOpenmrsFetch.mockClear();
|
|
458
399
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
400
|
+
// The store still holds `oldSession` and its fetch timestamp is well inside the freshness window,
|
|
401
|
+
// so only the in-flight refetch stops `getCurrentUser` from handing back the session on its way out.
|
|
402
|
+
let resolveFetch: (response: unknown) => void;
|
|
403
|
+
mockOpenmrsFetch.mockReturnValue(
|
|
404
|
+
new Promise((resolve) => {
|
|
405
|
+
resolveFetch = resolve;
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
const refetch = refetchCurrentUser();
|
|
409
|
+
|
|
410
|
+
const pending = getCurrentUser({ includeAuthStatus: true });
|
|
411
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledTimes(1);
|
|
412
|
+
|
|
413
|
+
resolveFetch!(createMockFetchResponse(newSession));
|
|
414
|
+
await refetch;
|
|
415
|
+
|
|
416
|
+
await expect(pending).resolves.toEqual(newSession);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should share a single request between concurrent callers', async () => {
|
|
420
|
+
const mockSession: Session = {
|
|
421
|
+
authenticated: true,
|
|
422
|
+
sessionId: 'test-session',
|
|
423
|
+
user: buildMockUser(),
|
|
424
|
+
};
|
|
425
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
426
|
+
|
|
427
|
+
await expect(Promise.all([getCurrentUser(), getCurrentUser()])).resolves.toEqual([mockSession, mockSession]);
|
|
428
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledTimes(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should not reject when the fetch fails, resolving once a session is loaded', async () => {
|
|
432
|
+
mockOpenmrsFetch.mockRejectedValue(new Error('Network error'));
|
|
433
|
+
|
|
434
|
+
let resolvedSession: Session | undefined;
|
|
435
|
+
const promise = getCurrentUser({ includeAuthStatus: true }).then((session) => {
|
|
436
|
+
resolvedSession = session;
|
|
479
437
|
});
|
|
438
|
+
|
|
439
|
+
await vi.waitFor(() => expect(mockReportError).toHaveBeenCalled());
|
|
440
|
+
expect(resolvedSession).toBeUndefined();
|
|
441
|
+
|
|
442
|
+
const mockSession: Session = { authenticated: true, sessionId: 'test-session', user: buildMockUser() };
|
|
443
|
+
sessionStore.setState({ loaded: true, session: mockSession });
|
|
444
|
+
|
|
445
|
+
await promise;
|
|
446
|
+
expect(resolvedSession).toEqual(mockSession);
|
|
480
447
|
});
|
|
481
448
|
|
|
482
|
-
it('should
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
449
|
+
it('should not resolve until the session is loaded', async () => {
|
|
450
|
+
// Prevent the automatic refetch from resolving the session so the only way
|
|
451
|
+
// the promise settles is the manual store update below.
|
|
452
|
+
mockOpenmrsFetch.mockReturnValue(new Promise(() => {}));
|
|
453
|
+
const mockUser = buildMockUser();
|
|
486
454
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
authenticated: true,
|
|
491
|
-
sessionId: 'test-session',
|
|
492
|
-
user: {} as LoggedInUser,
|
|
493
|
-
},
|
|
455
|
+
let resolvedUser: LoggedInUser | undefined;
|
|
456
|
+
const promise = getCurrentUser({ includeAuthStatus: false }).then((user) => {
|
|
457
|
+
resolvedUser = user as LoggedInUser;
|
|
494
458
|
});
|
|
495
459
|
|
|
496
|
-
|
|
497
|
-
|
|
460
|
+
await Promise.resolve();
|
|
461
|
+
expect(resolvedUser).toBeUndefined();
|
|
498
462
|
|
|
499
463
|
sessionStore.setState({
|
|
500
464
|
loaded: true,
|
|
501
465
|
session: {
|
|
502
|
-
authenticated:
|
|
503
|
-
sessionId: '
|
|
466
|
+
authenticated: true,
|
|
467
|
+
sessionId: 'test-session',
|
|
468
|
+
user: mockUser,
|
|
504
469
|
},
|
|
505
470
|
});
|
|
506
471
|
|
|
507
|
-
|
|
472
|
+
await promise;
|
|
473
|
+
expect(resolvedUser).toEqual(mockUser);
|
|
508
474
|
});
|
|
509
475
|
});
|
|
510
476
|
|
package/src/current-user.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { reportError } from '@openmrs/esm-error-handling';
|
|
3
3
|
import { createGlobalStore } from '@openmrs/esm-state';
|
|
4
4
|
import { isUndefined } from 'lodash-es';
|
|
5
|
-
import { Observable } from 'rxjs';
|
|
6
5
|
import { openmrsFetch, restBaseUrl, sessionEndpoint } from './openmrs-fetch';
|
|
7
6
|
import type { LoggedInUser, SessionLocation, Privilege, Role, Session, FetchResponse } from './types';
|
|
8
7
|
|
|
@@ -23,80 +22,99 @@ export const sessionStore = createGlobalStore<SessionStore>('session', {
|
|
|
23
22
|
loaded: false,
|
|
24
23
|
session: null,
|
|
25
24
|
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The upper bound on how old the session handed to a reader may be. It is measured from the moment a
|
|
28
|
+
* fetch is *started*, not from the moment its response lands, so the session a reader sees is always
|
|
29
|
+
* strictly newer than this.
|
|
30
|
+
*/
|
|
31
|
+
const sessionMaxAgeMillis = 60 * 1000;
|
|
32
|
+
|
|
26
33
|
let lastFetchTimeMillis = 0;
|
|
34
|
+
let inFlightRefresh: Promise<SessionStore> | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetches the session if the store is unloaded or its session is older than `sessionMaxAgeMillis`.
|
|
38
|
+
* Returns `null` only when the store already holds a session fresh enough to be read as-is, so a
|
|
39
|
+
* caller that gets a promise back must wait for it rather than read the store.
|
|
40
|
+
*
|
|
41
|
+
* A fetch already in flight is joined rather than duplicated. This is what keeps `sessionMaxAgeMillis`
|
|
42
|
+
* an upper bound: while a fetch is running, the store still holds the previous session, and returning
|
|
43
|
+
* that would hand back data older than the bound allows.
|
|
44
|
+
*
|
|
45
|
+
* The returned promise rejects if the fetch fails, but the rejection is already handled and reported,
|
|
46
|
+
* so a caller that only needs the fetch started can ignore it.
|
|
47
|
+
*/
|
|
48
|
+
function refreshSessionIfStale(): Promise<SessionStore> | null {
|
|
49
|
+
if (inFlightRefresh) {
|
|
50
|
+
return inFlightRefresh;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (sessionStore.getState().loaded && lastFetchTimeMillis >= Date.now() - sessionMaxAgeMillis) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return refetchCurrentUser();
|
|
58
|
+
}
|
|
27
59
|
|
|
28
60
|
/**
|
|
29
|
-
* The getCurrentUser function returns
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
61
|
+
* The getCurrentUser function returns a Promise that resolves once with the
|
|
62
|
+
* current user's session. If the session hasn't been loaded, was loaded more than
|
|
63
|
+
* a minute ago, or is in the middle of being refetched, the Promise waits for the
|
|
64
|
+
* fetch in question rather than resolving with data that may be out of date. The
|
|
65
|
+
* session it resolves with is therefore never more than a minute old.
|
|
66
|
+
*
|
|
67
|
+
* The function accepts an optional `opts` object with an `includeAuthStatus` boolean
|
|
68
|
+
* property that defaults to `true`. When `true`, the entire {@link Session} object
|
|
69
|
+
* from the API is provided. When `false`, only the {@link LoggedInUser} property of
|
|
70
|
+
* the response is provided.
|
|
35
71
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* `includeAuthStatus` is `false`, only the {@link LoggedInUser} property of the
|
|
40
|
-
* response object will be provided.
|
|
72
|
+
* To react to subsequent session changes (login, logout, user-property updates),
|
|
73
|
+
* use {@link getSessionStore} (`getState()` / `subscribe()`) or the `useSession`
|
|
74
|
+
* React hook rather than calling this repeatedly.
|
|
41
75
|
*
|
|
42
|
-
* @returns
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* (if `includeAuthStatus` is set to `true` or not provided).
|
|
76
|
+
* @returns A Promise resolving to a {@link LoggedInUser} object (if `includeAuthStatus`
|
|
77
|
+
* is `false`) or a {@link Session} object (if `includeAuthStatus` is `true` or not
|
|
78
|
+
* provided).
|
|
46
79
|
*
|
|
47
80
|
* @example
|
|
48
81
|
*
|
|
49
82
|
* ```js
|
|
50
83
|
* import { getCurrentUser } from '@openmrs/esm-api'
|
|
51
|
-
* const
|
|
52
|
-
*
|
|
53
|
-
* )
|
|
54
|
-
* subscription.unsubscribe()
|
|
55
|
-
* getCurrentUser({includeAuthStatus: true}).subscribe(
|
|
56
|
-
* data => console.log(data.authenticated)
|
|
57
|
-
* )
|
|
84
|
+
* const session = await getCurrentUser({ includeAuthStatus: true })
|
|
85
|
+
* console.log(session.authenticated)
|
|
58
86
|
* ```
|
|
59
|
-
*
|
|
60
|
-
* #### Be sure to unsubscribe when your component unmounts
|
|
61
|
-
*
|
|
62
|
-
* Otherwise your code will continue getting updates to the user object
|
|
63
|
-
* even after the UI component is gone from the screen. This is a memory
|
|
64
|
-
* leak and source of bugs.
|
|
65
87
|
*/
|
|
66
|
-
function getCurrentUser():
|
|
88
|
+
function getCurrentUser(): Promise<Session>;
|
|
67
89
|
/**
|
|
68
90
|
* @param opts Options for controlling the response format.
|
|
69
|
-
* @param opts.includeAuthStatus When `true`,
|
|
91
|
+
* @param opts.includeAuthStatus When `true`, resolves with the full {@link Session} object
|
|
70
92
|
* including authentication status.
|
|
71
|
-
* @returns
|
|
93
|
+
* @returns A Promise resolving to a {@link Session} object.
|
|
72
94
|
*/
|
|
73
|
-
function getCurrentUser(opts: { includeAuthStatus: true }):
|
|
95
|
+
function getCurrentUser(opts: { includeAuthStatus: true }): Promise<Session>;
|
|
74
96
|
/**
|
|
75
97
|
* @param opts Options for controlling the response format.
|
|
76
|
-
* @param opts.includeAuthStatus When `false`,
|
|
98
|
+
* @param opts.includeAuthStatus When `false`, resolves with only the {@link LoggedInUser} object
|
|
77
99
|
* without the surrounding session information.
|
|
78
|
-
* @returns
|
|
100
|
+
* @returns A Promise resolving to a {@link LoggedInUser} object.
|
|
79
101
|
*/
|
|
80
|
-
function getCurrentUser(opts: { includeAuthStatus: false }):
|
|
81
|
-
function getCurrentUser(opts
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
function getCurrentUser(opts: { includeAuthStatus: false }): Promise<LoggedInUser>;
|
|
103
|
+
function getCurrentUser(opts?: { includeAuthStatus?: boolean }): Promise<Session | LoggedInUser> {
|
|
104
|
+
const includeAuthStatus = opts?.includeAuthStatus ?? true;
|
|
105
|
+
const select = (session: Session) => (includeAuthStatus ? session : (session.user as LoggedInUser));
|
|
106
|
+
|
|
107
|
+
if (!refreshSessionIfStale()) {
|
|
108
|
+
return Promise.resolve(select((sessionStore.getState() as LoadedSessionStore).session));
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
return new
|
|
87
|
-
const
|
|
111
|
+
return new Promise<Session | LoggedInUser>((resolve) => {
|
|
112
|
+
const unsubscribe = sessionStore.subscribe((state) => {
|
|
88
113
|
if (state.loaded) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} else {
|
|
92
|
-
subscriber.next(state.session?.user);
|
|
93
|
-
}
|
|
114
|
+
unsubscribe();
|
|
115
|
+
resolve(select(state.session));
|
|
94
116
|
}
|
|
95
|
-
};
|
|
96
|
-
handler(sessionStore.getState());
|
|
97
|
-
// The observable subscribe function should return an unsubscribe function,
|
|
98
|
-
// which happens to be exactly what `subscribe` returns.
|
|
99
|
-
return sessionStore.subscribe(handler);
|
|
117
|
+
});
|
|
100
118
|
});
|
|
101
119
|
}
|
|
102
120
|
|
|
@@ -121,10 +139,7 @@ export { getCurrentUser };
|
|
|
121
139
|
* ```
|
|
122
140
|
*/
|
|
123
141
|
export function getSessionStore() {
|
|
124
|
-
|
|
125
|
-
refetchCurrentUser();
|
|
126
|
-
}
|
|
127
|
-
|
|
142
|
+
refreshSessionIfStale();
|
|
128
143
|
return sessionStore;
|
|
129
144
|
}
|
|
130
145
|
|
|
@@ -194,10 +209,10 @@ function isSuperUser(user: { roles: Array<Role> }) {
|
|
|
194
209
|
|
|
195
210
|
/**
|
|
196
211
|
* The `refetchCurrentUser` function causes a network request to redownload
|
|
197
|
-
* the user. All subscribers to the
|
|
198
|
-
* new
|
|
212
|
+
* the user. All subscribers to the session store will be notified of the
|
|
213
|
+
* new user once the new version of the user object is downloaded.
|
|
199
214
|
*
|
|
200
|
-
* @returns
|
|
215
|
+
* @returns A Promise resolving to the updated session store state.
|
|
201
216
|
*
|
|
202
217
|
* @example
|
|
203
218
|
* ```js
|
|
@@ -212,11 +227,23 @@ export function refetchCurrentUser(username?: string, password?: string) {
|
|
|
212
227
|
headers['Authorization'] = `Basic ${window.btoa(`${username}:${password}`)}`;
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
|
|
230
|
+
const refresh = handleSessionResponse(
|
|
216
231
|
openmrsFetch(sessionEndpoint, {
|
|
217
232
|
headers,
|
|
218
233
|
}),
|
|
219
234
|
);
|
|
235
|
+
|
|
236
|
+
// Publish the request so that readers can wait on it instead of reading the session it is about to
|
|
237
|
+
// replace. Each call still issues its own request; this only tracks the most recent one.
|
|
238
|
+
const clear = () => {
|
|
239
|
+
if (inFlightRefresh === refresh) {
|
|
240
|
+
inFlightRefresh = null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
refresh.then(clear, clear);
|
|
244
|
+
inFlightRefresh = refresh;
|
|
245
|
+
|
|
246
|
+
return refresh;
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
/**
|
|
@@ -326,13 +353,9 @@ export function getLoggedInUser() {
|
|
|
326
353
|
* }
|
|
327
354
|
* ```
|
|
328
355
|
*/
|
|
329
|
-
export function getSessionLocation() {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
res(session.sessionLocation);
|
|
333
|
-
}, rej);
|
|
334
|
-
sub.unsubscribe();
|
|
335
|
-
});
|
|
356
|
+
export async function getSessionLocation(): Promise<SessionLocation | undefined> {
|
|
357
|
+
const session = await getCurrentUser();
|
|
358
|
+
return session.sessionLocation;
|
|
336
359
|
}
|
|
337
360
|
|
|
338
361
|
/**
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { isObservable } from 'rxjs';
|
|
2
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
2
|
import { getConfig } from '@openmrs/esm-config';
|
|
4
3
|
import { navigate } from '@openmrs/esm-navigation';
|
|
5
|
-
import { openmrsFetch
|
|
4
|
+
import { openmrsFetch } from './openmrs-fetch';
|
|
6
5
|
|
|
7
6
|
vi.mock('@openmrs/esm-navigation', () => ({
|
|
8
7
|
clearHistory: vi.fn(),
|
|
@@ -335,61 +334,3 @@ describe('openmrsFetch', () => {
|
|
|
335
334
|
});
|
|
336
335
|
});
|
|
337
336
|
});
|
|
338
|
-
|
|
339
|
-
describe('openmrsObservableFetch', () => {
|
|
340
|
-
beforeEach(() => {
|
|
341
|
-
window.openmrsBase = '/openmrs';
|
|
342
|
-
window.fetch = vi.fn();
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('calls window.fetch with the correct arguments for a basic GET request', async () => {
|
|
346
|
-
// @ts-ignore
|
|
347
|
-
window.fetch.mockReturnValue(
|
|
348
|
-
Promise.resolve({
|
|
349
|
-
ok: true,
|
|
350
|
-
status: 200,
|
|
351
|
-
headers: {
|
|
352
|
-
has: () => false,
|
|
353
|
-
get: () => null,
|
|
354
|
-
},
|
|
355
|
-
clone: () => ({
|
|
356
|
-
text: () => Promise.resolve('{"value": "hi"}'),
|
|
357
|
-
}),
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
const observable = openmrsObservableFetch('/ws/rest/v1/session');
|
|
362
|
-
expect(isObservable(observable)).toBe(true);
|
|
363
|
-
|
|
364
|
-
await new Promise<void>((resolve, reject) =>
|
|
365
|
-
observable.subscribe(
|
|
366
|
-
(response) => {
|
|
367
|
-
expect(response.data).toEqual({ value: 'hi' });
|
|
368
|
-
resolve();
|
|
369
|
-
},
|
|
370
|
-
(err) => {
|
|
371
|
-
reject(err);
|
|
372
|
-
},
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
expect(window.fetch).toHaveBeenCalled();
|
|
377
|
-
// @ts-expect-error
|
|
378
|
-
expect(window.fetch.mock.calls[0][0]).toEqual('/openmrs/ws/rest/v1/session');
|
|
379
|
-
// @ts-expect-error
|
|
380
|
-
expect(window.fetch.mock.calls[0][1].headers.Accept).toEqual('application/json');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('aborts the fetch request when subscription is unsubscribed', () => {
|
|
384
|
-
// @ts-expect-error
|
|
385
|
-
window.fetch.mockReturnValue(new Promise(() => {}));
|
|
386
|
-
|
|
387
|
-
const subscription = openmrsObservableFetch('/ws/rest/v1/session').subscribe();
|
|
388
|
-
// @ts-expect-error
|
|
389
|
-
const abortSignal: AbortSignal = window.fetch.mock.calls[0][1].signal;
|
|
390
|
-
expect(abortSignal.aborted).toBe(false);
|
|
391
|
-
|
|
392
|
-
subscription.unsubscribe();
|
|
393
|
-
expect(abortSignal.aborted).toBe(true);
|
|
394
|
-
});
|
|
395
|
-
});
|
package/src/openmrs-fetch.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** @module @category API */
|
|
2
|
-
import { Observable } from 'rxjs';
|
|
3
2
|
import { isPlainObject } from 'lodash-es';
|
|
4
3
|
import { getConfig } from '@openmrs/esm-config';
|
|
5
4
|
import { clearHistory, navigate } from '@openmrs/esm-navigation';
|
|
@@ -256,67 +255,6 @@ export function openmrsFetch<T = any>(path: string, fetchInit: FetchConfig = {})
|
|
|
256
255
|
});
|
|
257
256
|
}
|
|
258
257
|
|
|
259
|
-
/**
|
|
260
|
-
* The openmrsObservableFetch function is a wrapper around openmrsFetch
|
|
261
|
-
* that returns an [Observable](https://rxjs-dev.firebaseapp.com/guide/observable)
|
|
262
|
-
* instead of a promise. It exists in case using an Observable is
|
|
263
|
-
* preferred or more convenient than a promise.
|
|
264
|
-
*
|
|
265
|
-
* @param url See [[openmrsFetch]]
|
|
266
|
-
* @param fetchInit See [[openmrsFetch]]
|
|
267
|
-
* @returns An Observable that produces exactly one Response object.
|
|
268
|
-
* The response object is exactly the same as for [[openmrsFetch]].
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
*
|
|
272
|
-
* ```js
|
|
273
|
-
* import { openmrsObservableFetch } from '@openmrs/esm-api'
|
|
274
|
-
* const subscription = openmrsObservableFetch(`${restBaseUrl}/session').subscribe(
|
|
275
|
-
* response => console.log(response.data),
|
|
276
|
-
* err => {throw err},
|
|
277
|
-
* () => console.log('finished')
|
|
278
|
-
* )
|
|
279
|
-
* subscription.unsubscribe()
|
|
280
|
-
* ```
|
|
281
|
-
*
|
|
282
|
-
* #### Cancellation
|
|
283
|
-
*
|
|
284
|
-
* To cancel the network request, simply call `subscription.unsubscribe();`
|
|
285
|
-
*
|
|
286
|
-
* @category API
|
|
287
|
-
*/
|
|
288
|
-
export function openmrsObservableFetch<T>(url: string, fetchInit: FetchConfig = {}) {
|
|
289
|
-
if (typeof fetchInit !== 'object') {
|
|
290
|
-
throw Error('The second argument to openmrsObservableFetch must be either omitted or an object');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const abortController = new AbortController();
|
|
294
|
-
|
|
295
|
-
fetchInit.signal = abortController.signal;
|
|
296
|
-
|
|
297
|
-
return new Observable<FetchResponse<T>>((observer) => {
|
|
298
|
-
let hasResponse = false;
|
|
299
|
-
|
|
300
|
-
openmrsFetch(url, fetchInit).then(
|
|
301
|
-
(response) => {
|
|
302
|
-
hasResponse = true;
|
|
303
|
-
observer.next(response);
|
|
304
|
-
observer.complete();
|
|
305
|
-
},
|
|
306
|
-
(err) => {
|
|
307
|
-
hasResponse = true;
|
|
308
|
-
observer.error(err);
|
|
309
|
-
},
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
return () => {
|
|
313
|
-
if (!hasResponse) {
|
|
314
|
-
abortController.abort();
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
258
|
export class OpenmrsFetchError extends Error implements FetchError {
|
|
321
259
|
constructor(url: string, response: Response, responseBody: ResponseBody | null, requestStacktrace: Error) {
|
|
322
260
|
super();
|