Skip to content

Commit

Permalink
Refactor switch to device internally (#96)
Browse files Browse the repository at this point in the history
- Renamed most switch references to device where possible
- Added migration system
- Added migration testing and fixes
- Removed Base64, since it started failing.
- Reduced logger noisiness
  • Loading branch information
6XGate authored Nov 22, 2024
1 parent 6313c6a commit af78c51
Show file tree
Hide file tree
Showing 61 changed files with 1,251 additions and 406 deletions.
15 changes: 14 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
{
"i18n-ally.localesPaths": ["src/renderer/locales"],
"vitest.disableWorkspaceWarning": true,
"i18n-ally.keystyle": "nested"
"i18n-ally.keystyle": "nested",
"cSpell.words": [
"bridgecmdr",
"extron",
"fgpa",
"pinia",
"radash",
"shinybow",
"sindresorhus",
"sixxgate",
"snes",
"vuelidate",
"vuetify"
]
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@
"execa": "^9.5.1",
"husky": "^9.1.6",
"ini": "^5.0.0",
"js-base64": "^3.7.7",
"levelup": "^5.1.1",
"mime": "^4.0.4",
"npm-check-updates": "^17.1.10",
Expand Down
15 changes: 15 additions & 0 deletions src/core/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** Converts a byte array into a Base64 string. */
export function toBase64(data: Uint8Array) {
return btoa(String.fromCodePoint(...data))
}

function* codePointsOf(data: string) {
for (let i = 0, cp = data.codePointAt(i); cp != null; ++i, cp = data.codePointAt(i)) {
yield cp
}
}

/** Coverts a Base64 string into a byte array. */
export function fromBase64(data: string) {
return new Uint8Array([...codePointsOf(atob(data))])
}
76 changes: 59 additions & 17 deletions src/core/rpc/transformer.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,78 @@
import { Base64 } from 'js-base64'
import { SuperJSON } from 'superjson'
import { Attachment } from '../attachments'
import { raiseError } from '../error-handling'
import { fromBase64, toBase64 } from '@/base64'

export default function useSuperJson() {
SuperJSON.registerCustom(
function useSuperJsonCommon() {
const superJson = new SuperJSON()

// HACK: tRPC and SuperJSON won't serialize functions; but
// doesn't filter them out, so the IPC throws an error
// if they are passed. They are also not passable via
// HTTP JSON serialization. This will ensure any
// types with functions won't be usable with
// routes, as functions will trigger an
// Error with this transformation.
superJson.registerCustom(
{
isApplicable: (v): v is () => undefined => typeof v === 'function',
serialize: (_: () => undefined) => raiseError(() => new TypeError('Functions may not be serialized')),
deserialize: (_: never) => raiseError(() => new TypeError('Functions may not be serialized'))
},
'Function'
)

return superJson
}

/**
* Sets up SuperJSON for use over Electron IPC.
*
* This will attempt to translate data into a form compatible with the limited
* structuralClone ability of Electron's IPC channels. This means most
* builtin objects and a very limited set of host objects may be
* passed directly through without translation.
*/
export function useIpcJson() {
const superJson = useSuperJsonCommon()
superJson.registerCustom(
{
isApplicable: (v) => v instanceof Attachment,
serialize: (attachment) => ({
name: attachment.name,
type: attachment.type,
data: Base64.fromUint8Array(attachment)
data: new Uint8Array(attachment.buffer) as never
}),
deserialize: (attachment) =>
new Attachment(attachment.name, attachment.type, Base64.toUint8Array(attachment.data))
deserialize: (attachment) => new Attachment(attachment.name, attachment.type, attachment.data)
},
'Attachment'
)

// HACK: tRPC won't serialize functions; but doesn't filter
// them out, so IPC throws an error if they are passed.
// This can also ensure any types with functions
// won't be returned as seen in the return
// type information for the router.
SuperJSON.registerCustom(
return superJson
}

/**
* Sets up SuperJSON for use over HTTP.
*
* This will attempt to translate data into a form compatible with normal JSON.
* This means only data allowed in regular JSON may be passed, other types
* of data must be translated into the best possible representation.
* Binary data will need to be Base64 encoded.
*/
export function useWebJson() {
const superJson = useSuperJsonCommon()
superJson.registerCustom(
{
isApplicable: (v): v is () => undefined => typeof v === 'function',
serialize: (_: () => undefined) => raiseError(() => new TypeError('Functions may not be serialized')),
deserialize: (_: never) => raiseError(() => new TypeError('Functions may not be serialized'))
isApplicable: (v) => v instanceof Attachment,
serialize: (attachment) => ({
name: attachment.name,
type: attachment.type,
data: toBase64(attachment)
}),
deserialize: (attachment) => new Attachment(attachment.name, attachment.type, fromBase64(attachment.data))
},
'Function'
'Attachment'
)

return SuperJSON
return superJson
}
47 changes: 47 additions & 0 deletions src/main/dao/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { memo } from 'radash'
import { z } from 'zod'
import {
Database,
DocumentId,
inferDocumentOf,
inferNewDocumentOf,
inferUpdatesOf,
inferUpsertOf
} from '../services/database'
import useTiesDatabase from './ties'
import type { RevisionId } from '../services/database'

export const DeviceModel = z.object({
driverId: DocumentId,
title: z.string().min(1),
path: z.string().min(1)
})

const useDevicesDatabase = memo(
() =>
new (class extends Database.of('devices', DeviceModel) {
readonly #ties = useTiesDatabase()

override async remove(id: DocumentId, rev?: RevisionId) {
await super.remove(id, rev)

const related = await this.#ties.forDevice(id)
await Promise.all(
related.map(async ({ _id, _rev }) => {
await this.#ties.remove(_id, _rev)
})
)
}
})()
)

export type Device = inferDocumentOf<typeof DeviceModel>
export const Device = inferDocumentOf(DeviceModel)
export type NewDevice = inferNewDocumentOf<typeof DeviceModel>
export const NewDevice = inferNewDocumentOf(DeviceModel)
export type DeviceUpdate = inferUpdatesOf<typeof DeviceModel>
export const DeviceUpdate = inferUpdatesOf(DeviceModel)
export type DeviceUpsert = inferUpsertOf<typeof DeviceModel>
export const DeviceUpsert = inferUpsertOf(DeviceModel)

export default useDevicesDatabase
4 changes: 3 additions & 1 deletion src/main/dao/sources.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo } from 'radash'
import { z } from 'zod'
import { Database, inferDocumentOf, inferNewDocumentOf, inferUpdatesOf } from '../services/database'
import { Database, inferDocumentOf, inferNewDocumentOf, inferUpdatesOf, inferUpsertOf } from '../services/database'
import useTiesDatabase from './ties'
import type { DocumentId, RevisionId } from '../services/database'

Expand Down Expand Up @@ -33,5 +33,7 @@ export type NewSource = inferNewDocumentOf<typeof SourceModel>
export const NewSource = inferNewDocumentOf(SourceModel)
export type SourceUpdate = inferUpdatesOf<typeof SourceModel>
export const SourceUpdate = inferUpdatesOf(SourceModel)
export type SourceUpsert = inferUpsertOf<typeof SourceModel>
export const SourceUpsert = inferUpsertOf(SourceModel)

export default useSourcesDatabase
38 changes: 0 additions & 38 deletions src/main/dao/switches.ts

This file was deleted.

19 changes: 14 additions & 5 deletions src/main/dao/ties.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { map, memo } from 'radash'
import { z } from 'zod'
import { Database, DocumentId, inferDocumentOf, inferNewDocumentOf, inferUpdatesOf } from '../services/database'
import {
Database,
DocumentId,
inferDocumentOf,
inferNewDocumentOf,
inferUpdatesOf,
inferUpsertOf
} from '../services/database'

export const TieModel = z.object({
sourceId: DocumentId,
switchId: DocumentId,
deviceId: DocumentId,
inputChannel: z.number().int().nonnegative(),
outputChannels: z.object({
video: z.number().int().nonnegative().optional(),
audio: z.number().int().nonnegative().optional()
})
})

const indices = { sourceId: ['sourceId'], switchId: ['switchId'] }
const indices = { sourceId: ['sourceId'], deviceId: ['deviceId'] }

const useTiesDatabase = memo(
() =>
new (class extends Database.of('ties', TieModel, indices) {
async forSwitch(switchId: DocumentId) {
async forDevice(deviceId: DocumentId) {
return await this.run(
async (db) =>
await db
.find({ selector: { switchId } })
.find({ selector: { deviceId } })
.then(async (r) => await map(r.docs, async (d) => await this.prepare(d)))
)
}
Expand All @@ -43,5 +50,7 @@ export type NewTie = inferNewDocumentOf<typeof TieModel>
export const NewTie = inferNewDocumentOf(TieModel)
export type TieUpdate = inferUpdatesOf<typeof TieModel>
export const TieUpdate = inferUpdatesOf(TieModel)
export type TieUpsert = inferUpsertOf<typeof TieModel>
export const TieUpsert = inferUpsertOf(TieModel)

export default useTiesDatabase
27 changes: 26 additions & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import Logger from 'electron-log'
import { sleep } from 'radash'
import appIcon from '../../resources/icon.png?asset&asarUnpack'
import { useAppRouter } from './routes/router'
import useMigrations from './services/migration'
import { createIpcHandler } from './services/rpc/ipc'
import { logError } from './utilities'
import { toError } from '@/error-handling'

// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.

Logger.initialize({ preload: true, spyRendererConsole: true })
Logger.transports.console.format = '{h}:{i}:{s}.{ms} [{level}] › {text}'
Logger.transports.file.level = 'debug'
Logger.errorHandler.startCatching()
Expand Down Expand Up @@ -53,6 +53,26 @@ async function createWindow() {
const kWait = 2000
let lastError: unknown

window.webContents.on('console-message', (_, level, message) => {
switch (level) {
case 0:
Logger.verbose(message)
break
case 1:
Logger.info(message)
break
case 2:
Logger.warn(message)
break
case 3:
Logger.error(message)
break
default:
Logger.log(message)
break
}
})

/* eslint-disable no-await-in-loop -- Retry loop must be serial. */
for (let tries = 3; tries > 0; --tries) {
try {
Expand Down Expand Up @@ -106,6 +126,11 @@ process.on('SIGTERM', () => {
// this event occurs.
await app.whenReady()

const migrate = useMigrations()
await migrate().catch((cause: unknown) => {
Logger.error(cause)
})

// Set app user model id for windows
electronApp.setAppUserModelId('org.sleepingcats.BridgeCmdr')

Expand Down
21 changes: 21 additions & 0 deletions src/main/migrations/20241118211400-rename-switches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod'
import { Database, DocumentId } from '../services/database'

export async function migrate() {
const Model = z.object({
driverId: DocumentId,
title: z.string().min(1),
path: z.string().min(1)
})

const switches = new Database('switches', Model)
const devices = new Database('devices', Model)

for (const device of await switches.all()) {
// eslint-disable-next-line no-await-in-loop
await devices.upsert(device)
}

await switches.destroy()
await devices.close()
}
28 changes: 28 additions & 0 deletions src/main/migrations/20241119202100-rename-switchId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod'
import { Database, DocumentId } from '../services/database'

export async function migrate() {
const Model = z.object({}).catchall(z.unknown())

const OldModel = z
.object({
_id: DocumentId,
sourceId: DocumentId,
switchId: DocumentId,
inputChannel: z.number().int().nonnegative(),
outputChannels: z.object({
video: z.number().int().nonnegative().optional(),
audio: z.number().int().nonnegative().optional()
})
})
.passthrough()

const ties = new Database('ties', Model)

for (const tie of await ties.all()) {
const { switchId, ...old } = OldModel.parse(tie)

// eslint-disable-next-line no-await-in-loop
await ties.replace({ ...old, deviceId: switchId })
}
}
Loading

0 comments on commit af78c51

Please sign in to comment.