345 lines
No EOL
13 KiB
JavaScript
345 lines
No EOL
13 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
} |