Skip to content

Commit

Permalink
Merge pull request #146 from core-ds/fix/loading-abort
Browse files Browse the repository at this point in the history
fix(module-loader): allow to abort module loading while stil loading
  • Loading branch information
heymdall-legal authored Oct 16, 2023
2 parents 73b6354 + 92cb660 commit 1d5758e
Show file tree
Hide file tree
Showing 16 changed files with 386 additions and 227 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-chefs-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alfalab/scripts-modules': patch
---

Исправлено удаление модулей и их ресурсов со страници при отмонтировании хука до загрузки модуля
1 change: 1 addition & 0 deletions packages/arui-scripts-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"typescript": "4.9.5"
},
"dependencies": {
"abortcontroller-polyfill": "^1.7.5",
"uuid": "^9.0.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { createModuleLoader } from '../create-module-loader';
import { getConsumerCounter } from '../utils/consumers-counter';
import { getCompatModule,getModule } from '../utils/get-module';
import { mountModuleResources } from '../utils/mount-module-resources';
import { fetchResources } from '../utils/fetch-resources';
import * as cleanGlobal from '../utils/clean-global';
import * as domUtils from '../utils/dom-utils';

jest.mock('../utils/mount-module-resources', () => ({
mountModuleResources: jest.fn(() => []),
jest.mock('../utils/fetch-resources', () => ({
fetchResources: jest.fn(() => []),
getResourcesTargetNodes: jest.fn(() => []),
}));

jest.mock('../utils/get-module', () => ({
getModule: jest.fn(),
getCompatModule: jest.fn(),
}));

jest.mock('../utils/consumers-counter', () => ({
getConsumerCounter: jest.fn(() => ({
increase: jest.fn(),
decrease: jest.fn(),
getCounter: jest.fn(),
})),
}));

describe('createModuleLoader', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should create a module loader', () => {
const loader = createModuleLoader({
moduleId: 'test',
Expand Down Expand Up @@ -121,9 +117,9 @@ describe('createModuleLoader', () => {
expect(onAfterModuleUnmount).toHaveBeenCalled();
});

it('should pass correct params to mountModuleResources', async () => {
it('should pass correct params to fetchResources', async () => {
const getModuleResources = jest.fn();
const mountModuleResourcesMock = mountModuleResources as jest.Mock;
const fetchResourcesMock = fetchResources as jest.Mock;
const loader = createModuleLoader({
moduleId: 'test',
hostAppId: 'test',
Expand All @@ -143,10 +139,10 @@ describe('createModuleLoader', () => {

await loader({ getResourcesParams: undefined, cssTargetSelector: '.target' });

expect(mountModuleResourcesMock).toHaveBeenCalledWith({
resourcesTargetNode: undefined,
expect(fetchResourcesMock).toHaveBeenCalledWith({
jsTargetNode: undefined,
cssTargetNode: undefined,
cssTargetSelector: '.target',
moduleConsumersCounter: expect.anything(),
moduleId: 'test',
scripts: [],
styles: [],
Expand Down Expand Up @@ -222,67 +218,126 @@ describe('createModuleLoader', () => {
);
});

describe('module consumers counter', () => {
it('should increase module consumers counter on mount', async () => {
it('should not remove module resources if there is still someone consuming it', async () => {
jest.spyOn(domUtils, 'removeModuleResources');
jest.spyOn(cleanGlobal, 'cleanGlobal');
const getModuleResources = jest.fn();
const loader = createModuleLoader({
moduleId: 'test',
hostAppId: 'test',
getModuleResources,
});

getModuleResources.mockResolvedValue({
scripts: [],
styles: [],
moduleState: {},
mountMode: 'default',
appName: 'AppName',
});

(getModule as jest.Mock).mockResolvedValue({});

const { unmount } = await loader({ getResourcesParams: undefined });
await loader({ getResourcesParams: undefined });

unmount();

expect(domUtils.removeModuleResources).not.toHaveBeenCalled();
expect(cleanGlobal.cleanGlobal).not.toHaveBeenCalled();
});

it('should remove module resources if there is no one consuming it', async () => {
jest.spyOn(domUtils, 'removeModuleResources');
jest.spyOn(cleanGlobal, 'cleanGlobal');
const getModuleResources = jest.fn();
const loader = createModuleLoader({
moduleId: 'unique-id',
hostAppId: 'test',
getModuleResources,
});

getModuleResources.mockResolvedValue({
scripts: [],
styles: [],
moduleState: {},
mountMode: 'default',
appName: 'AppName',
});

(getModule as jest.Mock).mockResolvedValueOnce({});

const { unmount } = await loader({ getResourcesParams: undefined });

unmount();

expect(domUtils.removeModuleResources).toHaveBeenCalledWith({ moduleId: 'unique-id', targetNodes: [] });
expect(cleanGlobal.cleanGlobal).toHaveBeenCalledWith('unique-id');
});

describe('aborting', () => {
it('should reject with error if provided abortSignal is aborted', async () => {
const getModuleResources = jest.fn();
const loader = createModuleLoader({
moduleId: 'test',
hostAppId: 'test',
getModuleResources,
});
const moduleConsumersCounter = (getConsumerCounter as jest.Mock).mock.results[0].value;

getModuleResources.mockResolvedValue({
scripts: [],
styles: [],
moduleState: {},
mountMode: 'default',
appName: 'AppName',
});

(getModule as jest.Mock).mockResolvedValueOnce({});
const abortController = new AbortController();
abortController.abort();

await loader({ getResourcesParams: undefined });
await expect(loader({ getResourcesParams: undefined, abortSignal: abortController.signal })).rejects.toThrow(
'Module test loading was aborted'
);

expect(moduleConsumersCounter.increase).toHaveBeenCalledWith('test');
expect(getModuleResources).not.toHaveBeenCalled();
});

it('should decrease module consumers counter on unmount', async () => {
it('should pass abortSignal to fetchResources', async () => {
const getModuleResources = jest.fn();
const loader = createModuleLoader({
moduleId: 'test',
hostAppId: 'test',
getModuleResources,
});
const moduleConsumersCounter = (getConsumerCounter as jest.Mock).mock.results[0].value;

const abortController = new AbortController();

getModuleResources.mockResolvedValue({
scripts: [],
styles: [],
moduleState: {},
mountMode: 'default',
appName: 'AppName',
});

(getModule as jest.Mock).mockResolvedValueOnce({});

const { unmount } = await loader({ getResourcesParams: undefined });

unmount();
await loader({ getResourcesParams: undefined, abortSignal: abortController.signal });

expect(moduleConsumersCounter.decrease).toHaveBeenCalledWith('test');
expect(fetchResources).toHaveBeenCalledWith({
jsTargetNode: undefined,
cssTargetNode: undefined,
cssTargetSelector: undefined,
moduleId: 'test',
scripts: [],
styles: [],
baseUrl: undefined,
abortSignal: abortController.signal,
});
});

it('should remove module resources if module consumers counter is 0', async () => {
it('should remove module resources when receive abort signal', async () => {
jest.spyOn(domUtils, 'removeModuleResources');
jest.spyOn(cleanGlobal, 'cleanGlobal');
const getModuleResources = jest.fn();
const loader = createModuleLoader({
moduleId: 'test',
moduleId: 'another-id',
hostAppId: 'test',
getModuleResources,
});
const moduleConsumersCounter = (getConsumerCounter as jest.Mock).mock.results[0].value;

const abortController = new AbortController();

getModuleResources.mockResolvedValue({
scripts: [],
Expand All @@ -293,15 +348,13 @@ describe('createModuleLoader', () => {
});

(getModule as jest.Mock).mockResolvedValueOnce({});
moduleConsumersCounter.getCounter.mockReturnValueOnce(0);

const { unmount } = await loader({ getResourcesParams: undefined });
await loader({ getResourcesParams: undefined, abortSignal: abortController.signal });

unmount();
abortController.abort();

expect(moduleConsumersCounter.getCounter).toHaveBeenCalledWith('test');
expect(domUtils.removeModuleResources).toHaveBeenCalledWith({ moduleId: 'test', targetNodes: [] });
expect(cleanGlobal.cleanGlobal).toHaveBeenCalledWith('test');
expect(domUtils.removeModuleResources).toHaveBeenCalledWith({ moduleId: 'another-id', targetNodes: [] });
expect(cleanGlobal.cleanGlobal).toHaveBeenCalledWith('another-id');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { cleanGlobal } from './utils/clean-global';
import { getConsumerCounter } from './utils/consumers-counter';
import { removeModuleResources } from './utils/dom-utils';
import { fetchResources,getResourcesTargetNodes } from './utils/fetch-resources';
import { getCompatModule, getModule } from './utils/get-module';
import { mountModuleResources } from './utils/mount-module-resources';
import { BaseModuleState, GetResourcesRequest, Loader, ModuleResources } from './types';

export type ModuleResourcesGetter<GetResourcesParams, ModuleState extends BaseModuleState> = (
Expand Down Expand Up @@ -46,7 +46,7 @@ export type CreateModuleLoaderParams<
onAfterModuleUnmount?: ModuleLoaderHookWithModule<ModuleExportType, ModuleState>;
};

const moduleConsumersCounter = getConsumerCounter();
const consumerCounter = getConsumerCounter();

export function createModuleLoader<
ModuleExportType,
Expand All @@ -68,7 +68,36 @@ export function createModuleLoader<
> {
validateUsedWebpackVersion();

return async ({ cssTargetSelector, getResourcesParams}) => {
return async ({ abortSignal, getResourcesParams, cssTargetSelector}) => {
// Если во время загрузки модуля пришел сигнал об отмене, то отменяем загрузку
if (abortSignal?.aborted) {
throw new Error(`Module ${moduleId} loading was aborted`);
}

abortSignal?.addEventListener('abort', () => {
moduleUnmount();
});

consumerCounter.increase(moduleId);

const resourcesNodes = getResourcesTargetNodes({
resourcesTargetNode,
cssTargetSelector,
});

function moduleUnmount() {
consumerCounter.decrease(moduleId);

if (consumerCounter.getCounter(moduleId) === 0) {
// Если на странице больше нет потребителей модуля, то удаляем его ресурсы - скрипты, стили и глобальные переменные
removeModuleResources({
moduleId,
targetNodes: [resourcesNodes.js, resourcesNodes.css],
});
cleanGlobal(moduleId);
}
}

// Загружаем описание модуля
const moduleResources = await getModuleResources({
moduleId,
Expand All @@ -78,19 +107,17 @@ export function createModuleLoader<

await onBeforeResourcesMount?.(moduleId, moduleResources);

const resourcesNodes = await mountModuleResources({
resourcesTargetNode,
await fetchResources({
cssTargetNode: resourcesNodes.css,
jsTargetNode: resourcesNodes.js,
cssTargetSelector,
moduleConsumersCounter,
moduleId,
scripts: moduleResources.scripts,
styles: moduleResources.styles,
baseUrl: moduleResources.moduleState.baseUrl,
abortSignal,
});

// увеличиваем счетчик потребителей только после добавления скриптов и стилей
moduleConsumersCounter.increase(moduleId);

await onBeforeModuleMount?.(moduleId, moduleResources);

// В зависимости от типа модуля, получаем его контент необходимым способом
Expand All @@ -107,19 +134,8 @@ export function createModuleLoader<

return {
unmount: () => {
moduleConsumersCounter.decrease(moduleId);

onBeforeModuleUnmount?.(moduleId, moduleResources, loadedModule);

if (moduleConsumersCounter.getCounter(moduleId) === 0) {
// Если на странице больше нет потребителей модуля, то удаляем его ресурсы - скрипты, стили и глобальные переменные
removeModuleResources({
moduleId,
targetNodes: resourcesNodes,
});
cleanGlobal(moduleId);
}

moduleUnmount();
onAfterModuleUnmount?.(moduleId, moduleResources, loadedModule);
},
module: loadedModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ describe('useModuleFactory', () => {
await waitForNextUpdate();

expect(result.current.loadingState).toBe('fulfilled');
expect(loader).toHaveBeenCalledWith({ getResourcesParams: loaderParams });
expect(loader).toHaveBeenCalledWith({
getResourcesParams: loaderParams,
abortSignal: expect.any(AbortSignal),
});
expect(moduleExport).toHaveBeenCalledWith(runParams, 'serverState');
});

Expand Down Expand Up @@ -55,7 +58,10 @@ describe('useModuleFactory', () => {

expect(result.current.loadingState).toBe('rejected');
expect(result.current.module).toBeUndefined();
expect(loader).toHaveBeenCalledWith({ getResourcesParams: loaderParams });
expect(loader).toHaveBeenCalledWith({
getResourcesParams: loaderParams,
abortSignal: expect.any(AbortSignal),
});
});

it('should call unmount function of a loader when the component unmounts', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ describe('useModuleLoader', () => {
expect(result.current.module).toBe(moduleExport);
expect(result.current.resources).toBe(resources);

expect(loader).toHaveBeenCalledWith({ getResourcesParams: loaderParams });
expect(loader).toHaveBeenCalledWith({
getResourcesParams: loaderParams,
abortSignal: expect.any(AbortSignal),
});
});

it('should return an error when the loader rejects', async () => {
Expand Down Expand Up @@ -62,7 +65,10 @@ describe('useModuleLoader', () => {
await waitForNextUpdate();

expect(result.current.loadingState).toBe('fulfilled');
expect(loader).toHaveBeenCalledWith({ getResourcesParams: loaderParams });
expect(loader).toHaveBeenCalledWith({
getResourcesParams: loaderParams,
abortSignal: expect.any(AbortSignal),
});

rerender({ loader, loaderParams: { id: 'my-module' } });

Expand Down
Loading

0 comments on commit 1d5758e

Please sign in to comment.