Skip to content

Commit

Permalink
Merge branch 'id-9333-extensions-caching' into 'master'
Browse files Browse the repository at this point in the history
id-9333 Allowed caching of extensions

See merge request id5-sync/id5-api.js!194
  • Loading branch information
abazylewicz-id5 committed Aug 27, 2024
2 parents 616660e + cb9151b commit a852189
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 20 deletions.
28 changes: 14 additions & 14 deletions packages/multiplexing/src/clientStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Module for managing storage of information in browser Local Storage and/or cookies
*/

import {cyrb53Hash, isStr, isNumber} from './utils.js';
import {isStr, isNumber} from './utils.js';

/* eslint-disable no-unused-vars */
import {ConsentData, LocalStorageGrant} from './consent.js';
Expand Down Expand Up @@ -219,19 +219,6 @@ export class ClientStore {
}
}

/**
* creates a hash of a value to be stored
* @param {string} value
* @returns {string} hashed value
*/
static makeStoredHash(value) {
return cyrb53Hash(typeof value === 'string' ? value : '');
}

getDateTime() {
return (new Date(this.get(this.storageConfig.LAST))).getTime();
}

clearDateTime() {
this.clear(this.storageConfig.LAST);
}
Expand Down Expand Up @@ -305,4 +292,17 @@ export class ClientStore {
storedConsentDataMatchesConsentData(consentData) {
return ClientStore.storedDataMatchesCurrentData(this.getHashedConsentData(), consentData.hashCode());
}

getExtensions() {
return this._getObject(this.storageConfig.EXTENSIONS);
}

storeExtensions(extensions, config) {

return this._updateObject(config, () => extensions)
}

clearExtensions() {
return this.clear(this.storageConfig.EXTENSIONS);
}
}
4 changes: 4 additions & 0 deletions packages/multiplexing/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default Object.freeze({
PRIVACY: {
name: 'id5id_privacy',
expiresDays: 30
},
EXTENSIONS: {
name: 'id5id_extensions',
expiresDays: 8/24
}
},
LEGACY_COOKIE_NAMES: [
Expand Down
20 changes: 17 additions & 3 deletions packages/multiplexing/src/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ export class Extensions {
*/
_log;

/**
* @type {Store}
* @private
*/
_store

/**
* @param {Id5CommonMetrics} metrics
* @param {Logger} logger
* @param {Store} store
*/
constructor(metrics, logger) {
constructor(metrics, logger, store) {
this._metrics = metrics;
this._log = logger;
this._store = store;
}

static CHUNKS_CONFIGS = Object.freeze({
Expand Down Expand Up @@ -116,6 +124,10 @@ export class Extensions {
* @returns {Promise<ExtensionsData>} - extensions data
*/
gather(fetchDataList) {
let cachedExtensions = this._store.getCachedExtensions();
if (cachedExtensions !== undefined) {
return Promise.resolve(cachedExtensions);
}
let extensionsCallTimeMeasurement = startTimeMeasurement();
let bouncePromise = this._submitBounce(fetchDataList);
return this.submitExtensionCall(ID5_LB_ENDPOINT, 'lb')
Expand All @@ -135,6 +147,7 @@ export class Extensions {
extensions = {...extensions, ...result.value};
}
});
this._store.storeExtensions(extensions);
return extensions;
}).catch((error) => {
extensionsCallTimeMeasurement.record(this._metrics.extensionsCallTimer('all', false));
Expand Down Expand Up @@ -175,9 +188,10 @@ export const EXTENSIONS = {
/**
* @param {Id5CommonMetrics} metrics
* @param {Logger} log
* @param {Store} store
* @returns {Extensions}
*/
createExtensions: function (metrics, log) {
return new Extensions(metrics, log);
createExtensions: function (metrics, log, store) {
return new Extensions(metrics, log, store);
}
};
2 changes: 1 addition & 1 deletion packages/multiplexing/src/instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ export class Instance {
const consentManagement = new ConsentManagement(localStorage, storageConfig, properties.forceAllowLocalStorageGrant, logger, metrics);
const grantChecker = () => consentManagement.localStorageGrant('client-store');
const store = new Store(new ClientStore(grantChecker, localStorage, storageConfig, logger), this._trueLinkAdapter);
const fetcher = new UidFetcher(metrics, logger, EXTENSIONS.createExtensions(metrics, logger));
const fetcher = new UidFetcher(metrics, logger, EXTENSIONS.createExtensions(metrics, logger, store));

const leader = new ActualLeader(this._window, properties, replicatingStorage, store, consentManagement, metrics, logger, fetcher);
leader.addFollower(this._followerRole); // add itself to be directly called
Expand Down
20 changes: 20 additions & 0 deletions packages/multiplexing/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {isNumber, isPlainObject, isStr} from './utils.js';
import CONSTANTS from './constants.js';

const MAX_RESPONSE_AGE_SEC = 14 * 24 * 3600; // 14 days
const SECONDS_IN_DAY = 24 * 60 * 60;

export class StoreItemConfig {
constructor(name, expiresDays) {
Expand Down Expand Up @@ -31,6 +32,7 @@ export class StorageConfig {
this.LAST = createConfig(defaultStorageConfig.LAST);
this.CONSENT_DATA = createConfig(defaultStorageConfig.CONSENT_DATA);
this.PRIVACY = createConfig(defaultStorageConfig.PRIVACY);
this.EXTENSIONS = new StoreItemConfig(defaultStorageConfig.EXTENSIONS.name, defaultStorageConfig.EXTENSIONS.expiresDays);
}

static DEFAULT = new StorageConfig();
Expand Down Expand Up @@ -132,6 +134,7 @@ export class Store {
});
this._clientStore.clearHashedConsentData();
this._trueLinkAdapter.clearPrivacy();
this._clientStore.clearExtensions();
}

/**
Expand All @@ -145,6 +148,23 @@ export class Store {
}
return undefined;
}

/**
* @return {ExtensionsData}
*/
getCachedExtensions() {
return this._clientStore.getExtensions();
}

/**
* @param {ExtensionsData} extensions
*/
storeExtensions(extensions) {
let expiresDays = isNumber(extensions.ttl) ? extensions.ttl / SECONDS_IN_DAY : StorageConfig.DEFAULT.EXTENSIONS.expiresDays
let config = new StoreItemConfig(StorageConfig.DEFAULT.EXTENSIONS.name, expiresDays)
return this._clientStore.storeExtensions(extensions, config);
}

}

export class CachedResponse {
Expand Down
54 changes: 54 additions & 0 deletions packages/multiplexing/test/spec/clientStore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {ClientStore} from '../../src/clientStore.js';
import {LocalStorage} from '../../src/localStorage.js';
import {StorageConfig, StoreItemConfig} from '../../src/store.js';
import {NO_OP_LOGGER} from '../../src/logger.js';
import Constants from '../../src/constants.js';

const TEST_RESPONSE_ID5_CONSENT = {
universal_uid: 'testresponseid5id',
Expand Down Expand Up @@ -303,5 +304,58 @@ describe('ClientStore', function () {
});
});
});

describe('extensions', function () {

describe('with local storage access granted', function () {
let grantChecker;
/** @type {ClientStore} */
let clientStore;
let extensionsConfig = new StoreItemConfig(Constants.STORAGE_CONFIG.EXTENSIONS.name, Constants.STORAGE_CONFIG.EXTENSIONS.expiresDays)
beforeEach(function () {
grantChecker = () => new LocalStorageGrant(true, GRANT_TYPE.CONSENT_API, API_TYPE.TCF_V2);
clientStore = new ClientStore(grantChecker, localStorage, DEFAULT_STORAGE_CONFIG, log);
});

it('should store extensions', function () {
// given
const ext = {
extA: {
extB: 'C'
}
};

localStorage.updateObjectWithExpiration.callsFake((key, updFn) => {
return updFn({});
});
// when
const result = clientStore.storeExtensions(ext, extensionsConfig);

// then
expect(localStorage.updateObjectWithExpiration).to.be.called;
expect(localStorage.updateObjectWithExpiration.firstCall.args[0]).to.be.eql(extensionsConfig);

expect(result).to.be.eql(ext);
});

it('should get extensions', function () {
// given
const storedExtensions = {
extA: {
extB: 'C'
}
};
localStorage.getObjectWithExpiration.returns(storedExtensions);

// when
const result = clientStore.getExtensions();

// then
expect(localStorage.getObjectWithExpiration).to.be.calledWith(extensionsConfig);
expect(result).to.be.eql(storedExtensions);
});
});
});

});
});
16 changes: 15 additions & 1 deletion packages/multiplexing/test/spec/extensions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sinon from 'sinon';
import {EXTENSIONS, ID5_BOUNCE_ENDPOINT, ID5_LB_ENDPOINT} from '../../src/extensions.js';
import {NO_OP_LOGGER} from '../../src/logger.js';
import {Id5CommonMetrics} from '@id5io/diagnostics';
import {Store} from '../../src/store.js';

const BOUNCE_DEFAULT_RESPONSE = {bounce: {setCookie: false}};

Expand All @@ -25,7 +26,8 @@ describe('Extensions', function () {

const logger = NO_OP_LOGGER; // `= console;` for debug purposes
const metrics = new Id5CommonMetrics('api', '1');
const extensions = EXTENSIONS.createExtensions(metrics, logger);
const store = sinon.createStubInstance(Store);
const extensions = EXTENSIONS.createExtensions(metrics, logger, store);

const lbExtensionsWithChunksFlag = chunksEnabled => {
return {
Expand Down Expand Up @@ -137,5 +139,17 @@ describe('Extensions', function () {
});
});

it('should not call any extensions when there are cached extensions', function () {
const cachedExtensions = {ext: "ext"};
store.getCachedExtensions.returns(cachedExtensions);
fetchStub = createFetchStub();

return extensions.gather([{}])
.then(response => {
expect(fetchStub).to.not.be.called;
expect(response).to.be.deep.equal(cachedExtensions);
});
});


});
14 changes: 14 additions & 0 deletions packages/multiplexing/test/spec/localStorage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ describe('LocalStorage', function () {
expect(storageMock.setItem).to.be.calledOnce;
});

it('expiration policy can use values less than a day', () => {
const testStorage = new LocalStorage(storageMock);

// when
testStorage.setObjectWithExpiration({name: 'test', expiresDays: 2/24}, {});

// then
expect(storageMock.setItem).to.be.calledWith('test', JSON.stringify({
data: {},
expireAt: currentTimeMs + 2 * 3600 * 1000 // 2 hours from the current time
}));
expect(storageMock.setItem).to.be.calledOnce;
});

it('update object with expiration policy', () => {
// given
const testStorage = new LocalStorage(storageMock);
Expand Down
21 changes: 20 additions & 1 deletion packages/multiplexing/test/spec/store.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sinon from 'sinon';
import {Store, StorageConfig, CachedResponse} from '../../src/store.js';
import {CachedResponse, StorageConfig, Store, StoreItemConfig} from '../../src/store.js';
import {ClientStore} from '../../src/clientStore.js';
import {API_TYPE, ConsentData} from '../../src/consent.js';
import CONSTANTS from '../../src/constants.js';
Expand Down Expand Up @@ -234,6 +234,7 @@ describe('Store', function () {
expect(clientStore.clearResponseV2.firstCall.args).to.be.eql([FETCH_ID_DATA[0].cacheId]);
expect(clientStore.clearResponseV2.secondCall.args).to.be.eql([FETCH_ID_DATA[1].cacheId]);
expect(clientStore.clearHashedConsentData).to.have.been.calledOnce;
expect(clientStore.clearExtensions).to.have.been.calledOnce;
expect(trueLinkAdapter.clearPrivacy).to.have.been.calledOnce;
});

Expand Down Expand Up @@ -261,6 +262,24 @@ describe('Store', function () {
expect(clientStore.incNbV2).to.be.calledWith('c1',1);
expect(clientStore.incNbV2).to.be.calledWith('c2',10);
});

it('should take into account the extension ttl', () => {
// when
const extensions = {extA: "A", ttl: 60 * 60};
store.storeExtensions(extensions);

// then
expect(clientStore.storeExtensions).to.be.calledWith(extensions, new StoreItemConfig(CONSTANTS.STORAGE_CONFIG.EXTENSIONS.name, 1/24) );
});

it('should use a default extension ttl if not provided', () => {
// when
const extensions = {extA: "A"};
store.storeExtensions(extensions);

// then
expect(clientStore.storeExtensions).to.be.calledWith(extensions, new StorageConfig().EXTENSIONS );
});
});

describe('Storage config', function () {
Expand Down

0 comments on commit a852189

Please sign in to comment.