From 36c953443f9da1bd47d9785fdced13f95bc7c339 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 5 Mar 2023 18:57:05 +0000 Subject: [PATCH] Add WIP initial multi-user websocket implementation for sheets --- .../src/components/start/AppRow.svelte | 16 +-- .../builder/app/[application]/_layout.svelte | 1 + packages/builder/yarn.lock | 51 +++++++- packages/frontend-core/package.json | 1 + .../src/components/sheet/Sheet.svelte | 8 +- .../src/components/sheet/stores/rows.js | 121 ++++++++++-------- .../src/components/sheet/websocket.js | 57 +++++++++ packages/frontend-core/yarn.lock | 61 +++++++++ packages/server/package.json | 3 +- .../src/api/controllers/plugin/index.ts | 6 +- .../server/src/api/controllers/row/index.ts | 11 ++ packages/server/src/app.ts | 4 +- packages/server/src/middleware/builder.ts | 8 +- packages/server/src/websocket.ts | 26 ---- packages/server/src/websockets/client.ts | 11 ++ packages/server/src/websockets/dataspace.ts | 29 +++++ packages/server/src/websockets/index.ts | 14 ++ packages/server/src/websockets/websocket.ts | 73 +++++++++++ packages/server/yarn.lock | 52 ++++---- 19 files changed, 431 insertions(+), 122 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/websocket.js delete mode 100644 packages/server/src/websocket.ts create mode 100644 packages/server/src/websockets/client.ts create mode 100644 packages/server/src/websockets/dataspace.ts create mode 100644 packages/server/src/websockets/index.ts create mode 100644 packages/server/src/websockets/websocket.ts diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte index 96c0e2154a..c6fb0b9531 100644 --- a/packages/builder/src/components/start/AppRow.svelte +++ b/packages/builder/src/components/start/AppRow.svelte @@ -15,12 +15,12 @@ } const goToBuilder = () => { - if (app.lockedOther) { - notifications.error( - `App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.` - ) - return - } + // if (app.lockedOther) { + // notifications.error( + // `App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.` + // ) + // return + // } $goto(`../../app/${app.devId}`) } @@ -59,9 +59,7 @@
- +
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index f561bd8ecd..c5c35bb3fd 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -89,6 +89,7 @@ } onMount(async () => { + return if (!hasSynced && application) { try { await API.syncApp(application) diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 7035ca1765..56bfd6ee8c 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -1467,6 +1467,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@spectrum-css/accordion@^3.0.24": version "3.0.30" resolved "https://registry.yarnpkg.com/@spectrum-css/accordion/-/accordion-3.0.30.tgz#0893a6db28bab984bf5adaf7e1ba194e741db615" @@ -2581,7 +2586,7 @@ dayjs@^1.10.4, dayjs@^1.11.2: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2774,6 +2779,22 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -5896,6 +5917,24 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -6713,6 +6752,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -6723,6 +6767,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 927f40c568..002d7814dc 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -8,6 +8,7 @@ "dependencies": { "@budibase/bbui": "2.3.18-alpha.15", "lodash": "^4.17.21", + "socket.io-client": "^4.6.1", "svelte": "^3.46.2" } } diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index c9fcb09f0d..ee8ed020b1 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -1,5 +1,5 @@
diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 4e05e1f525..0b7bd01368 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -72,31 +72,6 @@ export const createRowsStore = context => { }) }) - // Local handler to process new rows inside the fetch, and append any new - // rows to state that we haven't encountered before - const handleNewRows = newRows => { - let rowsToAppend = [] - let newRow - for (let i = 0; i < newRows.length; i++) { - newRow = newRows[i] - if (!rowCacheMap[newRow._id]) { - rowCacheMap[newRow._id] = true - rowsToAppend.push(newRow) - } - } - if (rowsToAppend.length) { - rows.update($rows => { - return [ - ...$rows, - ...rowsToAppend.map((row, idx) => ({ - ...row, - __idx: $rows.length + idx, - })), - ] - }) - } - } - // Adds a new empty row const addRow = async () => { try { @@ -127,6 +102,43 @@ export const createRowsStore = context => { } } + // Refreshes a specific row, handling updates, addition or deletion + const refreshRow = async id => { + // Get index of row to check if it exists + const $rows = get(rows) + const index = $rows.findIndex(row => row._id === id) + + // Fetch row from the server again + const res = await API.searchTable({ + tableId: get(tableId), + limit: 1, + query: { + equal: { + _id: id, + }, + }, + paginate: false, + }) + let newRow = res?.rows?.[0] + + // Process as either an update, addition or deletion + if (newRow) { + if (index !== -1) { + // An existing row was updated + rows.update(state => { + state[index] = { ...newRow, __idx: index } + return state + }) + } else { + // A new row was created + handleNewRows([newRow]) + } + } else if (index !== -1) { + // A row was removed + handleRemoveRows([$rows[index]]) + } + } + // Updates a value of a row const updateRow = async (rowId, column, value) => { const $rows = get(rows) @@ -151,35 +163,11 @@ export const createRowsStore = context => { notifications.error(`Error saving row: ${error?.message}`) } - // Fetch row from the server again - const res = await API.searchTable({ - tableId: get(tableId), - limit: 1, - query: { - equal: { - _id: row._id, - }, - }, - paginate: false, - }) - if (res?.rows?.[0]) { - newRow = res.rows[0] - } - - // Update state again with this row - newRow = { ...newRow, __idx: row.__idx } - rows.update(state => { - state[index] = newRow - return state - }) - - return newRow + return await refreshRow(row._id) } // Deletes an array of rows const deleteRows = async rowsToDelete => { - const deletedIds = rowsToDelete.map(row => row._id) - // Actually delete rows rowsToDelete.forEach(row => { delete row.__idx @@ -190,6 +178,38 @@ export const createRowsStore = context => { }) // Update state + handleRemoveRows(rowsToDelete) + } + + // Local handler to process new rows inside the fetch, and append any new + // rows to state that we haven't encountered before + const handleNewRows = newRows => { + let rowsToAppend = [] + let newRow + for (let i = 0; i < newRows.length; i++) { + newRow = newRows[i] + if (!rowCacheMap[newRow._id]) { + rowCacheMap[newRow._id] = true + rowsToAppend.push(newRow) + } + } + if (rowsToAppend.length) { + rows.update($rows => { + return [ + ...$rows, + ...rowsToAppend.map((row, idx) => ({ + ...row, + __idx: $rows.length + idx, + })), + ] + }) + } + } + + // Local handler to remove rows from state + const handleRemoveRows = rowsToRemove => { + const deletedIds = rowsToRemove.map(row => row._id) + // We deliberately do not remove IDs from the cache map as the data may // still exist inside the fetch, but we don't want to add it again rows.update(state => { @@ -217,6 +237,7 @@ export const createRowsStore = context => { updateRow, deleteRows, loadNextPage, + refreshRow, }, }, schema, diff --git a/packages/frontend-core/src/components/sheet/websocket.js b/packages/frontend-core/src/components/sheet/websocket.js new file mode 100644 index 0000000000..254619b36d --- /dev/null +++ b/packages/frontend-core/src/components/sheet/websocket.js @@ -0,0 +1,57 @@ +import { derived, get } from "svelte/store" +import { io } from "socket.io-client" + +export const createWebsocket = context => { + const { rows, config } = context + const tableId = derived(config, $config => $config.tableId) + + // Determine connection info + const tls = location.protocol === "https:" + const proto = tls ? "wss:" : "ws:" + const host = location.hostname + const port = location.port || (tls ? 443 : 80) + const socket = io(`${proto}//${host}:${port}`, { + path: "/socket/dataspace", + // Cap reconnection attempts to 3 (total of 15 seconds before giving up) + reconnectionAttempts: 3, + // Delay reconnection attempt by 5 seconds + reconnectionDelay: 5000, + reconnectionDelayMax: 5000, + // Timeout after 4 seconds so we never stack requests + timeout: 4000, + }) + + const connectToDataspace = tableId => { + if (!socket.connected) { + return + } + console.log("Idenifying dataspace", tableId) + + // Identify which dataspace we are editing + socket.emit("identify", tableId, response => { + // handle initial connection info + console.log("response", response) + }) + } + + // Event handlers + socket.on("connect", () => { + connectToDataspace(get(tableId)) + }) + + socket.on("row-update", data => { + if (data.id) { + rows.actions.refreshRow(data.id) + } + console.log(data) + }) + + socket.on("connect_error", err => { + console.log("Failed to connect to websocket:", err.message) + }) + + // Change websocket connection when dataspace changes + tableId.subscribe(connectToDataspace) + + return () => socket?.disconnect() +} diff --git a/packages/frontend-core/yarn.lock b/packages/frontend-core/yarn.lock index 7a2e6b7ba4..65f615d919 100644 --- a/packages/frontend-core/yarn.lock +++ b/packages/frontend-core/yarn.lock @@ -2,12 +2,73 @@ # yarn lockfile v1 +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + +debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +socket.io-client@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + svelte@^3.46.2: version "3.49.0" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029" integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA== + +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== diff --git a/packages/server/package.json b/packages/server/package.json index 39210f7d4a..3aadfb3702 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -62,6 +62,7 @@ "bull": "4.10.1", "chmodr": "1.2.0", "chokidar": "3.5.3", + "cookies": "0.8.0", "csvtojson": "2.0.10", "curlconverter": "3.21.0", "dd-trace": "3.13.2", @@ -107,7 +108,7 @@ "redis": "4", "server-destroy": "1.0.1", "snowflake-promise": "^4.5.0", - "socket.io": "^4.5.1", + "socket.io": "4.6.1", "svelte": "3.49.0", "swagger-parser": "10.0.3", "tar": "6.1.11", diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index faecbc1fd8..76411ddf48 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -7,7 +7,7 @@ import { } from "@budibase/backend-core" import { PluginType, FileType, PluginSource, Plugin } from "@budibase/types" import env from "../../../environment" -import { ClientAppSocket } from "../../../websocket" +import { clientAppSocket } from "../../../websockets" import { sdk as pro } from "@budibase/pro" export async function getPlugins(type?: PluginType) { @@ -91,7 +91,7 @@ export async function create(ctx: any) { const doc = await pro.plugins.storePlugin(metadata, directory, source) - ClientAppSocket.emit("plugins-update", { name, hash: doc.hash }) + clientAppSocket.emit("plugins-update", { name, hash: doc.hash }) ctx.body = { message: "Plugin uploaded successfully", plugins: [doc], @@ -133,6 +133,6 @@ export async function processUploadedPlugin( } const doc = await pro.plugins.storePlugin(metadata, directory, source) - ClientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash }) + clientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash }) return doc } diff --git a/packages/server/src/api/controllers/row/index.ts b/packages/server/src/api/controllers/row/index.ts index b59f245098..fa10b63522 100644 --- a/packages/server/src/api/controllers/row/index.ts +++ b/packages/server/src/api/controllers/row/index.ts @@ -2,6 +2,7 @@ import { quotas } from "@budibase/pro" import * as internal from "./internal" import * as external from "./external" import { isExternalTable } from "../../../integrations/utils" +import { dataspaceSocket } from "../../../websockets" function pickApi(tableId: any) { if (isExternalTable(tableId)) { @@ -45,6 +46,9 @@ export async function patch(ctx: any): Promise { ctx.eventEmitter.emitRow(`row:update`, appId, row, table) ctx.message = `${table.name} updated successfully.` ctx.body = row + + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } catch (err) { ctx.throw(400, err) } @@ -67,6 +71,9 @@ export const save = async (ctx: any) => { ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) ctx.message = `${table.name} saved successfully` ctx.body = row + + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } export async function fetchView(ctx: any) { const tableId = getTableId(ctx) @@ -105,6 +112,8 @@ export async function destroy(ctx: any) { response = rows for (let row of rows) { ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } } else { let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), { @@ -114,6 +123,8 @@ export async function destroy(ctx: any) { response = resp.response row = resp.row ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) + // Notify websocket change + dataspaceSocket.emit("row-update", { id: row._id }) } ctx.status = 200 // for automations include the row that was deleted diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 00f2aca7fc..18265eaa26 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -28,7 +28,7 @@ import * as automations from "./automations" import { Thread } from "./threads" import * as redis from "./utilities/redis" import { events, logging, middleware } from "@budibase/backend-core" -import { initialise as initialiseWebsockets } from "./websocket" +import { initialise as initialiseWebsockets } from "./websockets" import { startup } from "./startup" const Sentry = require("@sentry/node") const destroyable = require("server-destroy") @@ -72,7 +72,7 @@ if (env.isProd()) { const server = http.createServer(app.callback()) destroyable(server) -initialiseWebsockets(server) +initialiseWebsockets(app, server) let shuttingDown = false, errCode = 0 diff --git a/packages/server/src/middleware/builder.ts b/packages/server/src/middleware/builder.ts index 5174f618a0..ad89d0c658 100644 --- a/packages/server/src/middleware/builder.ts +++ b/packages/server/src/middleware/builder.ts @@ -35,12 +35,12 @@ async function checkDevAppLocks(ctx: BBContext) { if (!appId || !appId.startsWith(APP_DEV_PREFIX)) { return } - if (!(await doesUserHaveLock(appId, ctx.user))) { - ctx.throw(400, "User does not hold app lock.") - } + // if (!(await doesUserHaveLock(appId, ctx.user))) { + // ctx.throw(400, "User does not hold app lock.") + // } // they do have lock, update it - await updateLock(appId, ctx.user) + // await updateLock(appId, ctx.user) } async function updateAppUpdatedAt(ctx: BBContext) { diff --git a/packages/server/src/websocket.ts b/packages/server/src/websocket.ts deleted file mode 100644 index d6d91b0ca0..0000000000 --- a/packages/server/src/websocket.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Server } from "socket.io" -import http from "http" - -class Websocket { - socketServer: Server - - constructor(server: http.Server, path: string) { - this.socketServer = new Server(server, { - path, - }) - } - - // Emit an event to all sockets - emit(event: string, payload: any) { - this.socketServer.sockets.emit(event, payload) - } -} - -// Likely to be more socket instances in future -let ClientAppSocket: Websocket - -export const initialise = (server: http.Server) => { - ClientAppSocket = new Websocket(server, "/socket/client") -} - -export { ClientAppSocket } diff --git a/packages/server/src/websockets/client.ts b/packages/server/src/websockets/client.ts new file mode 100644 index 0000000000..d59325f66e --- /dev/null +++ b/packages/server/src/websockets/client.ts @@ -0,0 +1,11 @@ +import Socket from "./websocket" +import authorized from "../middleware/authorized" +import http from "http" +import Koa from "koa" +import { permissions } from "@budibase/backend-core" + +export default class ClientAppWebsocket extends Socket { + constructor(app: Koa, server: http.Server) { + super(app, server, "/socket/client", [authorized(permissions.BUILDER)]) + } +} diff --git a/packages/server/src/websockets/dataspace.ts b/packages/server/src/websockets/dataspace.ts new file mode 100644 index 0000000000..e5cd73b8f4 --- /dev/null +++ b/packages/server/src/websockets/dataspace.ts @@ -0,0 +1,29 @@ +import authorized from "../middleware/authorized" +import Socket from "./websocket" +import { permissions } from "@budibase/backend-core" +import http from "http" +import Koa from "koa" + +export default class DataspaceSocket extends Socket { + constructor(app: Koa, server: http.Server) { + super(app, server, "/socket/dataspace", [authorized(permissions.BUILDER)]) + + this.io.on("connection", socket => { + const user = socket.data.user + console.log(`Dataspace user connected: ${user?._id}`) + + // Initial identification of conneted dataspace + socket.on("identify", async (tableId, callback) => { + socket.join(tableId) + + const sockets = await this.io.in(tableId).fetchSockets() + callback(sockets.map(socket => socket.data.user)) + }) + + // Disconnection cleanup + socket.on("disconnect", reason => { + console.log(`Disconnecting ${user.email} because of ${reason}`) + }) + }) + } +} diff --git a/packages/server/src/websockets/index.ts b/packages/server/src/websockets/index.ts new file mode 100644 index 0000000000..551044ef8e --- /dev/null +++ b/packages/server/src/websockets/index.ts @@ -0,0 +1,14 @@ +import http from "http" +import Koa from "koa" +import DataspaceSocket from "./dataspace" +import ClientAppSocket from "./client" + +let clientAppSocket: ClientAppSocket +let dataspaceSocket: DataspaceSocket + +export const initialise = (app: Koa, server: http.Server) => { + clientAppSocket = new ClientAppSocket(app, server) + dataspaceSocket = new DataspaceSocket(app, server) +} + +export { clientAppSocket, dataspaceSocket } diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts new file mode 100644 index 0000000000..085d583f48 --- /dev/null +++ b/packages/server/src/websockets/websocket.ts @@ -0,0 +1,73 @@ +import { Server } from "socket.io" +import http from "http" +import Koa from "koa" +import Cookies from "cookies" +import { userAgent } from "koa-useragent" +import { auth } from "@budibase/backend-core" + +export default class Socket { + io: Server + + constructor( + app: Koa, + server: http.Server, + path: string, + additionalMiddlewares?: any[] + ) { + this.io = new Server(server, { + path, + }) + + // Attach default middlewares + const authenticate = auth.buildAuthMiddleware([], { + publicAllowed: true, + }) + const middlewares = [ + userAgent, + authenticate, + ...(additionalMiddlewares || []), + ] + + // Apply middlewares + this.io.use(async (socket, next) => { + // Build fake koa context + const res = new http.ServerResponse(socket.request) + const ctx: any = { + ...app.createContext(socket.request, res), + + // Additional overrides needed to make our middlewares work with this + // fake koa context + cookies: new Cookies(socket.request, res), + get: (field: string) => socket.request.headers[field], + throw: (code: number, message: string) => { + throw new Error(message) + }, + + // Needed for koa-useragent middleware + header: socket.request.headers, + } + + // Run all koa middlewares + try { + for (let [idx, middleware] of middlewares.entries()) { + await middleware(ctx, () => { + if (idx === middlewares.length - 1) { + // Middlewares are finished. + // Extract some data from our enriched koa context to persist + // as metadata for the socket + socket.data.user = ctx.user + next() + } + }) + } + } catch (error: any) { + next(error) + } + }) + } + + // Emit an event to all sockets + emit(event: string, payload: any) { + this.io.sockets.emit(event, payload) + } +} diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 465b849997..477d649f28 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -5647,7 +5647,7 @@ cookiejar@^2.1.3: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== -cookies@~0.8.0: +cookies@0.8.0, cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== @@ -6462,10 +6462,10 @@ engine.io-parser@~5.0.3: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== -engine.io@~6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" - integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== +engine.io@~6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.1.tgz#8056b4526a88e779f9c280d820422d4e3eeaaae5" + integrity sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -6476,7 +6476,7 @@ engine.io@~6.2.0: cors "~2.8.5" debug "~4.3.1" engine.io-parser "~5.0.3" - ws "~8.2.3" + ws "~8.11.0" enhanced-resolve@^5.9.3: version "5.9.3" @@ -13943,30 +13943,32 @@ snowflake-sdk@^1.6.0: uuid "^3.3.2" winston "^3.1.0" -socket.io-adapter@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" - integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" -socket.io-parser@~4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5" - integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g== +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.5.1: - version "4.5.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.2.tgz#1eb25fd380ab3d63470aa8279f8e48d922d443ac" - integrity sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ== +socket.io@4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70" + integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== dependencies: accepts "~1.3.4" base64id "~2.0.0" debug "~4.3.2" - engine.io "~6.2.0" - socket.io-adapter "~2.4.0" - socket.io-parser "~4.2.0" + engine.io "~6.4.1" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.1" socks@^2.7.0: version "2.7.0" @@ -15936,10 +15938,10 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@~8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== x3-linkedlist@1.2.0: version "1.2.0"