Skip to content
Open
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
51 changes: 48 additions & 3 deletions packages/oc/src/registry/domain/components-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import pLimit from '../../utils/pLimit';
import eventsHandler from './events-handler';

export default function componentsDetails(conf: Config, cdn: StorageAdapter) {
let cachedComponentsDetails: ComponentsDetails | undefined;
let refreshLoop: NodeJS.Timeout;

const returnError = (code: string, message: string | Error) => {
eventsHandler.fire('error', {
code,
Expand All @@ -26,6 +29,36 @@ export default function componentsDetails(conf: Config, cdn: StorageAdapter) {
const getFromJson = (): Promise<ComponentsDetails> =>
cdn.getJson(filePath(), true);

const poll = () => {
return setTimeout(async () => {
try {
const data = await getFromJson();

eventsHandler.fire('cache-poll', getUnixUTCTimestamp());

if (
!cachedComponentsDetails ||
data.lastEdit > cachedComponentsDetails.lastEdit
) {
cachedComponentsDetails = data;
}
} catch (err: any) {
eventsHandler.fire('error', {
code: 'components_details_get',
message: err?.message || String(err)
});
}
refreshLoop = poll();
}, conf.pollingInterval * 1000);
};

const cacheDataAndStartPolling = (data: ComponentsDetails) => {
cachedComponentsDetails = data;
refreshLoop = poll();

return data;
};

const getFromDirectories = async (options: {
componentsList: ComponentsList;
details?: ComponentsDetails;
Expand Down Expand Up @@ -78,9 +111,21 @@ export default function componentsDetails(conf: Config, cdn: StorageAdapter) {
const save = (data: ComponentsDetails): Promise<unknown> =>
cdn.putFileContent(JSON.stringify(data), filePath(), true);

const get = async (): Promise<ComponentsDetails> => {
if (cachedComponentsDetails) {
return cachedComponentsDetails;
}

cachedComponentsDetails = await getFromJson();

return cachedComponentsDetails;
};

const refresh = async (
componentsList: ComponentsList
): Promise<ComponentsDetails> => {
clearTimeout(refreshLoop);

const jsonDetails = await getFromJson().catch(() => undefined);
const dirDetails = await getFromDirectories({
componentsList,
Expand All @@ -94,14 +139,14 @@ export default function componentsDetails(conf: Config, cdn: StorageAdapter) {
await save(dirDetails).catch((err) =>
returnError('components_details_save', err)
);
return dirDetails;
return cacheDataAndStartPolling(dirDetails);
}

return jsonDetails;
return cacheDataAndStartPolling(jsonDetails);
};

return {
get: getFromJson,
get,
refresh
};
}
139 changes: 139 additions & 0 deletions packages/oc/test/unit/registry-domain-components-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ const sinon = require('sinon');

describe('registry : domain : components-details', () => {
const fireStub = sinon.stub();
const setTimeoutStub = sinon.stub();
const clearTimeoutStub = sinon.stub();
const ComponentsDetails = injectr(
'../../dist/registry/domain/components-details.js',
{
'./events-handler': { fire: fireStub },
'oc-get-unix-utc-timestamp': () => 1234567890
},
{
setTimeout: setTimeoutStub,
clearTimeout: clearTimeoutStub
}
).default;

const conf = {
pollingInterval: 5,
storage: {
options: {
componentsDir: 'components'
Expand Down Expand Up @@ -66,6 +73,16 @@ describe('registry : domain : components-details', () => {
'components/components-details.json'
);
});

it('should cache the details after the first fetch', async () => {
const cacheStubs = { getJson: sinon.stub().resolves(details) };
const componentsDetails = ComponentsDetails(conf, cacheStubs);

await componentsDetails.get();
await componentsDetails.get();

expect(cacheStubs.getJson.calledOnce).to.be.true;
});
});

describe('when details file does not exist on cdn', () => {
Expand Down Expand Up @@ -247,6 +264,34 @@ describe('registry : domain : components-details', () => {
}
});
});

it('should cache the refreshed details', async () => {
const cacheStubs = { getJson: sinon.stub() };
cacheStubs.getJson.onCall(0).resolves(details);
cacheStubs.getJson.onCall(1).resolves({
oc: {
date: 1459864868001,
files: { template: { size: 300 } }
}
});
cacheStubs.maxConcurrentRequests = 20;
cacheStubs.putFileContent = sinon.stub().resolves('ok');
const componentsDetails = ComponentsDetails(conf, cacheStubs);

await componentsDetails.refresh(list);
const cachedResult = await componentsDetails.get();

expect(cachedResult).to.eql({
lastEdit: 1234567890,
components: {
hello: {
'1.0.0': { publishDate: 1459864868000 },
'1.0.1': { publishDate: 1459864868001, templateSize: 300 }
}
}
});
expect(cacheStubs.getJson.callCount).to.equal(2);
});
});
});
});
Expand Down Expand Up @@ -478,4 +523,98 @@ describe('registry : domain : components-details', () => {
});
});
});

describe('polling', () => {
const list = {
lastEdit: 1459864868001,
components: {
hello: ['1.0.0', '1.0.1']
}
};

const details = {
lastEdit: 1459864868001,
components: {
hello: {
'1.0.0': { publishDate: 1459864868000 },
'1.0.1': { publishDate: 1459864868001 }
}
}
};

const newerDetails = {
lastEdit: 1459864868999,
components: {
hello: {
'1.0.0': { publishDate: 1459864868000 },
'1.0.1': { publishDate: 1459864868001 },
'2.0.0': { publishDate: 1459864868999 }
}
}
};

let stubs;
let componentsDetails;

beforeEach(async () => {
setTimeoutStub.reset();
clearTimeoutStub.reset();
fireStub.reset();
stubs = { getJson: sinon.stub().resolves(details) };
stubs.maxConcurrentRequests = 20;
stubs.putFileContent = sinon.stub().resolves('ok');
componentsDetails = ComponentsDetails(conf, stubs);
await componentsDetails.refresh(list);
});

it('should start the polling loop using the configured interval', () => {
expect(setTimeoutStub.called).to.be.true;
expect(setTimeoutStub.args[0][1]).to.equal(5000);
});

it('should clear the previous loop when refreshing again', async () => {
await componentsDetails.refresh(list);
expect(clearTimeoutStub.called).to.be.true;
});

it('should restart the loop after each poll', async () => {
const poll = setTimeoutStub.args[0][0];
await poll();
expect(setTimeoutStub.calledTwice).to.be.true;
});

it('should update the cache when the polled data is newer', async () => {
const poll = setTimeoutStub.args[0][0];
stubs.getJson.resolves(newerDetails);

await poll();

expect(await componentsDetails.get()).to.eql(newerDetails);
});

it('should keep the cached data when the polled data is not newer', async () => {
const poll = setTimeoutStub.args[0][0];
stubs.getJson.resolves({ ...details, lastEdit: details.lastEdit - 1 });

await poll();

expect(await componentsDetails.get()).to.eql(details);
});

it('should fire an error event when polling fails', async () => {
const poll = setTimeoutStub.args[0][0];
fireStub.reset();
stubs.getJson.rejects(new Error('poll failed'));

await poll();

const errorCall = fireStub
.getCalls()
.find((call) => call.args[0] === 'error');
expect(errorCall.args[1]).to.eql({
code: 'components_details_get',
message: 'poll failed'
});
});
});
});
Loading