Add WIP initial multi-user websocket implementation for sheets

This commit is contained in:
Andrew Kingston 2023-03-05 18:57:05 +00:00
parent 01867f5736
commit 36c953443f
19 changed files with 431 additions and 122 deletions

View File

@ -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 @@
<div class="app-row-actions">
<AppLockModal {app} buttonSize="M" />
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
<Button size="S" primary disabled={app.lockedOther} on:click={goToBuilder}>
Edit
</Button>
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
</div>
</div>

View File

@ -89,6 +89,7 @@
}
onMount(async () => {
return
if (!hasSynced && application) {
try {
await API.syncApp(application)

View File

@ -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"

View File

@ -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"
}
}

View File

@ -1,5 +1,5 @@
<script>
import { setContext, createEventDispatcher } from "svelte"
import { setContext, createEventDispatcher, onMount } from "svelte"
import { writable } from "svelte/store"
import { createReorderStores } from "./stores/reorder"
import { createViewportStores } from "./stores/viewport"
@ -15,6 +15,7 @@
import { createAPIClient } from "../../api"
import ScrollOverlay from "./ScrollOverlay.svelte"
import StickyColumn from "./StickyColumn.svelte"
import { createWebsocket } from "./websocket"
export let API
export let tableId
@ -62,6 +63,11 @@
// Set context for children to consume
setContext("spreadsheet", context)
// Initialise websocket for multi-user
onMount(() => {
return createWebsocket(context)
})
</script>
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">

View File

@ -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,

View File

@ -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()
}

View File

@ -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==

View File

@ -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",

View File

@ -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
}

View File

@ -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<any> {
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

View File

@ -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

View File

@ -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) {

View File

@ -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 }

View File

@ -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)])
}
}

View File

@ -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}`)
})
})
}
}

View File

@ -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 }

View File

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

View File

@ -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"