Skip to content

Commit

Permalink
Added websocket support for tRPC and ported updated system over
Browse files Browse the repository at this point in the history
  • Loading branch information
6XGate committed Nov 2, 2024
1 parent d6250c7 commit 0730caf
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 129 deletions.
3 changes: 1 addition & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
- Switch the majority of the IPC using tRPC.
- Updater will require websocket for subscriptions.
- System will require moving the open and save file support to DOM APIs.
- Drivers will require an overhaul to no longer need handles.
- Wrap some Electron APIs as services for easier mocking without electron itself.
- More drivers.
- Move more modules to core.
- Wrap some Electron APIs as services for easier mocking without electron itself.
- Drivers
- Shinybow
- Monoprice Blackbird
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@types/pouchdb-find": "^7.3.3",
"@types/setimmediate": "^1.0.4",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"@vitejs/plugin-vue": "^5.1.4",
Expand All @@ -96,6 +97,7 @@
"assert": "^2.1.0",
"auto-bind": "^5.0.1",
"buffer": "^6.0.3",
"bufferutil": "^4.0.8",
"duplexify": "^4.1.3",
"electron": "^31.7.3",
"electron-builder": "^24.13.3",
Expand Down Expand Up @@ -134,6 +136,7 @@
"type-fest": "^4.26.1",
"typescript": "^5.6.3",
"typescript-eslint-parser-for-extra-files": "^0.7.0",
"utf-8-validate": "^6.0.5",
"util": "^0.12.5",
"uuid": "^11.0.2",
"vite": "^5.4.10",
Expand All @@ -147,6 +150,7 @@
"vue-router": "^4.4.5",
"vue-tsc": "^2.1.10",
"vuetify": "^3.7.3",
"ws": "^8.18.0",
"xdg-basedir": "^5.1.0",
"zod": "^3.23.8"
},
Expand Down
25 changes: 25 additions & 0 deletions src/core/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const protocol = ['http:', 'ws:'] as const
const support = protocol.join(',')
export type Protocol = (typeof protocol)[number]

function isSupportedProtocol(value: unknown): value is Protocol {
return protocol.includes(value as never)
}

export type ServerSettings = [host: string, port: number, protocol: Protocol]

export function getServerUrl(url: URL, defaultPort: number) {
if (!isSupportedProtocol(url.protocol)) throw new TypeError(`${url.protocol} is not supported; only ${support}`)
if (url.pathname.length > 1) throw new TypeError('Server must be at the root')
if (url.search.length > 0) throw new TypeError('Query parameters mean nothing')
if (url.hash.length > 0) throw new TypeError('Query parameters mean nothing')
if (url.username.length > 0) throw new TypeError('Username currently unsupported')
if (url.password.length > 0) throw new TypeError('Password currently unsupported')
if (url.hostname.length === 0) return ['127.0.0.1', defaultPort, url.protocol] satisfies ServerSettings
if (url.port.length === 0) return [url.hostname, defaultPort, url.protocol] satisfies ServerSettings

const port = Number(url.port)
if (Number.isNaN(port)) throw new TypeError(`${url.port} is not a valid port`)

return [url.hostname, port, url.protocol] satisfies ServerSettings
}
1 change: 0 additions & 1 deletion src/main/dao/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { memo } from 'radash'
import { useLevelDb } from '../services/level'

