Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(client): correct loading behavior, add MedplumClient lifecycle events #4701

Merged
merged 4 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { encodeBase64 } from './base64';
import { LRUCache } from './cache';
import { ContentType } from './contenttype';
import { encryptSHA256, getRandomString } from './crypto';
import { EventTarget } from './eventtarget';
import { TypedEventTarget } from './eventtarget';
import {
CurrentContext,
FhircastConnection,
Expand Down Expand Up @@ -682,6 +682,17 @@ export interface RequestProfileSchemaOptions {
expandProfile?: boolean;
}

/**
* This map enumerates all the lifecycle events that `MedplumClient` emits and what the shape of the `Event` is.
*/
export type MedplumClientEventMap = {
change: { type: 'change' };
offline: { type: 'offline' };
profileRefreshing: { type: 'profileRefreshing' };
profileRefreshed: { type: 'profileRefreshed' };
storageInitialized: { type: 'storageInitialized' };
};

/**
* The MedplumClient class provides a client for the Medplum FHIR server.
*
Expand Down Expand Up @@ -739,7 +750,7 @@ export interface RequestProfileSchemaOptions {
* <meta name="algolia:pageRank" content="100" />
* </head>
*/
export class MedplumClient extends EventTarget {
export class MedplumClient extends TypedEventTarget<MedplumClientEventMap> {
private readonly options: MedplumClientOptions;
private readonly fetch: FetchLike;
private readonly createPdfImpl?: CreatePdfFunction;
Expand Down Expand Up @@ -817,6 +828,7 @@ export class MedplumClient extends EventTarget {
this.attemptResumeActiveLogin().catch(console.error);
}
this.initPromise = Promise.resolve();
this.dispatchEvent({ type: 'storageInitialized' });
} else {
this.initComplete = false;
this.initPromise = this.storage.getInitPromise();
Expand All @@ -826,6 +838,7 @@ export class MedplumClient extends EventTarget {
this.attemptResumeActiveLogin().catch(console.error);
}
this.initComplete = true;
this.dispatchEvent({ type: 'storageInitialized' });
})
.catch(console.error);
}
Expand Down Expand Up @@ -2713,6 +2726,7 @@ export class MedplumClient extends EventTarget {
return Promise.resolve(undefined);
}
this.profilePromise = new Promise((resolve, reject) => {
this.dispatchEvent({ type: 'profileRefreshing' });
this.get('auth/me')
.then((result: SessionDetails) => {
this.profilePromise = undefined;
Expand All @@ -2721,6 +2735,7 @@ export class MedplumClient extends EventTarget {
if (profileChanged) {
this.dispatchEvent({ type: 'change' });
}
this.dispatchEvent({ type: 'profileRefreshed' });
resolve(result.profile);
})
.catch(reject);
Expand Down
63 changes: 62 additions & 1 deletion packages/mock/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ClientStorage,
ContentType,
LoginState,
MemoryStorage,
MockAsyncClientStorage,
NewPatientRequest,
NewProjectRequest,
Expand All @@ -12,8 +13,10 @@ import {
getReferenceString,
indexSearchParameterBundle,
indexStructureDefinitionBundle,
sleep,
} from '@medplum/core';
import { readJson } from '@medplum/definitions';
import { FhirRouter, MemoryRepository } from '@medplum/fhir-router';
import {
Agent,
Bot,
Expand All @@ -26,7 +29,7 @@ import {
} from '@medplum/fhirtypes';
import { randomUUID, webcrypto } from 'node:crypto';
import { TextEncoder } from 'node:util';
import { MockClient } from './client';
import { MockClient, MockFetchClient } from './client';
import { DrAliceSmith, DrAliceSmithSchedule, HomerSimpson } from './mocks';
import { MockSubscriptionManager } from './subscription-manager';

Expand Down Expand Up @@ -268,6 +271,64 @@ describe('MockClient', () => {
expect(console.log).toHaveBeenCalled();
});

test('mockFetchOverride -- Missing one of router, repo, or client throws', () => {
const router = new FhirRouter();
const repo = new MemoryRepository();
const client = new MockFetchClient(router, repo, 'https://example.com/');
expect(
() =>
new MockClient({
// @ts-expect-error Missing router
mockFetchOverride: { repo, client },
})
).toThrow('mockFetchOverride must specify all fields: client, repo, router');
expect(
() =>
new MockClient({
// @ts-expect-error Missing repo
mockFetchOverride: { repo, client },
})
).toThrow('mockFetchOverride must specify all fields: client, repo, router');
expect(
() =>
new MockClient({
// @ts-expect-error Missing client
mockFetchOverride: { router, repo },
})
).toThrow('mockFetchOverride must specify all fields: client, repo, router');
});

test('mockFetchOverride -- Can spy on passed-in fetch', async () => {
const baseUrl = 'https://example.com/';

const router = new FhirRouter();
const repo = new MemoryRepository();
const client = new MockFetchClient(router, repo, baseUrl);
const fetchClientSpy = jest.spyOn(client, 'mockFetch');

const storage = new ClientStorage(new MemoryStorage());
storage.setObject('activeLogin', {
accessToken:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJsb2dpbl9pZCI6InRlc3RpbmcxMjMifQ.lJGCbp2taTarRbamxaKFsTR_VRVgzvttKMmI5uFQSM0',
refreshToken: '456',
profile: {
reference: 'Practitioner/123',
},
project: {
reference: 'Project/123',
},
});

const mockClient = new MockClient({
storage,
mockFetchOverride: { router, repo, client },
});

await sleep(0);
expect(mockClient).toBeDefined();
expect(fetchClientSpy).toHaveBeenCalledWith(`${baseUrl}auth/me`, expect.objectContaining({ method: 'GET' }));
});

test('Search', async () => {
const client = new MockClient();
const result = await client.search('Patient', 'name=Simpson');
Expand Down
42 changes: 36 additions & 6 deletions packages/mock/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
BinarySource,
ContentType,
CreateBinaryOptions,
IClientStorage,
LoginState,
MedplumClient,
MedplumClientOptions,
Expand Down Expand Up @@ -93,9 +92,21 @@ export interface MockClientOptions extends MedplumClientOptions {
* MedplumContext.profile returning undefined as if no one were logged in.
*/
readonly profile?: ReturnType<MedplumClient['getProfile']> | null;
readonly storage?: IClientStorage;
/**
* Override the `MockFetchClient` used by this `MockClient`.
*/
readonly mockFetchOverride?: MockFetchOverrideOptions;
}

/**
* Override must contain all of `router`, `repo`, and `client`.
*/
export type MockFetchOverrideOptions = {
client: MockFetchClient;
router: FhirRouter;
repo: MemoryRepository;
};

export class MockClient extends MedplumClient {
readonly router: FhirRouter;
readonly repo: MemoryRepository;
Expand All @@ -107,11 +118,30 @@ export class MockClient extends MedplumClient {
subManager: MockSubscriptionManager | undefined;

constructor(clientOptions?: MockClientOptions) {
const router = new FhirRouter();
const repo = new MemoryRepository();

const baseUrl = clientOptions?.baseUrl ?? 'https://example.com/';
const client = new MockFetchClient(router, repo, baseUrl, clientOptions?.debug);

let router: FhirRouter;
let repo: MemoryRepository;
let client: MockFetchClient;

if (clientOptions?.mockFetchOverride) {
if (
!(
clientOptions.mockFetchOverride?.router &&
clientOptions.mockFetchOverride?.repo &&
clientOptions.mockFetchOverride?.client
)
) {
throw new Error('mockFetchOverride must specify all fields: client, repo, router');
}
router = clientOptions.mockFetchOverride.router;
repo = clientOptions.mockFetchOverride.repo;
client = clientOptions.mockFetchOverride.client;
} else {
router = new FhirRouter();
repo = new MemoryRepository();
client = new MockFetchClient(router, repo, baseUrl, clientOptions?.debug);
}

super({
baseUrl,
Expand Down
Loading
Loading