mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
Implement pub-sub handler system for ws notifications.
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"leaflet-arrowheads": "^1.2.2",
|
"leaflet-arrowheads": "^1.2.2",
|
||||||
"leaflet-realtime": "^2.2.0",
|
"leaflet-realtime": "^2.2.0",
|
||||||
"leaflet.markercluster": "^1.4.1",
|
"leaflet.markercluster": "^1.4.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"marked": "^9.1.4",
|
"marked": "^9.1.4",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"plotly.js-dist": "^2.27.0",
|
"plotly.js-dist": "^2.27.0",
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
snackText () { return this.$store.state.snack.snackText },
|
snackText () { return this.$store.state.snack.snackText },
|
||||||
snackColour () { return this.$store.state.snack.snackColour },
|
snackColour () { return this.$store.state.snack.snackColour },
|
||||||
...mapGetters(["serverEvent"])
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -77,24 +76,45 @@ export default {
|
|||||||
this.$store.commit('setSnackText', "");
|
this.$store.commit('setSnackText', "");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async serverEvent (event) {
|
|
||||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
|
||||||
// Projects changed in some way or another
|
|
||||||
await this.refreshProjects();
|
|
||||||
} else if (event.channel == ".jwt" && event.payload?.token) {
|
|
||||||
await this.setCredentials({token: event.payload?.token});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
handleJWT (context, {payload}) {
|
||||||
|
this.setCredentials({token: payload.token});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleProject (context, {payload}) {
|
||||||
|
this.refreshProjects();
|
||||||
|
},
|
||||||
|
|
||||||
|
registerNotificationHandlers () {
|
||||||
|
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table: '.jwt',
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
this.handleJWT(context, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table: 'project',
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
this.handleProject(context, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
...mapActions(["setCredentials", "refreshProjects"])
|
...mapActions(["setCredentials", "refreshProjects"])
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
// Local Storage values are always strings
|
// Local Storage values are always strings
|
||||||
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
||||||
|
this.registerNotificationHandlers();
|
||||||
await this.setCredentials();
|
await this.setCredentials();
|
||||||
this.refreshProjects();
|
this.refreshProjects();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
function old_processServerEvent({state, getters, commit, dispatch}, [message]) {
|
||||||
|
//console.log("Processing server event", message);
|
||||||
|
|
||||||
|
function processPlan ({payload}) {
|
||||||
|
if (payload.operation == "INSERT") {
|
||||||
|
commit("setSequence", payload.new);
|
||||||
|
} else if (payload.operation == "UPDATE") {
|
||||||
|
commit("replaceSequence", [payload.old, payload.new]);
|
||||||
|
} else if (payload.operation == "DELETE") {
|
||||||
|
commit("deleteSequence", payload.old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("setServerEvent", message);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
console.warn("processServerEvent called without arguments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.channel) {
|
||||||
|
console.warn("processServerEvent message missing channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.payload) {
|
||||||
|
console.warn("processServerEvent message missing payload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.payload.operation == "INSERT") {
|
||||||
|
if (message.payload.new == null) {
|
||||||
|
console.warn("Expected payload.new to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.payload.operation == "UPDATE") {
|
||||||
|
if (message.payload.old == null || message.payload.new == null) {
|
||||||
|
console.warn("Expected payload.old and paylaod.new to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.payload.operation == "DELETE") {
|
||||||
|
if (message.payload.old == null) {
|
||||||
|
console.warn("Expected payload.old to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Unrecognised operation: ${message.payload.operation}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.channel == "planned_lines") {
|
||||||
|
// Process a change in the planner
|
||||||
|
processPlan(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function registerHandler({ commit }, { table, handler }) {
|
||||||
|
commit('REGISTER_HANDLER', { table, handler });
|
||||||
|
}
|
||||||
|
|
||||||
|
function processServerEvent({ commit, dispatch, state, rootState }, message) {
|
||||||
|
//console.log("processServerEvent", message);
|
||||||
|
// Error handling for invalid messages
|
||||||
|
if (!message) {
|
||||||
|
console.error("processServerEvent called without arguments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.channel) {
|
||||||
|
console.error("processServerEvent message missing channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.payload) {
|
||||||
|
console.error("processServerEvent message missing payload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.payload.operation == "INSERT") {
|
||||||
|
if (message.payload.new == null) {
|
||||||
|
console.error("Expected payload.new to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.payload.operation == "UPDATE") {
|
||||||
|
if (message.payload.old == null || message.payload.new == null) {
|
||||||
|
console.error("Expected payload.old and paylaod.new to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (message.payload.operation == "DELETE") {
|
||||||
|
if (message.payload.old == null) {
|
||||||
|
console.error("Expected payload.old to be non-null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Unrecognised operation: ${message.payload.operation}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = message.channel; // or message.payload?.table;
|
||||||
|
//console.log("table=", table);
|
||||||
|
if (!table || !state.handlers[table] || state.handlers[table].length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a debounced runner per table if not exists
|
||||||
|
if (!state.debouncedRunners) {
|
||||||
|
state.debouncedRunners = {}; // Not reactive needed? Or use Vue.set
|
||||||
|
}
|
||||||
|
if (!state.debouncedRunners[table]) {
|
||||||
|
const config = {
|
||||||
|
wait: 300, // min silence in ms
|
||||||
|
maxWait: 1000, // max wait before force run, adjustable
|
||||||
|
trailing: true,
|
||||||
|
leading: false
|
||||||
|
};
|
||||||
|
state.debouncedRunners[table] = debounce((lastMessage) => {
|
||||||
|
const context = { commit, dispatch, state: rootState, rootState }; // Approximate action context
|
||||||
|
state.handlers[table].forEach(handler => {
|
||||||
|
try {
|
||||||
|
//console.log("Trying handler:", handler);
|
||||||
|
handler(context, lastMessage);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error in handler for table ${table}:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, config.wait, { maxWait: config.maxWait });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the debounced function with the current message
|
||||||
|
// Debounce will use the last call's argument if multiple
|
||||||
|
state.debouncedRunners[table](message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { registerHandler, processServerEvent };
|
||||||
|
|||||||
@@ -11,4 +11,12 @@ function setServerConnectionState (state, isConnected) {
|
|||||||
state.serverConnected = !!isConnected;
|
state.serverConnected = !!isConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { setServerEvent, clearServerEvent, setServerConnectionState };
|
function REGISTER_HANDLER(state, { table, handler }) {
|
||||||
|
if (!state.handlers[table]) {
|
||||||
|
state.handlers[table] = [];
|
||||||
|
}
|
||||||
|
state.handlers[table].push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default { setServerEvent, clearServerEvent, setServerConnectionState, REGISTER_HANDLER };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const state = () => ({
|
const state = () => ({
|
||||||
serverEvent: null,
|
serverEvent: null,
|
||||||
serverConnected: false
|
serverConnected: false,
|
||||||
|
handlers: {}, // table: array of functions (each fn receives { commit, dispatch, state, rootState, message })
|
||||||
});
|
});
|
||||||
|
|
||||||
export default state;
|
export default state;
|
||||||
|
|||||||
@@ -43,41 +43,81 @@ export default {
|
|||||||
return this.loading || this.projectId;
|
return this.loading || this.projectId;
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapGetters(["loading", "projectId", "projectSchema", "serverEvent"])
|
...mapGetters(["loading", "projectId", "projectSchema"])
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
async serverEvent (event) {
|
|
||||||
if (event.channel == "project" && event.payload?.operation == "DELETE" && event.payload?.schema == "public") {
|
|
||||||
// Project potentially deleted
|
|
||||||
await this.getProject(this.$route.params.project);
|
|
||||||
} else if (event.payload?.schema == this.projectSchema) {
|
|
||||||
if (event.channel == "event") {
|
|
||||||
this.refreshEvents();
|
|
||||||
} else if (event.channel == "planned_lines") {
|
|
||||||
this.refreshPlan();
|
|
||||||
} else if (["raw_lines", "raw_shots", "final_lines", "final_shots"].includes(event.channel)) {
|
|
||||||
this.refreshSequences();
|
|
||||||
} else if (["preplot_lines", "preplot_points"].includes(event.channel)) {
|
|
||||||
this.refreshLines();
|
|
||||||
} else if (event.channel == "info") {
|
|
||||||
if ((event.payload?.new ?? event.payload?.old)?.key == "plan") {
|
|
||||||
this.refreshPlan();
|
|
||||||
}
|
|
||||||
} else if (event.channel == "project") {
|
|
||||||
this.getProject(this.$route.params.project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
handleLines (context, {payload}) {
|
||||||
|
if (payload.pid != this.projectId) {
|
||||||
|
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshLines();
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePlannedLines (context, {payload}) {
|
||||||
|
if (payload.pid != this.projectId) {
|
||||||
|
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshPlan();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSequences (context, {payload}) {
|
||||||
|
if (payload.pid != this.projectId) {
|
||||||
|
console.warn(`${this.projectId} ignoring notification for ${payload.pid}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("handleSequences");
|
||||||
|
this.refreshSequences();
|
||||||
|
},
|
||||||
|
|
||||||
|
registerNotificationHandlers () {
|
||||||
|
|
||||||
|
|
||||||
|
["preplot_lines", "preplot_points"].forEach( table => {
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table,
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
this.handleLines(context, message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table: 'planned_lines',
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
this.handlePlannedLines(context, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
["raw_lines", "raw_shots", "final_lines", "final_shots"].forEach( table => {
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table,
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
this.handleSequences(context, message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
...mapActions(["getProject", "refreshLines", "refreshSequences", "refreshEvents", "refreshLabels", "refreshPlan"])
|
...mapActions(["getProject", "refreshLines", "refreshSequences", "refreshEvents", "refreshLabels", "refreshPlan"])
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
await this.getProject(this.$route.params.project);
|
await this.getProject(this.$route.params.project);
|
||||||
if (this.projectFound) {
|
if (this.projectFound) {
|
||||||
|
this.registerNotificationHandlers();
|
||||||
|
|
||||||
this.refreshLines();
|
this.refreshLines();
|
||||||
this.refreshSequences();
|
this.refreshSequences();
|
||||||
this.refreshEvents();
|
this.refreshEvents();
|
||||||
|
|||||||
@@ -184,17 +184,7 @@ export default {
|
|||||||
: this.items.filter(i => !i.archived);
|
: this.items.filter(i => !i.archived);
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapGetters(['loading', 'serverEvent', 'projects'])
|
...mapGetters(['loading', 'projects'])
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
async serverEvent (event) {
|
|
||||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
|
||||||
if (event.payload?.operation == "DELETE" || event.payload?.operation == "INSERT") {
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -220,6 +210,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async load () {
|
async load () {
|
||||||
|
await this.refreshProjects();
|
||||||
await this.list();
|
await this.list();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const key in this.items) {
|
for (const key in this.items) {
|
||||||
@@ -234,6 +225,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
registerNotificationHandlers () {
|
||||||
|
this.$store.dispatch('registerHandler', {
|
||||||
|
table: 'project`',
|
||||||
|
|
||||||
|
handler: (context, message) => {
|
||||||
|
if (message.payload?.table == "public") {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
contextMenu (e, {item}) {
|
contextMenu (e, {item}) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.contextMenuShow = false;
|
this.contextMenuShow = false;
|
||||||
@@ -372,10 +375,11 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapActions(["api", "showSnack"])
|
...mapActions(["api", "showSnack", "refreshProjects"])
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.registerNotificationHandlers();
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user