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); }); }); } }; }