Skip to content

Commit

Permalink
feat(server-renderer): replace register & rerender with rerenderAfter (
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable authored and clebert committed Jan 9, 2019
1 parent b33f744 commit 5fbfcc6
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 218 deletions.
3 changes: 3 additions & 0 deletions packages/server-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"dependencies": {
"@feature-hub/core": "^0.11.0"
},
"devDependencies": {
"jest-mock-console": "^0.4.2"
},
"publishConfig": {
"access": "public"
}
Expand Down
195 changes: 56 additions & 139 deletions packages/server-renderer/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
// tslint:disable:no-implicit-dependencies

import {
FeatureAppEnvironment,
FeatureServiceBinder,
FeatureServiceProviderDefinition
} from '@feature-hub/core';
import mockConsole from 'jest-mock-console';
import {ServerRendererV1, ServerRequest, defineServerRenderer} from '..';
import {ServerRendererConfig} from '../config';
import {useFakeTimers} from './use-fake-timers';

describe('defineServerRenderer', () => {
let mockEnv: FeatureAppEnvironment<undefined, {}>;
let mockEnv: FeatureAppEnvironment<ServerRendererConfig, {}>;
let serverRendererDefinition: FeatureServiceProviderDefinition;
let serverRequest: ServerRequest;

beforeEach(() => {
jest.useFakeTimers();

mockEnv = {config: undefined, featureServices: {}, idSpecifier: undefined};
mockEnv = {
config: {timeout: 5},
featureServices: {},
idSpecifier: undefined
};

serverRequest = {
path: '/app',
Expand All @@ -25,10 +31,6 @@ describe('defineServerRenderer', () => {
serverRendererDefinition = defineServerRenderer(serverRequest);
});

afterEach(() => {
jest.useRealTimers();
});

it('creates a server renderer definition', () => {
expect(serverRendererDefinition.id).toBe('s2:server-renderer');
expect(serverRendererDefinition.dependencies).toBeUndefined();
Expand All @@ -41,7 +43,7 @@ describe('defineServerRenderer', () => {
expect(sharedServerRenderer['1.0']).toBeDefined();
});

for (const invalidConfig of [null, {rerenderWait: false}]) {
for (const invalidConfig of [null, {timeout: false}]) {
describe(`with an invalid config ${JSON.stringify(
invalidConfig
)}`, () => {
Expand Down Expand Up @@ -73,25 +75,15 @@ describe('defineServerRenderer', () => {
});

describe('rendering', () => {
const createServerRendererConsumer = (
consumerUid: string,
rerenderWait = 0
) => {
const createServerRendererConsumer = (consumerUid: string) => {
const serverRenderer = serverRendererBinder(consumerUid).featureService;

let firstRender = true;
let completed = false;

const render = () => {
if (firstRender) {
firstRender = false;
serverRenderer.register(() => completed);

setTimeout(async () => {
completed = true;

await serverRenderer.rerender();
}, rerenderWait);
serverRenderer.rerenderAfter(Promise.resolve());
}
};

Expand All @@ -109,31 +101,7 @@ describe('defineServerRenderer', () => {
});
});

describe('with an integrator, and a consumer that is completed in the first render pass', () => {
it('resolves with an html string after the first render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer = serverRendererBinder('test:consumer')
.featureService;

const mockRender = jest.fn(() => {
serverRendererConsumer.register(() => true);

return 'testHtml';
});

const html = await useFakeTimers(async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender)
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(1);
});
});

describe('with an integrator, and a consumer that is completed after triggering a rerender', () => {
describe('with an integrator, and a consumer that triggers a rerender', () => {
it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
Expand All @@ -149,29 +117,27 @@ describe('defineServerRenderer', () => {
return 'testHtml';
});

const html = await useFakeTimers(async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender)
const html = await serverRendererIntegrator.renderUntilCompleted(
mockRender
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});

describe('with an integrator, and two consumers that are completed after both triggered a rerender within "rerenderWait" milliseconds', () => {
describe('with an integrator, and two consumers that both trigger a rerender in the first render pass', () => {
it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
'test:consumer:1'
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
100
'test:consumer:2'
);

const mockRender = jest.fn(() => {
Expand All @@ -181,94 +147,15 @@ describe('defineServerRenderer', () => {
return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
150
const html = await serverRendererIntegrator.renderUntilCompleted(
mockRender
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});

describe('with an integrator, and two consumers that are completed after triggering a rerender more than "rerenderWait" milliseconds apart from one another', () => {
describe('and the default "rerenderWait"', () => {
it('resolves with an html string after the third render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
101
);

const mockRender = jest.fn(() => {
serverRendererConsumer1.render();
serverRendererConsumer2.render();

return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
151
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(3);
});
});

describe('and a custom, higher rerenderWait', () => {
beforeEach(() => {
serverRendererBinder = serverRendererDefinition.create({
config: {rerenderWait: 51},
featureServices: {}
})['1.0'] as FeatureServiceBinder<ServerRendererV1>;
});

it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
101
);

const mockRender = jest.fn(() => {
serverRendererConsumer1.render();
serverRendererConsumer2.render();

return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
152
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});
});

describe('when the given render function throws an error', () => {
it('rejects with the error', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
Expand All @@ -284,16 +171,46 @@ describe('defineServerRenderer', () => {
});
});

describe('when renderUntilCompleted is called multiple times', () => {
it('rejects with an error', async () => {
describe('when rendering takes longer than the configured timeout', () => {
it('rejects with an error after the configured timeout', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
const mockRender = jest.fn(() => 'testHtml');
const mockRender = jest.fn(() => {
serverRenderer.rerenderAfter(new Promise<void>(() => undefined));

await serverRenderer.renderUntilCompleted(mockRender);
return 'testHtml';
});

return expect(
useFakeTimers(
async () => serverRenderer.renderUntilCompleted(mockRender),
5
)
).rejects.toEqual(new Error('Got rendering timeout after 5 ms.'));
});
});

describe('when no timeout is configured', () => {
beforeEach(() => {
serverRendererBinder = serverRendererDefinition.create({
config: undefined,
featureServices: {}
})['1.0'] as FeatureServiceBinder<ServerRendererV1>;
});

it('logs a warning', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
const mockRender = jest.fn(() => 'testHtml');
const restoreConsole = mockConsole();

await useFakeTimers(async () =>
serverRenderer.renderUntilCompleted(mockRender)
).rejects.toEqual(new Error('Rendering has already been started.'));
);

expect(console.warn).toHaveBeenCalledWith(
'No timeout is configured for the server renderer. This could lead to unexpectedly long render times or, in the worst case, never resolving render calls!'
);

restoreConsole();
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/server-renderer/src/__tests__/use-fake-timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function useFakeTimers<TResult>(
actualTimeoutInMilliseconds += 1;
}

if (expectedTimeoutInMilliseconds !== undefined) {
if (typeof expectedTimeoutInMilliseconds === 'number') {
expect(actualTimeoutInMilliseconds).toBe(expectedTimeoutInMilliseconds);
}

Expand Down
8 changes: 3 additions & 5 deletions packages/server-renderer/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface ServerRendererConfig {
rerenderWait: number;
timeout?: number;
}

function isValidConfig(
Expand All @@ -13,11 +13,9 @@ function isValidConfig(
return false;
}

const {rerenderWait} = config as ServerRendererConfig;
const {timeout} = config as ServerRendererConfig;

return (
typeof rerenderWait === 'undefined' || typeof rerenderWait === 'number'
);
return typeof timeout === 'undefined' || typeof timeout === 'number';
}

export function validateConfig(
Expand Down
8 changes: 5 additions & 3 deletions packages/server-renderer/src/define-server-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
FeatureServiceProviderDefinition,
SharedFeatureService
} from '@feature-hub/core';
import {validateConfig} from './config';
import {ServerRendererConfig, validateConfig} from './config';
import {
ServerRenderer,
ServerRendererV1,
Expand All @@ -21,8 +21,10 @@ export function defineServerRenderer(
id: 's2:server-renderer',

create: (env): SharedServerRenderer => {
const {rerenderWait = 50} = validateConfig(env.config) || {};
const serverRenderer = new ServerRenderer(serverRequest, rerenderWait);
const {timeout} =
validateConfig(env.config) || ({} as ServerRendererConfig);

const serverRenderer = new ServerRenderer(serverRequest, timeout);

return {
'1.0': () => ({featureService: serverRenderer})
Expand Down
25 changes: 0 additions & 25 deletions packages/server-renderer/src/internal/debounce-async.ts

This file was deleted.

5 changes: 5 additions & 0 deletions packages/server-renderer/src/internal/set-timeout-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function setTimeoutAsync(duration: number): Promise<void> {
return new Promise<void>((resolve: () => void) =>
setTimeout(resolve, duration)
);
}
Loading

0 comments on commit 5fbfcc6

Please sign in to comment.