@openmrs/esm-api 10.0.1-pre.5542 → 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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 14 files with swc (114.74ms)
1
+ [0] Successfully compiled: 14 files with swc (65.51ms)
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,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 an observable that produces
16
- * **zero or more values, over time**. It will produce zero values
17
- * by default if the user is not logged in. And it will provide a
18
- * first value when the logged in user is fetched from the server.
19
- * Subsequent values will be produced whenever the user object is
20
- * updated.
21
- *
22
- * The function accepts an optional `opts` object with an `includeAuthStatus`
23
- * boolean property that defaults to `true`. When `includeAuthStatus` is `true`,
24
- * the entire {@link Session} object from the API will be provided. When
25
- * `includeAuthStatus` is `false`, only the {@link LoggedInUser} property of the
26
- * response object will be provided.
27
- *
28
- * @returns An Observable that produces zero or more values (as described above).
29
- * The values produced will be a {@link LoggedInUser} object (if `includeAuthStatus`
30
- * is set to `false`) or a {@link Session} object with authentication status
31
- * (if `includeAuthStatus` is set to `true` or not provided).
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 subscription = getCurrentUser().subscribe(
38
- * user => console.log(user)
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(): Observable<Session>;
41
+ declare function getCurrentUser(): Promise<Session>;
53
42
  /**
54
43
  * @param opts Options for controlling the response format.
55
- * @param opts.includeAuthStatus When `true`, returns the full {@link Session} object
44
+ * @param opts.includeAuthStatus When `true`, resolves with the full {@link Session} object
56
45
  * including authentication status.
57
- * @returns An Observable that produces {@link Session} objects.
46
+ * @returns A Promise resolving to a {@link Session} object.
58
47
  */
59
48
  declare function getCurrentUser(opts: {
60
49
  includeAuthStatus: true;
61
- }): Observable<Session>;
50
+ }): Promise<Session>;
62
51
  /**
63
52
  * @param opts Options for controlling the response format.
64
- * @param opts.includeAuthStatus When `false`, returns only the {@link LoggedInUser} object
53
+ * @param opts.includeAuthStatus When `false`, resolves with only the {@link LoggedInUser} object
65
54
  * without the surrounding session information.
66
- * @returns An Observable that produces {@link LoggedInUser} objects.
55
+ * @returns A Promise resolving to a {@link LoggedInUser} object.
67
56
  */