export type UserStore = ReturnType<typeof useUserStore>
const useUserStore = memo(function useUserStore() {
const { levelup } = useLevelDb()

Expand Down
2 changes: 1 addition & 1 deletion src/main/info/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ReadonlyDeep } from 'type-fest'
export type AppConfig = ReadonlyDeep<ReturnType<typeof useAppConfig>>
const useAppConfig = memo(function useAppConfig() {
const config = {
rpcUrl: 'http://127.0.0.1:7180'
rpcUrl: 'ws://127.0.0.1:7180'
}

process.env['rpc_url_'] = config.rpcUrl
Expand Down
2 changes: 0 additions & 2 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import useAppConfig from './info/config'
import useCrypto from './plugins/webcrypto'
import useApiServer from './server'
import useSystem from './services/system'
import useUpdater from './services/updater'
import { logError } from './utilities'
import { waitTill } from '@/basics'
import { toError } from '@/error-handling'
Expand Down Expand Up @@ -131,7 +130,6 @@ useAppConfig()

useApiServer()
useCrypto()
useUpdater()
useSystem()

await createWindow()
4 changes: 3 additions & 1 deletion src/main/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useTiesRouter from './data/ties'
import useDriversRouter from './drivers'
import useSerialPortRouter from './ports'
import useStartupRouter from './startup'
import useUpdaterRouter from './updater'

export const useAppRouter = memo(() =>
router({
Expand All @@ -23,7 +24,8 @@ export const useAppRouter = memo(() =>
storage: useUserStoreRouter(),
ties: useTiesRouter(),
switches: useSwitchesRouter(),
sources: useSourcesRouter()
sources: useSourcesRouter(),
updates: useUpdaterRouter()
})
)

Expand Down
46 changes: 46 additions & 0 deletions src/main/routes/updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { observable } from '@trpc/server/observable'
import { memo } from 'radash'
import { procedure, router } from '../services/trpc'
import useUpdater from '../services/updater'
import type { AppUpdaterEventMap } from '../services/updater'

const useUpdaterRouter = memo(function useUpdaterRouter() {
const updater = useUpdater()

function defineEvent<Name extends keyof AppUpdaterEventMap>(name: Name) {
type Args = AppUpdaterEventMap[Name]
// TODO: tRPC 11, use "for await...of on" with AbortSignal
return () =>
observable<Args>((emit) => {
const proxy = (...args: Args) => {
emit.next(args)
}

updater.on(name, proxy as never)

return () => {
updater.off(name, proxy as never)
}
})
}

return router({
onChecking: procedure.subscription(defineEvent('checking')),
onAvailable: procedure.subscription(defineEvent('available')),
onProgress: procedure.subscription(defineEvent('progress')),
onDownloaded: procedure.subscription(defineEvent('downloaded')),
onCancelled: procedure.subscription(defineEvent('cancelled')),
checkForUpdates: procedure.query(async () => await updater.checkForUpdates()),
downloadUpdate: procedure.query(async () => {
await updater.downloadUpdate()
}),
cancelUpdate: procedure.query(async () => {
await updater.cancelUpdate()
}),
installUpdate: procedure.query(async () => {
await updater.installUpdate()
})
})
})

export default useUpdaterRouter
67 changes: 43 additions & 24 deletions src/main/server.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,61 @@
import { createHTTPServer } from '@trpc/server/adapters/standalone'
import { applyWSSHandler } from '@trpc/server/adapters/ws'
import Logger from 'electron-log'
import { WebSocketServer } from 'ws'
import useAppConfig from './info/config'
import { useAppRouter } from './routes/router'
import { getServerUrl } from '@/url'

function getServerUrl(url: URL): [host: string, port: number] {
Logger.log(url.pathname)
if (url.protocol !== 'http:') throw new TypeError('Only HTTP is supported')
if (url.pathname.length > 1) throw new TypeError('Server must be at the root')
if (url.search.length > 0) throw new TypeError('Query parameters mean nothing')
if (url.hash.length > 0) throw new TypeError('Query parameters mean nothing')
if (url.username.length > 0) throw new TypeError('Username currently unsupported')
if (url.password.length > 0) throw new TypeError('Password currently unsupported')
if (url.hostname.length === 0) return ['127.0.0.1', 7180]
if (url.port.length === 0) return [url.hostname, 7180]

const port = Number(url.port)
if (Number.isNaN(port)) throw new TypeError(`${url.port} is not a valid port`)

return [url.hostname, port]
}
function startWebSocketServer(url: URL, host: string, port: number) {
// TODO: Authentication via the IPC, later we'll implement a proper authentication model.

export default function useApiServer() {
const config = useAppConfig()
const url = new URL(config.rpcUrl)
const [host, port] = getServerUrl(url)
process.env['WS_NO_UTF_8_VALIDATE'] = '1'

const wss = new WebSocketServer({ host, port })

wss.on('listening', () => {
Logger.info(`RPC server at ${url}`)
})

const handler = applyWSSHandler({ wss, router: useAppRouter() })

process.on('exit', () => {
handler.broadcastReconnectNotification()
wss.close()
})

process.on('SIGTERM', () => {
handler.broadcastReconnectNotification()
wss.close()
})
}

function startHttpServer(url: URL, host: string, port: number) {
// TODO: Authentication via the IPC, later we'll implement a proper authentication model.

const httpServer = createHTTPServer({
const server = createHTTPServer({
router: useAppRouter()
})

httpServer.listen(port, host)
httpServer.server.on('listening', () => {
server.server.on('listening', () => {
Logger.info(`RPC server at ${url}`)
})

server.listen(port, host)
process.on('exit', () => {
httpServer.server.close()
server.server.close()
})

process.on('SIGTERM', () => {
server.server.close()
})
}

export default function useApiServer() {
const config = useAppConfig()
const url = new URL(config.rpcUrl)
const [host, port, protocol] = getServerUrl(url, 7180)

if (protocol === 'http:') startHttpServer(url, host, port)
if (protocol === 'ws:') startWebSocketServer(url, host, port)
}
Loading

0 comments on commit 0730caf

Please sign in to comment.