Feat/lazy loaded singleton (#2570)
* feat(singleton-manager): lazifyInstation functionality * Update getLocalizeManager to register itself lazily Co-authored by: Thijs Louisse <Thijs.Louisse@ing.com> --------- Co-authored-by: Byoungyoung Kim <Byoungyoung.Kim@ing.com>
This commit is contained in:
parent
02d0106a06
commit
6e0ed97423
12 changed files with 158 additions and 35 deletions
5
.changeset/poor-penguins-wait.md
Normal file
5
.changeset/poor-penguins-wait.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make the manager singletons get instantiated and registered lazily.
|
||||||
5
.changeset/seven-hotels-vanish.md
Normal file
5
.changeset/seven-hotels-vanish.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'singleton-manager': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `lazifyInstantiation` method to singleton-manager. It will help create side-effect-free app setups, avoiding hosisting problems during bundling conflicts and/or long-winded, multi-file setup logic.
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { SingletonManagerClass } from './SingletonManagerClass.js';
|
import { SingletonManagerClass } from './SingletonManagerClass.js';
|
||||||
|
|
||||||
|
export { lazifyInstantiation } from './lazifyInstantiation.js';
|
||||||
|
|
||||||
export { SingletonManagerClass };
|
export { SingletonManagerClass };
|
||||||
export const singletonManager = new SingletonManagerClass();
|
export const singletonManager = new SingletonManagerClass();
|
||||||
|
|
|
||||||
57
packages/singleton-manager/src/lazifyInstantiation.js
Normal file
57
packages/singleton-manager/src/lazifyInstantiation.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
// TODO: testing is done now indirectly via @lion/ui tests. Aditionally, add dedicated tests for this package
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazifies the instantiation of singletons. It does so by wrapping the singleton in a Proxy, which delays
|
||||||
|
* instantiation until one of its members gets called/set for the first time.
|
||||||
|
* This is important in cases where a singleton needs to be overridden on an app level:
|
||||||
|
* let's say there are two versions of @lion/ui in an app that need to synchronize their locale.
|
||||||
|
* We want to make sure that the latest LocalizeManager instance (localize) of the latest lion version (that has the latest features and is backwards compatible with earlier versions) is registered before others.
|
||||||
|
* We see often that this complicates setup/index files (the need of extra files to make sure logic gets executed before side-effectful imports. Also, build processes like Rollup tend to merge files and therefore hoist imports and change execution order.
|
||||||
|
* This method aims to solve above problems.
|
||||||
|
* @type {<T>(func: () => T) => T}
|
||||||
|
*/
|
||||||
|
const lazifyInstantiation = registerSingleton => {
|
||||||
|
let /** @type any */ singletonInstance = null;
|
||||||
|
|
||||||
|
const getSingletonInstance = () => {
|
||||||
|
if (singletonInstance === null) {
|
||||||
|
singletonInstance = registerSingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singletonInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {any} */ const proxy = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_target, prop) {
|
||||||
|
const instance = getSingletonInstance();
|
||||||
|
// Somehow addEventListener and removeEventListner throws Illegal Invocation error without binding
|
||||||
|
if (prop === 'addEventListener' || prop === 'removeEventListener') {
|
||||||
|
return Reflect.get(instance, prop).bind(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === '__instance_for_testing') {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(instance, prop, instance);
|
||||||
|
},
|
||||||
|
set(_target, prop, value) {
|
||||||
|
return Reflect.set(getSingletonInstance(), prop, value);
|
||||||
|
},
|
||||||
|
// Used for sinon.spy
|
||||||
|
getOwnPropertyDescriptor(_target, prop) {
|
||||||
|
return Reflect.getOwnPropertyDescriptor(getSingletonInstance(), prop);
|
||||||
|
},
|
||||||
|
// Used for sinon.spy
|
||||||
|
getPrototypeOf() {
|
||||||
|
return Reflect.getPrototypeOf(getSingletonInstance());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { lazifyInstantiation };
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
import { singletonManager } from 'singleton-manager';
|
import { singletonManager, lazifyInstantiation } from 'singleton-manager';
|
||||||
import { IconManager } from './IconManager.js';
|
import { IconManager } from './IconManager.js';
|
||||||
|
|
||||||
export const icons = singletonManager.get('@lion/ui::icons::0.x') || new IconManager();
|
/**
|
||||||
|
* @returns {IconManager}
|
||||||
|
*/
|
||||||
|
function getIconManager() {
|
||||||
|
if (!singletonManager.has('@lion/ui::icons::0.x')) {
|
||||||
|
const iconManager = new IconManager();
|
||||||
|
singletonManager.set('@lion/ui::icons::0.x', iconManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return singletonManager.get('@lion/ui::icons::0.x');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const icons = lazifyInstantiation(getIconManager);
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
export default /** @param {(strings: TemplateStringsArray, ... expr: string[]) => string} tag */ tag =>
|
// @ts-nocheck
|
||||||
tag`<svg focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" data-test-id="svg-hammer"><path d="M.476 12.915L6.7 6 9 8.556l-6.223 6.915a1.542 1.542 0 0 1-1.15.529 1.54 1.54 0 0 1-1.15-.53c-.636-.706-.636-1.85 0-2.555zm12.638-9.031L16 6.863 12.866 10 4 .919 9.35 0l1.912 1.972.251-.251c.52-.52 2.4 1.363 1.88 1.882l-.279.28z"></path></svg>`;
|
export default /** @type {import("lit").TemplateResult} */ (
|
||||||
|
tag =>
|
||||||
|
tag`<svg focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" data-test-id="svg-hammer"><path d="M.476 12.915L6.7 6 9 8.556l-6.223 6.915a1.542 1.542 0 0 1-1.15.529 1.54 1.54 0 0 1-1.15-.53c-.636-.706-.636-1.85 0-2.555zm12.638-9.031L16 6.863 12.866 10 4 .919 9.35 0l1.912 1.972.251-.251c.52-.52 2.4 1.363 1.88 1.882l-.279.28z"></path></svg>`
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
export default /** @param {(strings: TemplateStringsArray, ... expr: string[]) => string} tag */ tag =>
|
// @ts-nocheck
|
||||||
tag`<svg focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" data-test-id="svg-heart"><path d="M8.002 14.867a.828.828 0 0 1-.324-.066C7.36 14.665 0 11.466 0 6.153 0 3.592 1.543.825 4.98 1.01c1.465.077 2.437.828 3.018 1.491.581-.667 1.553-1.414 3.023-1.491.107-.008.207-.008.31-.008C13.58 1.001 16 2.614 16 6.153c0 5.313-7.36 8.512-7.671 8.644a.787.787 0 0 1-.327.07z"></path></svg>`;
|
export default /** @type {import("lit").TemplateResult} */ (
|
||||||
|
tag =>
|
||||||
|
tag`<svg focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" data-test-id="svg-heart"><path d="M8.002 14.867a.828.828 0 0 1-.324-.066C7.36 14.665 0 11.466 0 6.153 0 3.592 1.543.825 4.98 1.01c1.465.077 2.437.828 3.018 1.491.581-.667 1.553-1.414 3.023-1.491.107-.008.207-.008.31-.008C13.58 1.001 16 2.614 16 6.153c0 5.313-7.36 8.512-7.671 8.644a.787.787 0 0 1-.327.07z"></path></svg>`
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -183,13 +183,19 @@ describe('lion-icon', () => {
|
||||||
try {
|
try {
|
||||||
icons.addIconResolver(
|
icons.addIconResolver(
|
||||||
'foo',
|
'foo',
|
||||||
|
() =>
|
||||||
|
/** @type {Promise<TemplateResult>} */ (
|
||||||
// eslint-disable-next-line no-promise-executor-return
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
() => new Promise(resolve => setTimeout(() => resolve(heartSvg), 10)),
|
new Promise(resolve => setTimeout(() => resolve(heartSvg)))
|
||||||
|
),
|
||||||
);
|
);
|
||||||
icons.addIconResolver(
|
icons.addIconResolver(
|
||||||
'bar',
|
'bar',
|
||||||
|
() =>
|
||||||
|
/** @type {Promise<TemplateResult>} */ (
|
||||||
// eslint-disable-next-line no-promise-executor-return
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
() => new Promise(resolve => setTimeout(() => resolve(hammerSvg), 4)),
|
new Promise(resolve => setTimeout(() => resolve(hammerSvg)))
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const el = await fixture(html`<lion-icon icon-id="foo:lorem:ipsum"></lion-icon>`);
|
const el = await fixture(html`<lion-icon icon-id="foo:lorem:ipsum"></lion-icon>`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { singletonManager } from 'singleton-manager';
|
import { singletonManager, lazifyInstantiation } from 'singleton-manager';
|
||||||
import { LocalizeManager } from './LocalizeManager.js';
|
import { LocalizeManager } from './LocalizeManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,18 +57,24 @@ import { LocalizeManager } from './LocalizeManager.js';
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
* @deprecated Use `localize` directly instead (as this is always side-effect free now)
|
||||||
|
*
|
||||||
* @returns {LocalizeManager}
|
* @returns {LocalizeManager}
|
||||||
*/
|
*/
|
||||||
export function getLocalizeManager() {
|
function getLocalizeManager() {
|
||||||
if (singletonManager.has('@lion/ui::localize::0.x')) {
|
if (!singletonManager.has('@lion/ui::localize::0.x')) {
|
||||||
return singletonManager.get('@lion/ui::localize::0.x');
|
|
||||||
}
|
|
||||||
|
|
||||||
const localizeManager = new LocalizeManager({
|
const localizeManager = new LocalizeManager({
|
||||||
autoLoadOnLocaleChange: true,
|
autoLoadOnLocaleChange: true,
|
||||||
fallbackLocale: 'en-GB',
|
fallbackLocale: 'en-GB',
|
||||||
});
|
});
|
||||||
singletonManager.set('@lion/ui::localize::0.x', localizeManager);
|
singletonManager.set('@lion/ui::localize::0.x', localizeManager);
|
||||||
|
|
||||||
return localizeManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return singletonManager.get('@lion/ui::localize::0.x');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalizeManagerLazily() {
|
||||||
|
return lazifyInstantiation(getLocalizeManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getLocalizeManagerLazily as getLocalizeManager };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { expect } from '@open-wc/testing';
|
import { expect } from '@open-wc/testing';
|
||||||
|
import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { singletonManager } from 'singleton-manager';
|
import { singletonManager } from 'singleton-manager';
|
||||||
import { LocalizeManager } from '../src/LocalizeManager.js';
|
import { LocalizeManager } from '../src/LocalizeManager.js';
|
||||||
import { getLocalizeManager } from '../src/getLocalizeManager.js';
|
|
||||||
|
/** @typedef {LocalizeManager & { __instance_for_testing?: LocalizeManager }} LocalizeManagerForTesting */
|
||||||
|
|
||||||
describe('getLocalizeManager', () => {
|
describe('getLocalizeManager', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -12,20 +14,26 @@ describe('getLocalizeManager', () => {
|
||||||
|
|
||||||
it('gets a default instance when nothing registered on singletonManager with "@lion/ui::localize::0.x"', () => {
|
it('gets a default instance when nothing registered on singletonManager with "@lion/ui::localize::0.x"', () => {
|
||||||
expect(singletonManager.get('@lion/ui::localize::0.x')).to.be.undefined;
|
expect(singletonManager.get('@lion/ui::localize::0.x')).to.be.undefined;
|
||||||
const localizeManager = getLocalizeManager();
|
const /** @type {LocalizeManagerForTesting} */ localizeManager = getLocalizeManager();
|
||||||
expect(localizeManager).to.equal(singletonManager.get('@lion/ui::localize::0.x'));
|
expect(localizeManager.__instance_for_testing).to.equal(
|
||||||
|
singletonManager.get('@lion/ui::localize::0.x'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gets the same instance when called multiple times', () => {
|
it('gets the same instance when called multiple times', () => {
|
||||||
const localizeManager = getLocalizeManager();
|
const /** @type {LocalizeManagerForTesting} */ localizeManager = getLocalizeManager();
|
||||||
const localizeManagerSecondCall = getLocalizeManager();
|
const /** @type {LocalizeManagerForTesting} */ localizeManagerSecondCall = getLocalizeManager();
|
||||||
expect(localizeManager).to.equal(localizeManagerSecondCall);
|
expect(localizeManager.__instance_for_testing).not.to.be.undefined;
|
||||||
|
expect(localizeManager.__instance_for_testing).to.equal(
|
||||||
|
localizeManagerSecondCall.__instance_for_testing,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gets the instance that was registered on singletonManager with "@lion/ui::localize::0.x"', () => {
|
it('gets the instance that was registered on singletonManager with "@lion/ui::localize::0.x"', () => {
|
||||||
// Set your own for custom behavior or for deduping purposes
|
// Set your own for custom behavior or for deduping purposes
|
||||||
class MyLocalizeManager extends LocalizeManager {}
|
class MyLocalizeManager extends LocalizeManager {}
|
||||||
singletonManager.set('@lion/ui::localize::0.x', MyLocalizeManager);
|
singletonManager.set('@lion/ui::localize::0.x', MyLocalizeManager);
|
||||||
expect(getLocalizeManager()).to.equal(MyLocalizeManager);
|
const /** @type {LocalizeManagerForTesting} */ localizeManager = getLocalizeManager();
|
||||||
|
expect(localizeManager.__instance_for_testing).to.equal(MyLocalizeManager);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { singletonManager } from 'singleton-manager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/LocalizeMixinTypes.js').LocalizeMixin} LocalizeMixinHost
|
* @typedef {import('../types/LocalizeMixinTypes.js').LocalizeMixin} LocalizeMixinHost
|
||||||
|
* @typedef {import('../../validate-messages/src/getLocalizedMessage.js').LocalizeManager & { __instance_for_testing?: import('../../validate-messages/src/getLocalizedMessage.js').LocalizeManager, aCallToRegisterLazilyLoadedInstance?: () => void }} LocalizeManagerForTesting
|
||||||
*/
|
*/
|
||||||
|
|
||||||
describe('Entrypoints localize', () => {
|
describe('Entrypoints localize', () => {
|
||||||
|
|
@ -24,15 +25,20 @@ describe('Entrypoints localize', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"@lion/ui/localize.js" has side effects (c.q. registers itself on singletonManager)', async () => {
|
it('"@lion/ui/localize.js" has side effects (c.q. registers itself on singletonManager)', async () => {
|
||||||
await import('@lion/ui/localize.js');
|
const /** @type {{ localize: LocalizeManagerForTesting }} */ { localize } = await import(
|
||||||
|
'@lion/ui/localize.js'
|
||||||
|
);
|
||||||
|
localize.aCallToRegisterLazilyLoadedInstance?.();
|
||||||
|
|
||||||
expect(singletonManagerSetSpy).to.have.been.calledOnce;
|
expect(singletonManagerSetSpy).to.have.been.calledOnce;
|
||||||
|
|
||||||
const { getLocalizeManager } = await import('@lion/ui/localize-no-side-effects.js');
|
const /** @type {{ getLocalizeManager: () => LocalizeManagerForTesting }} */ {
|
||||||
|
getLocalizeManager,
|
||||||
|
} = await import('@lion/ui/localize-no-side-effects.js');
|
||||||
|
|
||||||
expect(singletonManagerSetSpy).to.have.been.calledWith(
|
expect(singletonManagerSetSpy).to.have.been.calledWith(
|
||||||
'@lion/ui::localize::0.x',
|
'@lion/ui::localize::0.x',
|
||||||
getLocalizeManager(),
|
getLocalizeManager().__instance_for_testing,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import { singletonManager } from 'singleton-manager';
|
import { singletonManager, lazifyInstantiation } from 'singleton-manager';
|
||||||
import { OverlaysManager } from './OverlaysManager.js';
|
import { OverlaysManager } from './OverlaysManager.js';
|
||||||
|
|
||||||
export const overlays =
|
/**
|
||||||
/** @type {OverlaysManager} */
|
* @returns {OverlaysManager}
|
||||||
(singletonManager.get('@lion/ui::overlays::0.x')) || new OverlaysManager();
|
*/
|
||||||
|
function getOverlaysManager() {
|
||||||
|
if (!singletonManager.has('@lion/ui::overlays::0.x')) {
|
||||||
|
const overlaysManager = new OverlaysManager();
|
||||||
|
singletonManager.set('@lion/ui::overlays::0.x', overlaysManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return singletonManager.get('@lion/ui::overlays::0.x');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const overlays = lazifyInstantiation(getOverlaysManager);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue