mirror of https://github.com/rancher/dashboard.git
325 lines
9.5 KiB
JavaScript
325 lines
9.5 KiB
JavaScript
/**
|
|
* Advanced Worker is enabled by performance setting
|
|
* relocates cluster resource sockets off the UI thread and into a webworker
|
|
*/
|
|
|
|
// Status of this is TBD - https://github.com/rancher/dashboard/issues/15111
|
|
|
|
import { SCHEMA, COUNT } from '@shell/config/types';
|
|
import ResourceWatcher, { watchKeyFromMessage } from '@shell/plugins/steve/resourceWatcher';
|
|
import ResourceCache from '@shell/plugins/steve/caches/resourceCache';
|
|
import { EVENT_MESSAGE, EVENT_CONNECT_ERROR, EVENT_DISCONNECT_ERROR } from '@shell/utils/socket';
|
|
import { normalizeType, keyFieldFor } from '@shell/plugins/dashboard-store/normalize';
|
|
import { addSchemaIndexFields } from '@shell/plugins/steve/schema.utils';
|
|
|
|
const caches = {};
|
|
|
|
const state = {
|
|
watcher: undefined,
|
|
store: '', // Store name
|
|
/**
|
|
* Store `watch`/`unwatch` events to process when the socket is created
|
|
*/
|
|
workerQueue: [],
|
|
batchChanges: {},
|
|
debugWorker: false
|
|
};
|
|
|
|
const trace = (...args) => {
|
|
state.debugWorker && console.info('Advanced Worker:', ...args); // eslint-disable-line no-console
|
|
};
|
|
|
|
trace('created');
|
|
|
|
const maintenanceInterval = setInterval(() => {
|
|
if (Object.keys(state.batchChanges).length) {
|
|
self.postMessage({ batchChanges: state.batchChanges });
|
|
state.batchChanges = {};
|
|
}
|
|
}, 5000); // 5 seconds
|
|
|
|
const makeResourceProps = (msg) => {
|
|
const { resourceType, data: { type }, data } = msg;
|
|
const rawType = resourceType || type;
|
|
const normalizedType = normalizeType(rawType === 'counts' ? COUNT : rawType);
|
|
const keyField = keyFieldFor(normalizedType);
|
|
|
|
if ( normalizedType === SCHEMA ) {
|
|
addSchemaIndexFields(data);
|
|
}
|
|
|
|
return {
|
|
type: normalizedType,
|
|
id: data[keyField],
|
|
data
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Pass the EVENT_CONNECT_ERROR / EVENT_DISCONNECT_ERROR back to the UI thread
|
|
*/
|
|
const handleConnectionError = (eventType, event, watcher) => {
|
|
trace('createWatcher', eventType, event);
|
|
self.postMessage({
|
|
[eventType]: {
|
|
type: event.type,
|
|
detail: event.detail,
|
|
srcElement: {
|
|
disconnectedAt: watcher.disconnectedAt,
|
|
url: watcher.url,
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Remove any pending messages related to this resource from the queue
|
|
*/
|
|
const removeFromWorkerQueue = (watchKey) => {
|
|
state.workerQueue = state.workerQueue.filter((workerMessage) => {
|
|
const [, msg] = Object.entries(workerMessage)[0];
|
|
const workerMessageWatchKey = watchKeyFromMessage(msg);
|
|
|
|
return watchKey !== workerMessageWatchKey;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* These are things that we do when we get a message from the UI thread
|
|
*/
|
|
const workerActions = {
|
|
// ToDo: SM we'll make a generic loader for all resource types when we need it but it'll be pretty similar to this
|
|
loadSchemas: (collection) => {
|
|
if (!caches[SCHEMA]) {
|
|
caches[SCHEMA] = new ResourceCache(SCHEMA);
|
|
}
|
|
caches[SCHEMA].load(collection);
|
|
},
|
|
createWatcher: (opt) => {
|
|
trace('createWatcher', opt);
|
|
|
|
const {
|
|
metadata, maxTries, url, csrf
|
|
} = opt;
|
|
|
|
if (!state.watcher) {
|
|
state.watcher = new ResourceWatcher(url, true, null, null, maxTries, csrf);
|
|
|
|
state.watcher.addEventListener(EVENT_MESSAGE, (e) => {
|
|
const event = e.detail;
|
|
|
|
if (event.data) {
|
|
const msg = JSON.parse(event.data);
|
|
|
|
if (msg.name) {
|
|
if (resourceWatcherActions[msg.name]) {
|
|
resourceWatcherActions[msg.name](msg);
|
|
} else {
|
|
resourceWatcherActions.dispatch(msg);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
state.watcher.addEventListener('resync', (e) => {
|
|
self.postMessage({ redispatch: { resyncWatch: e.detail.data } });
|
|
});
|
|
|
|
state.watcher.addEventListener(EVENT_CONNECT_ERROR, (e) => {
|
|
handleConnectionError(EVENT_CONNECT_ERROR, e, state.watcher);
|
|
});
|
|
|
|
state.watcher.addEventListener(EVENT_DISCONNECT_ERROR, (e) => {
|
|
handleConnectionError(EVENT_DISCONNECT_ERROR, e, state.watcher);
|
|
});
|
|
|
|
state.watcher.setDebug(state.debugWorker);
|
|
|
|
state.watcher.connect(metadata);
|
|
|
|
// Flush the workerQueue
|
|
while (state.workerQueue.length > 0) {
|
|
trace('createWatcher', 'flushing workerQueue', state.workerQueue);
|
|
|
|
const workerMessage = state.workerQueue.shift();
|
|
const [action, msg] = Object.entries(workerMessage)[0];
|
|
|
|
if (workerActions[action]) {
|
|
workerActions[action](msg);
|
|
} else {
|
|
console.warn('no associated action for:', action); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
}
|
|
},
|
|
watch: (msg) => {
|
|
trace('watch', msg);
|
|
|
|
const watchKey = watchKeyFromMessage(msg);
|
|
|
|
if (msg.stop) {
|
|
workerActions.unwatch(watchKey);
|
|
|
|
return;
|
|
}
|
|
|
|
// If socket is in error don't try to watch.... unless we `force` it
|
|
if (!msg.force && !!state.watcher?.watches[watchKey]?.error) {
|
|
return;
|
|
}
|
|
|
|
if (!state.watcher) {
|
|
state.workerQueue.push({ watch: msg });
|
|
|
|
return;
|
|
}
|
|
|
|
const {
|
|
resourceType,
|
|
namespace,
|
|
id,
|
|
selector,
|
|
resourceVersion
|
|
} = msg;
|
|
|
|
const resourceVersionTime = resourceVersion ? Date.now() : undefined;
|
|
const skipResourceVersion = [SCHEMA, COUNT].includes(resourceType);
|
|
|
|
const watchObject = {
|
|
resourceType,
|
|
id,
|
|
namespace,
|
|
selector,
|
|
force: msg.force,
|
|
};
|
|
|
|
state.watcher.watch(watchKey, resourceVersion, resourceVersionTime, watchObject, skipResourceVersion);
|
|
},
|
|
unwatch: (watchKey) => {
|
|
trace('unwatch', watchKey);
|
|
|
|
removeFromWorkerQueue(watchKey);
|
|
|
|
if (!state.watcher) {
|
|
return;
|
|
}
|
|
|
|
state.watcher.unwatch(watchKey);
|
|
},
|
|
initWorker: ({ storeName }) => {
|
|
trace('initWorker', storeName);
|
|
|
|
state.store = storeName;
|
|
},
|
|
destroyWorker: () => {
|
|
trace('destroyWorker');
|
|
|
|
clearInterval(maintenanceInterval);
|
|
|
|
function destroyWorkerComplete() {
|
|
delete self.onmessage;
|
|
self.postMessage({ destroyWorker: true }); // we're only passing the boolean here because the key needs to be something truthy to ensure it's passed on the object.
|
|
}
|
|
|
|
// disconnect takes a callback which we'll use to close the webworker
|
|
if (state.watcher) {
|
|
state.watcher?.disconnect().then(destroyWorkerComplete);
|
|
} else {
|
|
destroyWorkerComplete();
|
|
}
|
|
},
|
|
|
|
toggleDebug: ({ on }) => {
|
|
state.debugWorker = !!on;
|
|
state.watcher.setDebug(!!on);
|
|
},
|
|
updateBatch(type, id, change) {
|
|
if (!state.batchChanges[type]) {
|
|
state.batchChanges[type] = {};
|
|
}
|
|
state.batchChanges[type][id] = change;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* These are things that we do when we get a message from the resourceWatcher
|
|
*/
|
|
const resourceWatcherActions = {
|
|
'resource.change': (msg) => {
|
|
const { type, id, data } = makeResourceProps(msg);
|
|
|
|
if (caches[type]) {
|
|
caches[type].change(data, () => workerActions.updateBatch(type, id, data));
|
|
} else {
|
|
workerActions.updateBatch(type, id, data);
|
|
}
|
|
},
|
|
// ToDo: SM create is functionally identical to change in the cache but the worker isn't supposed to know that hence the near-duplicate code
|
|
'resource.create': (msg) => {
|
|
const { type, id, data } = makeResourceProps(msg);
|
|
|
|
if (caches[type]) {
|
|
caches[type].create(data, () => workerActions.updateBatch(type, id, data));
|
|
} else {
|
|
workerActions.updateBatch(type, id, data);
|
|
}
|
|
},
|
|
'resource.start': (msg) => {
|
|
// State is handled in the resourceWatcher, no need to bubble out to UI thread
|
|
},
|
|
'resource.remove': (msg) => {
|
|
const { type, id } = makeResourceProps(msg);
|
|
|
|
if (caches[type]) {
|
|
caches[type].remove(id, () => workerActions.updateBatch(type, id, {}));
|
|
} else {
|
|
workerActions.updateBatch(type, id, {});
|
|
}
|
|
},
|
|
'resource.stop': (msg) => {
|
|
trace('resource.stop', msg);
|
|
|
|
// State is handled in the resourceWatcher....
|
|
const watchKey = watchKeyFromMessage(msg);
|
|
|
|
removeFromWorkerQueue(watchKey);
|
|
|
|
// ... however we still want to bubble out to UI thread
|
|
// We'll save some hassle and ignore any resource.stop bubble if we're in error. the only thing that will clear that is a resync
|
|
if (!state.watcher?.watches[watchKey]?.error) {
|
|
// See comment in resourceWatcher 'resource.stop' handler, until we can resolve the resourceVersion within the resourceWatcher
|
|
// internally, we'll want to bubble this out to the UI thread. When that's resolved this won't be needed
|
|
resourceWatcherActions.dispatch({
|
|
...msg,
|
|
advancedWorker: true,
|
|
});
|
|
}
|
|
},
|
|
'resource.error': (msg) => {
|
|
// State is handled in the resourceWatcher, no need to bubble out to UI thread
|
|
console.warn(`Resource error [${ state.store }]`, msg.resourceType, ':', msg.data.error); // eslint-disable-line no-console
|
|
},
|
|
dispatch: (msg) => {
|
|
self.postMessage({ dispatch: msg });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Covers message from UI Thread to Worker
|
|
*/
|
|
self.onmessage = (e) => {
|
|
/* on the off chance there's more than key in the message, we handle them in the order that they "keys" method provides which is
|
|
// good enough for now considering that we never send more than one message action at a time right now */
|
|
const messageActions = Object.keys(e?.data);
|
|
|
|
messageActions.forEach((action) => {
|
|
if (workerActions[action]) {
|
|
workerActions[action](e?.data[action]);
|
|
} else {
|
|
// This catches any window sendMessage event. We're hitting this on hot-reload of code where somehow this file is loaded
|
|
// Could be related to extensions, which have their own version of this
|
|
console.debug('no associated action for:', action); // eslint-disable-line no-console
|
|
}
|
|
});
|
|
}; // bind everything to the worker's onmessage handler via the workerActions
|