c3lf-system-3/web/src/shared-state-plugin/index.js

345 lines
13 KiB
JavaScript
Raw Normal View History

2024-06-22 23:04:32 +00:00
import {isProxy, toRaw} from 'vue';
export default (config) => {
if (!('isLoadedKey' in config)) {
throw new Error("isLoadedKey not defined in config");
}
if (('asyncFetch' in config) && !('lastfetched' in config)) {
throw new Error("asyncFetch defined but lastfetched not defined in config");
}
if (config.debug) console.log('plugin created');
const clone = (obj) => {
if (isProxy(obj)) {
obj = toRaw(obj);
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj.__proto__ === ({}).__proto__) {
return Object.assign({}, obj);
}
if (obj.__proto__ === [].__proto__) {
return obj.slice();
}
return obj;
}
const deepEqual = (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) {
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (let key in b) {
if (!(key in a)) {
return false;
}
}
for (let key in a) {
if (!(key in b)) {
return false;
}
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
return false;
}
const toRawRecursive = (obj) => {
if (isProxy(obj)) {
obj = toRaw(obj);
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj.__proto__ === ({}).__proto__) {
const new_obj = {};
for (let key in obj) {
new_obj[key] = toRawRecursive(obj[key]);
}
return new_obj;
}
if (obj.__proto__ === [].__proto__) {
return obj.map((item) => toRawRecursive(item));
}
return obj;
}
/** may only be called from worker */
const worker_fun = function (self, ctx) {
/* globals WebSocket, SharedWorker, onconnect, onmessage, postMessage, close, location */
let intialized = false;
let state = {};
let ports = [];
let notify_socket;
const tryConnect = () => {
if (self.WebSocket === undefined) {
if (ctx.debug) console.log("no websocket support");
return;
}
if (!notify_socket || notify_socket.readyState !== WebSocket.OPEN) {
// global location is not useful in worker loaded from data url
const scheme = ctx.location.protocol === "https:" ? "wss" : "ws";
if (ctx.debug) console.log("connecting to", scheme + '://' + ctx.location.host + '/ws/2/notify/');
notify_socket = new WebSocket(scheme + '://' + ctx.location.host + '/ws/2/notify/');
notify_socket.onopen = (e) => {
if (ctx.debug) console.log("open", JSON.stringify(e));
};
notify_socket.onclose = (e) => {
if (ctx.debug) console.log("close", JSON.stringify(e));
setTimeout(() => {
tryConnect();
}, 1000);
};
notify_socket.onerror = (e) => {
if (ctx.debug) console.log("error", JSON.stringify(e));
setTimeout(() => {
tryConnect();
}, 1000);
};
notify_socket.onmessage = (e) => {
let data = JSON.parse(e.data);
if (ctx.debug) console.log("message", data);
//this.loadEventItems()
//this.loadTickets()
}
}
}
const deepEqual = (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) {
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (let key in b) {
if (!(key in a)) {
return false;
}
}
for (let key in a) {
if (!(key in b)) {
return false;
}
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
return false;
}
const handle_message = (message_data, reply, others, all) => {
switch (message_data.type) {
case 'state_init':
if (!intialized) {
intialized = true;
state = message_data.state;
reply({type: 'state_init', first: true});
} else {
reply({type: 'state_init', first: false, state: state});
}
break;
case 'state_diff':
if (message_data.key in state) {
if (!deepEqual(state[message_data.key], message_data.old_value)) {
if (ctx.debug) console.log("state diff old value mismatch | state:", state[message_data.key], " old:", message_data.old_value);
}
if (!deepEqual(state[message_data.key], message_data.new_value)) {
if (ctx.debug) console.log("state diff changed | state:", state[message_data.key], " new:", message_data.new_value);
state[message_data.key] = message_data.new_value;
others(message_data);
} else {
if (ctx.debug) console.log("state diff no change | state:", state[message_data.key], " new:", message_data.new_value);
}
} else {
if (ctx.debug) console.log("state diff key not found", message_data.key);
}
break;
default:
if (ctx.debug) console.log("unknown message", message_data);
}
}
onconnect = (connect_event) => {
const port = connect_event.ports[0];
ports.push(port);
port.onmessage = (message_event) => {
const reply = (message_data) => {
port.postMessage(message_data);
}
const others = (message_data) => {
for (let i = 0; i < ports.length; i++) {
if (ports[i] !== port) {
ports[i].postMessage(message_data);
}
}
}
const all = (message_data) => {
for (let i = 0; i < ports.length; i++) {
ports[i].postMessage(message_data);
}
}
handle_message(message_event.data, reply, others, all);
}
port.start();
if (ctx.debug) console.log("worker connected", JSON.stringify(connect_event));
tryConnect();
}
if (ctx.debug) console.log("worker loaded");
}
const worker_context = {
location: {
protocol: location.protocol, host: location.host
}, bug: config.debug
}
const worker_code = '(' + worker_fun.toString() + ')(self,' + JSON.stringify(worker_context) + ')';
const worker_url = 'data:application/javascript;base64,' + btoa(worker_code);
const worker = new SharedWorker(worker_url, 'vuex-shared-state-plugin');
worker.port.start();
if (config.debug) console.log('worker started');
const updateWorkerState = (key, new_value, old_value = null) => {
if (new_value === old_value) {
if (config.debug) console.log('updateWorkerState: no change', key, new_value);
return;
}
if (new_value === undefined) {
if (config.debug) console.log('updateWorkerState: undefined', key, new_value);
return;
}
worker.port.postMessage({
type: 'state_diff',
key: key,
new_value: isProxy(new_value) ? toRawRecursive(new_value) : new_value,
old_value: isProxy(old_value) ? toRawRecursive(old_value) : old_value
});
}
const registerInitialState = (keys, local_state) => {
const value = keys.reduce((obj, key) => {
obj[key] = isProxy(local_state[key]) ? toRawRecursive(local_state[key]) : local_state[key];
return obj;
}, {});
if (config.debug) console.log('registerInitilState', value);
worker.port.postMessage({
type: 'state_init', state: value
});
}
return (store) => {
worker.port.onmessage = function (e) {
switch (e.data.type) {
case 'state_init':
if (config.debug) console.log('state_init', e.data);
if (e.data.first) {
if (config.debug) console.log('worker state initialized');
} else {
for (let key in e.data.state) {
if (key in store.state) {
if (config.debug) console.log('worker state init received', key, clone(e.data.state[key]));
if (!deepEqual(store.state[key], e.data.state[key])) {
store.state[key] = e.data.state[key];
}
} else {
if (config.debug) console.log("state init key not found", key);
}
}
}
store.state[config.isLoadedKey] = true;
if ('afterInit' in config) {
setTimeout(() => {
store.dispatch(config.afterInit);
}, 0);
}
break;
case 'state_diff':
if (config.debug) console.log('state_diff', e.data);
if (e.data.key in store.state) {
if (config.debug) console.log('worker state update', e.data.key, clone(e.data.new_value));
//TODO this triggers the watcher again, but we don't want that
store.state[e.data.key] = e.data.new_value;
} else {
if (config.debug) console.log("state diff key not found", e.data.key);
}
break;
default:
if (config.debug) console.log("unknown message", e.data);
}
};
registerInitialState(config.state, store.state);
if ('mutations' in config) {
store.subscribe((mutation, state) => {
if (mutation.type in config.mutations) {
console.log(mutation.type, mutation.payload);
console.log(state);
}
});
}
/*if ('actions' in config) {
store.subscribeAction((action, state) => {
if (action.type in config.actions) {
console.log(action.type, action.payload);
console.log(state);
}
});
}*/
if ('state' in config) {
config.watch.forEach((member) => {
store.watch((state, getters) => state[member], (newValue, oldValue) => {
if (config.debug) console.log('watch', member, clone(newValue), clone(oldValue));
updateWorkerState(member, newValue, oldValue);
});
});
}
};
}