68
57
  declare function getCurrentUser(opts: {
69
58
  includeAuthStatus: false;
70
- }): Observable<LoggedInUser>;
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 current user will be notified of the
106
- * new users once the new version of the user object is downloaded.
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 The same observable as returned by {@link getCurrentUser}.
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":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAElC,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;AAGH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,iBAAS,cAAc,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;AAC/C;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;AAChF;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE;IAAE,iBAAiB,EAAE,KAAK,CAAA;CAAE,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;AAuBtF,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,6CAM9B;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,yBAYtE;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,wBAAgB,kBAAkB,yCAOjC;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"}
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"}
@@ -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
- function getCurrentUser(opts = {
12
- includeAuthStatus: true
13
- }) {
14
- if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) {
15
- refetchCurrentUser();
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
- return new Observable((subscriber)=>{
18
- const handler = (state)=>{
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
- if (opts.includeAuthStatus) {
21
- subscriber.next(state.session);
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
- if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) {
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 current user will be notified of the
113
- * new users once the new version of the user object is downloaded.
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 The same observable as returned by {@link getCurrentUser}.
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
- return handleSessionResponse(openmrsFetch(sessionEndpoint, {
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
- return new Promise((res, rej)=>{
231
- const sub = getCurrentUser().subscribe((session)=>{
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
@@ -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":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAIlC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,GAAE,WAAgB,gCA8BjF;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"}
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"}
@@ -11,8 +11,7 @@ function _define_property(obj, key, value) {
11
11
  }
12
12
  return obj;
13
13
  }
14
- /** @module @category API */ import { Observable } from "rxjs";
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 of({ authenticated: false });
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 of({ authenticated: false });
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.5542",
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.5542",
61
- "@openmrs/esm-error-handling": "^10.0.1-pre.5542",
62
- "@openmrs/esm-globals": "^10.0.1-pre.5542",
63
- "@openmrs/esm-navigation": "^10.0.1-pre.5542"
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.5542",
67
- "@openmrs/esm-error-handling": "10.0.1-pre.5542",
68
- "@openmrs/esm-globals": "10.0.1-pre.5542",
69
- "@openmrs/esm-navigation": "10.0.1-pre.5542",
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"
@@ -1,6 +1,5 @@
1
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
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
- afterEach(() => {
332
- // Clean up any active subscriptions
333
- subscriptions.forEach((sub) => sub.unsubscribe?.());
334
- subscriptions = [];
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 an Observable', () => {
338
- const result = getCurrentUser();
339
- expect(result).toBeInstanceOf(Observable);
343
+ it('should return a Promise', () => {
344
+ expect(getCurrentUser()).toBeInstanceOf(Promise);
340
345
  });
341
346
 
342
- it('should emit user when session is loaded and includeAuthStatus is false', () => {
343
- return new Promise<void>((resolve) => {
344
- const mockUser: LoggedInUser = {
345
- uuid: 'user-uuid',
346
- display: 'Test User',
347
- username: 'testuser',
348
- systemId: 'test-sys-id',
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
- sessionStore.setState({
359
- loaded: true,
360
- session: {
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
- const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
368
- expect(user).toEqual(mockUser);
369
- resolve();
370
- });
371
- subscriptions.push(sub);
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 emit full session when includeAuthStatus is true', () => {
376
- return new Promise<void>((resolve) => {
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
- sessionStore.setState({
398
- loaded: true,
399
- session: mockSession,
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
- const sub = getCurrentUser({ includeAuthStatus: true }).subscribe((session) => {
403
- expect(session).toEqual(mockSession);
404
- resolve();
405
- });
406
- subscriptions.push(sub);
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 not emit when session is not loaded', () => {
411
- const handler = vi.fn();
412
- const sub = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
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
- sessionStore.setState({
451
- loaded: true,
452
- session: {
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
- const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
460
- emittedUsers.push(user);
461
- if (emittedUsers.length === 2) {
462
- expect(emittedUsers[0]).toEqual(mockUser1);
463
- expect(emittedUsers[1]).toEqual(mockUser2);
464
- resolve();
465
- }
466
- });
467
- subscriptions.push(sub);
468
-
469
- setTimeout(() => {
470
- sessionStore.setState({
471
- loaded: true,
472
- session: {
473
- authenticated: true,
474
- sessionId: 'session-2',
475
- user: mockUser2,
476
- },
477
- });
478
- }, 10);
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 allow unsubscribing', () => {
483
- const handler = vi.fn();
484
- const subscription = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
485
- subscriptions.push(subscription);
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
- sessionStore.setState({
488
- loaded: true,
489
- session: {
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
- handler.mockClear();
497
- subscription.unsubscribe();
460
+ await Promise.resolve();
461
+ expect(resolvedUser).toBeUndefined();
498
462
 
499
463
  sessionStore.setState({
500
464
  loaded: true,
501
465
  session: {
502
- authenticated: false,
503
- sessionId: 'new-session',
466
+ authenticated: true,
467
+ sessionId: 'test-session',
468
+ user: mockUser,
504
469
  },
505
470
  });
506
471
 
507
- expect(handler).not.toHaveBeenCalled();
472
+ await promise;
473
+ expect(resolvedUser).toEqual(mockUser);
508
474
  });
509
475
  });
510
476
 
@@ -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 an observable that produces
30
- * **zero or more values, over time**. It will produce zero values
31
- * by default if the user is not logged in. And it will provide a
32
- * first value when the logged in user is fetched from the server.
33
- * Subsequent values will be produced whenever the user object is
34
- * updated.
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
- * The function accepts an optional `opts` object with an `includeAuthStatus`
37
- * boolean property that defaults to `true`. When `includeAuthStatus` is `true`,
38
- * the entire {@link Session} object from the API will be provided. When
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 An Observable that produces zero or more values (as described above).
43
- * The values produced will be a {@link LoggedInUser} object (if `includeAuthStatus`
44
- * is set to `false`) or a {@link Session} object with authentication status
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 subscription = getCurrentUser().subscribe(
52
- * user => console.log(user)
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(): Observable<Session>;
88
+ function getCurrentUser(): Promise<Session>;
67
89
  /**
68
90
  * @param opts Options for controlling the response format.
69
- * @param opts.includeAuthStatus When `true`, returns the full {@link Session} object
91
+ * @param opts.includeAuthStatus When `true`, resolves with the full {@link Session} object
70
92
  * including authentication status.
71
- * @returns An Observable that produces {@link Session} objects.
93
+ * @returns A Promise resolving to a {@link Session} object.
72
94
  */
73
- function getCurrentUser(opts: { includeAuthStatus: true }): Observable<Session>;
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`, returns only the {@link LoggedInUser} object
98
+ * @param opts.includeAuthStatus When `false`, resolves with only the {@link LoggedInUser} object
77
99
  * without the surrounding session information.
78
- * @returns An Observable that produces {@link LoggedInUser} objects.
100
+ * @returns A Promise resolving to a {@link LoggedInUser} object.
79
101
  */
80
- function getCurrentUser(opts: { includeAuthStatus: false }): Observable<LoggedInUser>;
81
- function getCurrentUser(opts = { includeAuthStatus: true }): Observable<Session | LoggedInUser> {
82
- if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) {
83
- refetchCurrentUser();
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 Observable((subscriber) => {
87
- const handler = (state: SessionStore) => {
111
+ return new Promise<Session | LoggedInUser>((resolve) => {
112
+ const unsubscribe = sessionStore.subscribe((state) => {
88
113
  if (state.loaded) {
89
- if (opts.includeAuthStatus) {
90
- subscriber.next(state.session);
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
- if (lastFetchTimeMillis < Date.now() - 1000 * 60 || !sessionStore.getState().loaded) {
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 current user will be notified of the
198
- * new users once the new version of the user object is downloaded.
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 The same observable as returned by {@link getCurrentUser}.
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
- return handleSessionResponse(
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
- return new Promise<SessionLocation | undefined>((res, rej) => {
331
- const sub = getCurrentUser().subscribe((session) => {
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, openmrsObservableFetch } from './openmrs-fetch';
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
- });
@@ -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();