Live: move centrifuge service to a web worker (#41090)

* Fix: make webpack pickup workers written in TS

* Add comlink to dependencies

* Temporary fix: copy paste `toDataQueryError` from @grafana/runtime to avoid web dependencies

* Implemented comlink-based centrifuge worker & worker proxy

* Temporary fix: implement comlink transferHandlers for subscriptions and streamingdataframes

* Move liveTimer filtering from CentrifugeService into GrafanaLiveService

* Switch from CentrifugeService to CentrifugeServiceWorkerProxy in GrafanaLive

* Naming fix

* Refactor: move liveTimer-based data filtering from GrafanaLiveService to CentrifugeServiceWorker

* observe dataStream on an async scheduler

* Fix: - Unsubscribe is now propagated from the main thread to the worker, - improve worker&workerProxy types

* Fix: Prettify types

* Fix: Add error & complete observers

* Docs: Add comment explaining the `subscriberTransferHandler`

* Fix: Replace `StreamingDataFrameHandler` with explicitly converting StreamingDataFrame to a DataFrameDTO

* Refactor: move liveTimer filtering to service.ts to make it easy to implement a `live-service-web-worker` feature flag

* Feat: add `live-service-web-worker` feature flag

* Fix: extract toDataQueryError.ts to a separate file within `@grafana-runtime` to avoid having a dependency from webworker to the whole package (@grafana-runtime/index.ts)

* Update public/app/features/dashboard/dashgrid/liveTimer.ts

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>

* Fix: fixed default import class in worker file

* Fix: cast worker as Endpoint

* Migrate from worker-loader to webpack native worker support v1 - broken prod build

* Fix: Use custom path in HtmlWebpackPlugin

* Fix: Loading workers from CDNs

* Fix: Avoid issues with jest ESM support by mocking `createWorker` files

* Fix: move the custom mockWorker rendering layout to `test/mocks`

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
pull/43065/head
Artur Wierzbicki 4 years ago committed by GitHub
parent e2ed140de2
commit f45eb309ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      package.json
  2. 2
      packages/grafana-runtime/src/index.ts
  3. 31
      packages/grafana-runtime/src/utils/queryResponse.ts
  4. 31
      packages/grafana-runtime/src/utils/toDataQueryError.ts
  5. 23
      public/app/core/utils/CorsWorker.ts
  6. 12
      public/app/features/dashboard/dashgrid/liveTimer.ts
  7. 1
      public/app/features/explore/NodeGraphContainer.test.tsx
  8. 3
      public/app/features/live/centrifuge/createCentrifugeServiceWorker.ts
  9. 33
      public/app/features/live/centrifuge/remoteObservable.ts
  10. 60
      public/app/features/live/centrifuge/service.ts
  11. 52
      public/app/features/live/centrifuge/service.worker.ts
  12. 40
      public/app/features/live/centrifuge/serviceWorkerProxy.ts
  13. 27
      public/app/features/live/centrifuge/transferHandlers.ts
  14. 14
      public/app/features/live/index.ts
  15. 1
      public/app/plugins/panel/nodeGraph/NodeGraph.test.tsx
  16. 12
      public/app/plugins/panel/nodeGraph/__mocks__/layout.worker.js
  17. 3
      public/app/plugins/panel/nodeGraph/createLayoutWorker.ts
  18. 5
      public/app/plugins/panel/nodeGraph/layout.ts
  19. 1
      public/test/jest-setup.ts
  20. 27
      public/test/mocks/workers.ts
  21. 64
      scripts/webpack/plugins/CorsWorkerPlugin.js
  22. 12
      scripts/webpack/webpack.common.js
  23. 21
      yarn.lock

@ -225,8 +225,7 @@
"webpack-cleanup-plugin": "0.5.1",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.3.1",
"webpack-merge": "5.8.0",
"worker-loader": "^3.0.8"
"webpack-merge": "5.8.0"
},
"dependencies": {
"@emotion/css": "11.1.3",
@ -269,6 +268,7 @@
"centrifuge": "2.7.5",
"classnames": "2.2.6",
"clipboard": "2.0.4",
"comlink": "4.3.1",
"common-tags": "^1.8.0",
"core-js": "3.10.0",
"d3": "5.15.0",

@ -17,12 +17,12 @@ export {
StreamOptionsProvider,
} from './utils/DataSourceWithBackend';
export {
toDataQueryError,
toDataQueryResponse,
frameToMetricFindValue,
BackendDataSourceResponse,
DataResponse,
} from './utils/queryResponse';
export { toDataQueryError } from './utils/toDataQueryError';
export { PanelRenderer, PanelRendererProps, PanelRendererType, setPanelRenderer } from './components/PanelRenderer';
export { setQueryRunnerFactory, createQueryRunner, QueryRunnerFactory } from './services/QueryRunner';
export { DataSourcePicker, DataSourcePickerProps, DataSourcePickerState } from './components/DataSourcePicker';

@ -14,6 +14,7 @@ import {
dataFrameFromJSON,
} from '@grafana/data';
import { FetchError, FetchResponse } from '../services';
import { toDataQueryError } from './toDataQueryError';
/**
* Single response object from a backend data source. Properties are optional but response should contain at least
@ -159,36 +160,6 @@ export function toTestingStatus(err: FetchError): any {
throw err;
}
/**
* Convert an object into a DataQueryError -- if this is an HTTP response,
* it will put the correct values in the error field
*
* @public
*/
export function toDataQueryError(err: DataQueryError | string | Object): DataQueryError {
const error = (err || {}) as DataQueryError;
if (!error.message) {
if (typeof err === 'string' || err instanceof String) {
return { message: err } as DataQueryError;
}
let message = 'Query error';
if (error.message) {
message = error.message;
} else if (error.data && error.data.message) {
message = error.data.message;
} else if (error.data && error.data.error) {
message = error.data.error;
} else if (error.status) {
message = `Query error: ${error.status} ${error.statusText}`;
}
error.message = message;
}
return error;
}
/**
* Return the first string or non-time field as the value
*

@ -0,0 +1,31 @@
import { DataQueryError } from '@grafana/data';
/**
* Convert an object into a DataQueryError -- if this is an HTTP response,
* it will put the correct values in the error field
*
* @public
*/
export function toDataQueryError(err: DataQueryError | string | Object): DataQueryError {
const error = (err || {}) as DataQueryError;
if (!error.message) {
if (typeof err === 'string' || err instanceof String) {
return { message: err } as DataQueryError;
}
let message = 'Query error';
if (error.message) {
message = error.message;
} else if (error.data && error.data.message) {
message = error.data.message;
} else if (error.data && error.data.error) {
message = error.data.error;
} else if (error.status) {
message = `Query error: ${error.status} ${error.statusText}`;
}
error.message = message;
}
return error;
}

@ -0,0 +1,23 @@
// works with webpack plugin: scripts/webpack/plugins/CorsWorkerPlugin.js
export class CorsWorker extends window.Worker {
constructor(url: URL, options?: WorkerOptions) {
// by default, worker inherits HTML document's location and pathname which leads to wrong public path value
// the CorsWorkerPlugin will override it with the value based on the initial worker chunk, ie.
// initial worker chunk: http://host.com/cdn/scripts/worker-123.js
// resulting public path: http://host.com/cdn/scripts
const scriptUrl = url.toString();
const urlParts = scriptUrl.split('/');
urlParts.pop();
const scriptsBasePathUrl = `${urlParts.join('/')}/`;
const importScripts = `importScripts('${scriptUrl}');`;
const objectURL = URL.createObjectURL(
new Blob([`__webpack_worker_public_path__ = '${scriptsBasePathUrl}'; ${importScripts}`], {
type: 'application/javascript',
})
);
super(objectURL, options);
URL.revokeObjectURL(objectURL);
}
}

@ -1,4 +1,5 @@
import { dateMath, dateTime, TimeRange } from '@grafana/data';
import { BehaviorSubject } from 'rxjs';
import { PanelChrome } from './PanelChrome';
// target is 20hz (50ms), but we poll at 100ms to smooth out jitter
@ -15,7 +16,7 @@ class LiveTimer {
budget = 1;
threshold = 1.5; // trial and error appears about right
ok = true;
ok = new BehaviorSubject(true);
lastUpdate = Date.now();
isLive = false; // the dashboard time range ends in "now"
@ -69,11 +70,16 @@ class LiveTimer {
measure = () => {
const now = Date.now();
this.budget = (now - this.lastUpdate) / interval;
this.ok = this.budget <= this.threshold;
const oldOk = this.ok.getValue();
const newOk = this.budget <= this.threshold;
if (oldOk !== newOk) {
this.ok.next(newOk);
}
this.lastUpdate = now;
// For live dashboards, listen to changes
if (this.ok && this.isLive && this.timeRange) {
if (this.isLive && this.ok.getValue() && this.timeRange) {
// when the time-range is relative fire events
let tr: TimeRange | undefined = undefined;
for (const listener of this.listeners) {

@ -3,7 +3,6 @@ import { render, screen } from '@testing-library/react';
import { UnconnectedNodeGraphContainer } from './NodeGraphContainer';
import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data';
import { ExploreId } from '../../types';
jest.mock('../../plugins/panel/nodeGraph/layout.worker.js');
describe('NodeGraphContainer', () => {
it('is collapsed if shown with traces', () => {

@ -0,0 +1,3 @@
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
export const createWorker = () => new Worker(new URL('./service.worker.ts', import.meta.url));

@ -0,0 +1,33 @@
import * as comlink from 'comlink';
import { from, Observable, switchMap } from 'rxjs';
export const remoteObservableAsObservable = <T>(remoteObs: comlink.RemoteObject<Observable<T>>): Observable<T> =>
new Observable((subscriber) => {
// Passing the callbacks as 3 separate arguments is deprecated, but it's the only option for now
//
// RxJS recreates the functions via `Function.bind` https://github.com/ReactiveX/rxjs/blob/62aca850a37f598b5db6085661e0594b81ec4281/src/internal/Subscriber.ts#L169
// and thus erases the ProxyMarker created via comlink.proxy(fN) when the callbacks
// are grouped together in a Observer object (ie. { next: (v) => ..., error: (err) => ..., complete: () => ... })
//
// solution: TBD (autoproxy all functions?)
const remoteSubPromise = remoteObs.subscribe(
comlink.proxy((nextValueInRemoteObs: T) => {
subscriber.next(nextValueInRemoteObs);
}),
comlink.proxy((err) => {
subscriber.error(err);
}),
comlink.proxy(() => {
subscriber.complete();
})
);
return {
unsubscribe: () => {
remoteSubPromise.then((remoteSub) => remoteSub.unsubscribe());
},
};
});
export const promiseWithRemoteObservableAsObservable = <T>(
promiseWithProxyObservable: Promise<comlink.RemoteObject<Observable<T>>>
): Observable<T> => from(promiseWithProxyObservable).pipe(switchMap((val) => remoteObservableAsObservable(val)));

@ -1,5 +1,6 @@
import Centrifuge from 'centrifuge/dist/centrifuge';
import { LiveDataStreamOptions, toDataQueryError } from '@grafana/runtime';
import { LiveDataStreamOptions } from '@grafana/runtime';
import { toDataQueryError } from '@grafana/runtime/src/utils/toDataQueryError';
import { BehaviorSubject, Observable } from 'rxjs';
import {
DataFrame,
@ -15,25 +16,52 @@ import {
LiveChannelPresenceStatus,
LoadingState,
StreamingDataFrame,
toDataFrameDTO,
} from '@grafana/data';
import { CentrifugeLiveChannel } from './channel';
import { liveTimer } from 'app/features/dashboard/dashgrid/liveTimer';
type CentrifugeSrvDeps = {
export type CentrifugeSrvDeps = {
appUrl: string;
orgId: number;
orgRole: string;
sessionId: string;
liveEnabled: boolean;
dataStreamSubscriberReadiness: Observable<boolean>;
};
export class CentrifugeSrv {
export interface CentrifugeSrv {
/**
* Listen for changes to the connection state
*/
getConnectionState(): Observable<boolean>;
/**
* Watch for messages in a channel
*/
getStream<T>(address: LiveChannelAddress, config: LiveChannelConfig): Observable<LiveChannelEvent<T>>;
/**
* Connect to a channel and return results as DataFrames
*/
getDataStream(options: LiveDataStreamOptions, config: LiveChannelConfig): Observable<DataQueryResponse>;
/**
* For channels that support presence, this will request the current state from the server.
*
* Join and leave messages will be sent to the open stream
*/
getPresence(address: LiveChannelAddress, config: LiveChannelConfig): Promise<LiveChannelPresenceStatus>;
}
export class CentrifugeService implements CentrifugeSrv {
readonly open = new Map<string, CentrifugeLiveChannel>();
readonly centrifuge: Centrifuge;
readonly connectionState: BehaviorSubject<boolean>;
readonly connectionBlocker: Promise<void>;
private dataStreamSubscriberReady = true;
constructor(private deps: CentrifugeSrvDeps) {
deps.dataStreamSubscriberReadiness.subscribe((next) => (this.dataStreamSubscriberReady = next));
const liveUrl = `${deps.appUrl.replace(/^http/, 'ws')}/api/live/ws`;
this.centrifuge = new Centrifuge(liveUrl, {});
this.centrifuge.setConnectData({
@ -66,15 +94,15 @@ export class CentrifugeSrv {
// Internal functions
//----------------------------------------------------------
onConnect = (context: any) => {
private onConnect = (context: any) => {
this.connectionState.next(true);
};
onDisconnect = (context: any) => {
private onDisconnect = (context: any) => {
this.connectionState.next(false);
};
onServerSideMessage = (context: any) => {
private onServerSideMessage = (context: any) => {
console.log('Publication from server-side channel', context);
};
@ -82,7 +110,7 @@ export class CentrifugeSrv {
* Get a channel. If the scope, namespace, or path is invalid, a shutdown
* channel will be returned with an error state indicated in its status
*/
getChannel<TMessage>(addr: LiveChannelAddress, config: LiveChannelConfig): CentrifugeLiveChannel<TMessage> {
private getChannel<TMessage>(addr: LiveChannelAddress, config: LiveChannelConfig): CentrifugeLiveChannel<TMessage> {
const id = `${this.deps.orgId}/${addr.scope}/${addr.namespace}/${addr.path}`;
let channel = this.open.get(id);
if (channel != null) {
@ -145,7 +173,6 @@ export class CentrifugeSrv {
let data: StreamingDataFrame | undefined = undefined;
let filtered: DataFrame | undefined = undefined;
let state = LoadingState.Streaming;
let last = liveTimer.lastUpdate;
let lastWidth = -1;
const process = (msg: DataFrameJSON) => {
@ -172,11 +199,18 @@ export class CentrifugeSrv {
}
}
const elapsed = liveTimer.lastUpdate - last;
if (elapsed > 1000 || liveTimer.ok) {
if (this.dataStreamSubscriberReady) {
filtered.length = data.length; // make sure they stay up-to-date
subscriber.next({ state, data: [filtered], key });
last = liveTimer.lastUpdate;
subscriber.next({
state,
data: [
// workaround for serializing issues when sending DataFrame from web worker to the main thread
// DataFrame is making use of ArrayVectors which are es6 classes and thus not cloneable out of the box
// `toDataFrameDTO` converts ArrayVectors into native arrays.
toDataFrameDTO(filtered),
],
key,
});
}
};

@ -0,0 +1,52 @@
import { CentrifugeService, CentrifugeSrvDeps } from './service';
import * as comlink from 'comlink';
import './transferHandlers';
import { remoteObservableAsObservable } from './remoteObservable';
import { LiveChannelAddress, LiveChannelConfig } from '@grafana/data';
import { LiveDataStreamOptions } from '@grafana/runtime';
let centrifuge: CentrifugeService;
const initialize = (
deps: CentrifugeSrvDeps,
remoteDataStreamSubscriberReadiness: comlink.RemoteObject<
CentrifugeSrvDeps['dataStreamSubscriberReadiness'] & comlink.ProxyMarked
>
) => {
centrifuge = new CentrifugeService({
...deps,
dataStreamSubscriberReadiness: remoteObservableAsObservable(remoteDataStreamSubscriberReadiness),
});
};
const getConnectionState = () => {
return comlink.proxy(centrifuge.getConnectionState());
};
const getDataStream = (options: LiveDataStreamOptions, config: LiveChannelConfig) => {
return comlink.proxy(centrifuge.getDataStream(options, config));
};
const getStream = (address: LiveChannelAddress, config: LiveChannelConfig) => {
return comlink.proxy(centrifuge.getStream(address, config));
};
const getPresence = async (address: LiveChannelAddress, config: LiveChannelConfig) => {
return await centrifuge.getPresence(address, config);
};
const workObj = {
initialize,
getConnectionState,
getDataStream,
getStream,
getPresence,
};
export type RemoteCentrifugeService = typeof workObj;
comlink.expose(workObj);
export default class {
constructor() {}
}

@ -0,0 +1,40 @@
import { CentrifugeSrv, CentrifugeSrvDeps } from './service';
import { RemoteCentrifugeService } from './service.worker';
import './transferHandlers';
import * as comlink from 'comlink';
import { asyncScheduler, Observable, observeOn } from 'rxjs';
import { LiveChannelAddress, LiveChannelConfig, LiveChannelEvent } from '@grafana/data';
import { promiseWithRemoteObservableAsObservable } from './remoteObservable';
import { createWorker } from './createCentrifugeServiceWorker';
export class CentrifugeServiceWorkerProxy implements CentrifugeSrv {
private centrifugeWorker;
constructor(deps: CentrifugeSrvDeps) {
this.centrifugeWorker = comlink.wrap<RemoteCentrifugeService>(createWorker() as comlink.Endpoint);
this.centrifugeWorker.initialize(deps, comlink.proxy(deps.dataStreamSubscriberReadiness));
}
getConnectionState: CentrifugeSrv['getConnectionState'] = () => {
return promiseWithRemoteObservableAsObservable(this.centrifugeWorker.getConnectionState());
};
getDataStream: CentrifugeSrv['getDataStream'] = (options, config) => {
return promiseWithRemoteObservableAsObservable(this.centrifugeWorker.getDataStream(options, config)).pipe(
// async scheduler splits the synchronous task of deserializing data from web worker and
// consuming the message (ie. updating react component) into two to avoid blocking the event loop
observeOn(asyncScheduler)
);
};
getPresence: CentrifugeSrv['getPresence'] = (address, config) => {
return this.centrifugeWorker.getPresence(address, config);
};
getStream: CentrifugeSrv['getStream'] = <T>(address: LiveChannelAddress, config: LiveChannelConfig) => {
return promiseWithRemoteObservableAsObservable(
this.centrifugeWorker.getStream(address, config) as Promise<comlink.Remote<Observable<LiveChannelEvent<T>>>>
);
};
}

@ -0,0 +1,27 @@
import * as comlink from 'comlink';
import { Subscriber } from 'rxjs';
// Observers, ie. functions passed to `observable.subscribe(...)`, are converted to a subclass of `Subscriber` before they are sent to the source Observable.
// The conversion happens internally in the RxJS library - this transfer handler is catches them and wraps them with a proxy
const subscriberTransferHandler: any = {
canHandle(value: any): boolean {
return value && value instanceof Subscriber;
},
serialize(value: Function): [MessagePort, Transferable[]] {
const obj = comlink.proxy(value);
const { port1, port2 } = new MessageChannel();
comlink.expose(obj, port1);
return [port2, [port2]];
},
deserialize(value: MessagePort): comlink.Remote<MessagePort> {
value.start();
return comlink.wrap<MessagePort>(value);
},
};
comlink.transferHandlers.set('SubscriberHandler', subscriberTransferHandler);

@ -1,10 +1,12 @@
import { config, getBackendSrv, getGrafanaLiveSrv, setGrafanaLiveSrv } from '@grafana/runtime';
import { CentrifugeSrv } from './centrifuge/service';
import { registerLiveFeatures } from './features';
import { GrafanaLiveService } from './live';
import { GrafanaLiveChannelConfigService } from './channel-config';
import { GrafanaLiveChannelConfigSrv } from './channel-config/types';
import { contextSrv } from '../../core/services/context_srv';
import { CentrifugeServiceWorkerProxy } from './centrifuge/serviceWorkerProxy';
import { CentrifugeService } from './centrifuge/service';
import { liveTimer } from 'app/features/dashboard/dashgrid/liveTimer';
const grafanaLiveScopesSingleton = new GrafanaLiveChannelConfigService();
@ -18,13 +20,19 @@ export const sessionId =
Math.random().toString(36).substring(2, 15);
export function initGrafanaLive() {
const centrifugeSrv = new CentrifugeSrv({
const centrifugeServiceDeps = {
appUrl: `${window.location.origin}${config.appSubUrl}`,
orgId: contextSrv.user.orgId,
orgRole: contextSrv.user.orgRole,
liveEnabled: config.liveEnabled,
sessionId,
});
dataStreamSubscriberReadiness: liveTimer.ok.asObservable(),
};
const centrifugeSrv = config.featureToggles['live-service-web-worker']
? new CentrifugeServiceWorkerProxy(centrifugeServiceDeps)
: new CentrifugeService(centrifugeServiceDeps);
setGrafanaLiveSrv(
new GrafanaLiveService({
scopes: getGrafanaLiveScopes(),

@ -3,7 +3,6 @@ import { render, screen, fireEvent, waitFor, getByText } from '@testing-library/
import userEvent from '@testing-library/user-event';
import { NodeGraph } from './NodeGraph';
import { makeEdgesDataFrame, makeNodesDataFrame } from './utils';
jest.mock('./layout.worker.js');
jest.mock('react-use/lib/useMeasure', () => {
return {

@ -1,12 +0,0 @@
const { layout } = jest.requireActual('../layout.worker.js');
export default class TestWorker {
constructor() {}
postMessage(data) {
const { nodes, edges, config } = data;
setTimeout(() => {
layout(nodes, edges, config);
this.onmessage({ data: { nodes, edges } });
}, 1);
}
}

@ -0,0 +1,3 @@
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
export const createWorker = () => new Worker(new URL('./layout.worker.js', import.meta.url));

@ -4,8 +4,7 @@ import { Field } from '@grafana/data';
import { useNodeLimit } from './useNodeLimit';
import useMountedState from 'react-use/lib/useMountedState';
import { graphBounds } from './utils';
// @ts-ignore
import LayoutWorker from './layout.worker.js';
import { createWorker } from './createLayoutWorker';
export interface Config {
linkDistance: number;
@ -135,7 +134,7 @@ function defaultLayout(
edges: EdgeDatum[],
done: (data: { nodes: NodeDatum[]; edges: EdgeDatum[] }) => void
) {
const worker = new LayoutWorker();
const worker = createWorker();
worker.onmessage = (event: MessageEvent<{ nodes: NodeDatum[]; edges: EdgeDatumLayout[] }>) => {
for (let i = 0; i < nodes.length; i++) {
// These stats needs to be Field class but the data is stringified over the worker boundary

@ -3,6 +3,7 @@ import { EventBusSrv } from '@grafana/data';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import $ from 'jquery';
import 'mutationobserver-shim';
import './mocks/workers';
const testAppEvents = new EventBusSrv();
const global = window as any;

@ -0,0 +1,27 @@
const { layout } = jest.requireActual('../../app/plugins/panel/nodeGraph/layout.worker.js');
class LayoutMockWorker {
constructor() {}
postMessage(data: any) {
const { nodes, edges, config } = data;
setTimeout(() => {
layout(nodes, edges, config);
// @ts-ignore
this.onmessage({ data: { nodes, edges } });
}, 1);
}
}
jest.mock('../../app/plugins/panel/nodeGraph/createLayoutWorker', () => ({
createWorker: () => new LayoutMockWorker(),
}));
class BasicMockWorker {
postMessage() {}
}
const mockCreateWorker = {
createWorker: () => new BasicMockWorker(),
};
jest.mock('../../app/features/live/centrifuge/createCentrifugeServiceWorker', () => mockCreateWorker);

@ -0,0 +1,64 @@
const { RuntimeGlobals, RuntimeModule } = require('webpack');
class CorsWorkerPublicPathRuntimeModule extends RuntimeModule {
constructor(publicPath) {
super('publicPath', RuntimeModule.STAGE_BASIC);
this.publicPath = publicPath;
}
/**
* @returns {string} runtime code
*/
generate() {
const { compilation, publicPath } = this;
const publicPathValue = compilation.getPath(publicPath || '', {
hash: compilation.hash || 'XXXX',
});
return `${RuntimeGlobals.publicPath} = __webpack_worker_public_path__ || '${publicPathValue}';`;
}
}
// https://github.com/webpack/webpack/discussions/14648#discussioncomment-1604202
// by @ https://github.com/piotr-oles
class CorsWorkerPlugin {
/**
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap(
'CorsWorkerPlugin',
/**
* @param {import('webpack').Compilation} compilation
*/
(compilation) => {
const getChunkLoading = (chunk) => {
const entryOptions = chunk.getEntryOptions();
return entryOptions && entryOptions.chunkLoading !== undefined
? entryOptions.chunkLoading
: compilation.outputOptions.chunkLoading;
};
const getChunkPublicPath = (chunk) => {
const entryOptions = chunk.getEntryOptions();
return entryOptions && entryOptions.publicPath !== undefined
? entryOptions.publicPath
: compilation.outputOptions.publicPath;
};
compilation.hooks.runtimeRequirementInTree.for(RuntimeGlobals.publicPath).tap('CorsWorkerPlugin', (chunk) => {
if (getChunkLoading(chunk) === 'import-scripts') {
const publicPath = getChunkPublicPath(chunk);
if (publicPath !== 'auto') {
const module = new CorsWorkerPublicPathRuntimeModule(publicPath);
compilation.addRuntimeModule(chunk, module);
return true;
}
}
});
}
);
}
}
module.exports = CorsWorkerPlugin;

@ -1,7 +1,7 @@
const fs = require('fs-extra');
const path = require('path');
const webpack = require('webpack');
const CorsWorkerPlugin = require('./plugins/CorsWorkerPlugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
class CopyUniconsPlugin {
@ -58,6 +58,7 @@ module.exports = {
source: false,
},
plugins: [
new CorsWorkerPlugin(),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
@ -124,15 +125,6 @@ module.exports = {
loader: 'file-loader',
options: { name: 'static/img/[name].[hash:8].[ext]' },
},
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
inline: 'fallback',
},
},
},
],
},
// https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3

@ -12355,6 +12355,13 @@ __metadata:
languageName: node
linkType: hard
"comlink@npm:4.3.1":
version: 4.3.1
resolution: "comlink@npm:4.3.1"
checksum: 557360a6558708c55aff74a25f834bfb9bfca8a42444682c4d5aead57681534a0206202be2a2760b4de124c3ba6d485b08978b6d5469cb3d26bf1438ee28a4f1
languageName: node
linkType: hard
"comma-separated-tokens@npm:^1.0.0":
version: 1.0.8
resolution: "comma-separated-tokens@npm:1.0.8"
@ -17829,6 +17836,7 @@ __metadata:
centrifuge: 2.7.5
classnames: 2.2.6
clipboard: 2.0.4
comlink: 4.3.1
common-tags: ^1.8.0
copy-webpack-plugin: 9.0.1
core-js: 3.10.0
@ -17994,7 +18002,6 @@ __metadata:
webpack-dev-server: 4.3.1
webpack-merge: 5.8.0
whatwg-fetch: 3.1.0
worker-loader: ^3.0.8
languageName: unknown
linkType: soft
@ -33911,18 +33918,6 @@ __metadata:
languageName: node
linkType: hard
"worker-loader@npm:^3.0.8":
version: 3.0.8
resolution: "worker-loader@npm:3.0.8"
dependencies:
loader-utils: ^2.0.0
schema-utils: ^3.0.0
peerDependencies:
webpack: ^4.0.0 || ^5.0.0
checksum: 84f4a7eeb2a1d8b9704425837e017c91eedfae67ac89e0b866a2dcf283323c1dcabe0258196278b7d5fd0041392da895c8a0c59ddf3a94f1b2e003df68ddfec3
languageName: node
linkType: hard
"worker-rpc@npm:^0.1.0":
version: 0.1.1
resolution: "worker-rpc@npm:0.1.1"

Loading…
Cancel
Save