From fe2bb8749a92e924191387a03beda143a613f47f Mon Sep 17 00:00:00 2001 From: LordTocs Date: Thu, 5 Sep 2024 01:14:21 -0400 Subject: [PATCH 01/35] Switch to MaybeRefOrGetter --- libs/castmate-ui-core/src/media/media-store.ts | 2 +- libs/castmate-ui-core/src/util/file-drop.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/castmate-ui-core/src/media/media-store.ts b/libs/castmate-ui-core/src/media/media-store.ts index 694fb441..e809710d 100644 --- a/libs/castmate-ui-core/src/media/media-store.ts +++ b/libs/castmate-ui-core/src/media/media-store.ts @@ -68,7 +68,7 @@ export const useMediaStore = defineStore("media", () => { } }) -export function useMediaDrop(element: Ref, subPath: MaybeRefOrGetter) { +export function useMediaDrop(element: MaybeRefOrGetter, subPath: MaybeRefOrGetter) { const mediaStore = useMediaStore() return useFileDragDrop(element, (files) => { diff --git a/libs/castmate-ui-core/src/util/file-drop.ts b/libs/castmate-ui-core/src/util/file-drop.ts index 511ea4b7..4bd86225 100644 --- a/libs/castmate-ui-core/src/util/file-drop.ts +++ b/libs/castmate-ui-core/src/util/file-drop.ts @@ -1,5 +1,5 @@ import { useEventListener } from "@vueuse/core" -import { computed, ref, Ref, toValue } from "vue" +import { computed, MaybeRefOrGetter, ref, Ref, toValue } from "vue" interface FromTo { fromElement?: HTMLElement @@ -22,7 +22,7 @@ async function getMimeType(url: string) { return response.headers.get("Content-Type") ?? undefined } -export function useFileDragDrop(element: Ref, onDrop: FileDropEvent) { +export function useFileDragDrop(element: MaybeRefOrGetter, onDrop: FileDropEvent) { const hoveringFiles = ref(false) useEventListener(element, "dragenter", (ev: DragEvent) => { From 41a1ef015712ee2a16df823a3afd987d1bd9ee27 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Sat, 7 Sep 2024 21:41:07 -0400 Subject: [PATCH 02/35] Improve Media Drop to handle folders, Switch to grid from table --- libs/castmate-core/src/media/media-manager.ts | 37 +---------- .../components/data/inputs/MediaFileInput.vue | 16 +++-- .../components/docking/ScrollingTabBody.vue | 6 +- .../src/components/media/MediaBrowserPage.vue | 30 +++++++-- .../src/components/media/MediaTreeFile.vue | 56 ++++++++-------- .../src/components/media/MediaTreeFolder.vue | 42 ++++++++++-- .../castmate-ui-core/src/media/media-store.ts | 64 +++++++++++-------- libs/castmate-ui-core/src/util/dom.ts | 17 +++++ libs/castmate-ui-core/src/util/file-drop.ts | 42 ++++++++---- 9 files changed, 190 insertions(+), 120 deletions(-) diff --git a/libs/castmate-core/src/media/media-manager.ts b/libs/castmate-core/src/media/media-manager.ts index 9f131155..2f7f04c0 100644 --- a/libs/castmate-core/src/media/media-manager.ts +++ b/libs/castmate-core/src/media/media-manager.ts @@ -42,41 +42,6 @@ export interface MediaFolder { path: string watcher: chokidar.FSWatcher } -/* -function downloadFile(url: string, dest: string) { - return new Promise((resolve, reject) => { - const writeStream = fsSync.createWriteStream(dest) - - const request = http.get(url, (resp) => { - if (resp.statusCode !== 200) { - reject(`Failed to download ${url} with ${resp.statusCode}`) - } - - resp.pipe(writeStream) - }) - - writeStream.on("finish", () => { - writeStream.close((err) => { - if (err) return reject(err) - resolve() - }) - }) - - request.on("error", (err) => { - fsSync.unlink(dest, (unlinkErr) => { - if (unlinkErr) return reject(unlinkErr) - reject(err) - }) - }) - - writeStream.on("error", (err) => { - fsSync.unlink(dest, (unlinkErr) => { - if (unlinkErr) return reject(unlinkErr) - reject(err) - }) - }) - }) -}*/ async function downloadFile(url: string, dest: string) { const writeStream = fsSync.createWriteStream(dest) @@ -146,7 +111,7 @@ export const MediaManager = Service( getLocalPath(mediaPath: string) { const baseMediaPath = resolveProjectPath("./media") - if (!mediaPath.startsWith("/default")) throw new Error("not a media path") + if (!mediaPath.startsWith("/default")) throw new Error(`"${mediaPath}" not a media path`) const defaultPath = path.relative("/default", mediaPath) diff --git a/libs/castmate-ui-core/src/components/data/inputs/MediaFileInput.vue b/libs/castmate-ui-core/src/components/data/inputs/MediaFileInput.vue index 8e46b665..ca288619 100644 --- a/libs/castmate-ui-core/src/components/data/inputs/MediaFileInput.vue +++ b/libs/castmate-ui-core/src/components/data/inputs/MediaFileInput.vue @@ -26,19 +26,14 @@ @wheel="stopPropagation" @mousedown="onDropdownMouseDown" > - - - - - - +
-
MediaTypeDuration
+ @@ -154,4 +149,11 @@ function mediaClicked(media: MediaFile) { display: flex; flex-direction: row; } + +.media-folder-tree { + width: 100%; + display: grid; + grid-template-columns: 1fr fit-content(100px) fit-content(150px); + gap: 0 2px; +} diff --git a/libs/castmate-ui-core/src/components/docking/ScrollingTabBody.vue b/libs/castmate-ui-core/src/components/docking/ScrollingTabBody.vue index 32850db0..763a0907 100644 --- a/libs/castmate-ui-core/src/components/docking/ScrollingTabBody.vue +++ b/libs/castmate-ui-core/src/components/docking/ScrollingTabBody.vue @@ -11,7 +11,7 @@ diff --git a/libs/castmate-ui-core/src/components/media/MediaBrowserPage.vue b/libs/castmate-ui-core/src/components/media/MediaBrowserPage.vue index e426a4d3..b7b30fea 100644 --- a/libs/castmate-ui-core/src/components/media/MediaBrowserPage.vue +++ b/libs/castmate-ui-core/src/components/media/MediaBrowserPage.vue @@ -1,5 +1,5 @@ @@ -35,6 +39,8 @@ import PButton from "primevue/button" import SoundPlayer from "./SoundPlayer.vue" import { ScrollingTabBody } from "../../main" +import { useMediaDrop } from "../../media/media-store" + const filters = ref({ global: { value: null, matchMode: FilterMatchMode.CONTAINS }, }) @@ -56,6 +62,10 @@ function isImagePreview(media: MediaMetadata) { const ext = path.extname(media.path) return [".gif", ".webp", ".apng"].includes(ext) } + +const tabBody = ref>() + +const { hoveringFiles } = useMediaDrop(() => tabBody.value?.scrollDiv, "/default", "media-folder") diff --git a/libs/castmate-ui-core/src/components/media/MediaTreeFile.vue b/libs/castmate-ui-core/src/components/media/MediaTreeFile.vue index d390cb22..1e9ed8c0 100644 --- a/libs/castmate-ui-core/src/components/media/MediaTreeFile.vue +++ b/libs/castmate-ui-core/src/components/media/MediaTreeFile.vue @@ -1,33 +1,32 @@ @@ -87,17 +86,22 @@ function handleClick(ev: MouseEvent) { diff --git a/libs/castmate-ui-core/src/media/media-store.ts b/libs/castmate-ui-core/src/media/media-store.ts index e809710d..f98ecc05 100644 --- a/libs/castmate-ui-core/src/media/media-store.ts +++ b/libs/castmate-ui-core/src/media/media-store.ts @@ -68,41 +68,49 @@ export const useMediaStore = defineStore("media", () => { } }) -export function useMediaDrop(element: MaybeRefOrGetter, subPath: MaybeRefOrGetter) { +export function useMediaDrop( + element: MaybeRefOrGetter, + subPath: MaybeRefOrGetter, + nestingClass?: string +) { const mediaStore = useMediaStore() - return useFileDragDrop(element, (files) => { - const basepath = toValue(subPath) - - for (const file of files) { - console.log("DROP", file) - if ( - !( - file.mimetype.startsWith("image") || - file.mimetype.startsWith("audio") || - file.mimetype.startsWith("video") + return useFileDragDrop( + element, + (files) => { + const basepath = toValue(subPath) + + for (const file of files) { + console.log("DROP", file) + if ( + !( + file.mimetype.startsWith("image") || + file.mimetype.startsWith("audio") || + file.mimetype.startsWith("video") + ) ) - ) - continue + continue - const pathParse = path.parse(file.path) + const pathParse = path.parse(file.path) - const mediaName = pathParse.base + const mediaName = pathParse.base - const proposedMediaPath = path.join(basepath, mediaName).replaceAll("\\", "/") + const proposedMediaPath = path.join(basepath, mediaName).replaceAll("\\", "/") - if (mediaStore.media[proposedMediaPath]) { - console.log("ALREADY HAS PATH", proposedMediaPath) - continue - } + if (mediaStore.media[proposedMediaPath]) { + console.log("ALREADY HAS PATH", proposedMediaPath) + continue + } - if (file.remote) { - console.log("DOWNLOAD", file.path, "to", proposedMediaPath) - mediaStore.downloadMedia(file.path, proposedMediaPath) - } else { - console.log("COPY", file.path, "to", proposedMediaPath) - mediaStore.copyMedia(file.path, proposedMediaPath) + if (file.remote) { + console.log("DOWNLOAD", file.path, "to", proposedMediaPath) + mediaStore.downloadMedia(file.path, proposedMediaPath) + } else { + console.log("COPY", file.path, "to", proposedMediaPath) + mediaStore.copyMedia(file.path, proposedMediaPath) + } } - } - }) + }, + nestingClass + ) } diff --git a/libs/castmate-ui-core/src/util/dom.ts b/libs/castmate-ui-core/src/util/dom.ts index ec746a13..a82d2ffd 100644 --- a/libs/castmate-ui-core/src/util/dom.ts +++ b/libs/castmate-ui-core/src/util/dom.ts @@ -15,6 +15,23 @@ export function isChildOfClass(element: HTMLElement, clazz: string) { return false } +export function isUnnestedChild(parent: HTMLElement, element: HTMLElement | undefined | null, clazz?: string) { + if (!element) return false + if (!parent.contains(element)) return false + + let currentElement: HTMLElement | null = element + while (currentElement) { + if (parent === currentElement) return true + + if (clazz && currentElement.classList.contains(clazz)) { + return false + } + currentElement = currentElement.parentElement + } + + return false +} + export function getElementScroll(elem: HTMLElement) { return { x: elem.scrollLeft ?? 0, y: elem.scrollTop ?? 0 } } diff --git a/libs/castmate-ui-core/src/util/file-drop.ts b/libs/castmate-ui-core/src/util/file-drop.ts index 4bd86225..566b1219 100644 --- a/libs/castmate-ui-core/src/util/file-drop.ts +++ b/libs/castmate-ui-core/src/util/file-drop.ts @@ -1,5 +1,6 @@ import { useEventListener } from "@vueuse/core" import { computed, MaybeRefOrGetter, ref, Ref, toValue } from "vue" +import { isUnnestedChild } from "./dom" interface FromTo { fromElement?: HTMLElement @@ -22,7 +23,11 @@ async function getMimeType(url: string) { return response.headers.get("Content-Type") ?? undefined } -export function useFileDragDrop(element: MaybeRefOrGetter, onDrop: FileDropEvent) { +export function useFileDragDrop( + element: MaybeRefOrGetter, + onDrop: FileDropEvent, + nestingClass?: string +) { const hoveringFiles = ref(false) useEventListener(element, "dragenter", (ev: DragEvent) => { @@ -32,17 +37,24 @@ export function useFileDragDrop(element: MaybeRefOrGetter { @@ -52,17 +64,23 @@ export function useFileDragDrop(element: MaybeRefOrGetter { From a637cb605358f35a634f383d0e98671c3de0c688 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Wed, 29 May 2024 00:06:50 -0400 Subject: [PATCH 03/35] Initial Viewer Data DB Implementation --- libs/castmate-core/package.json | 1 + .../src/viewer-data/viewer-data.ts | 184 ++++++++ yarn.lock | 433 +++++++++++++++++- 3 files changed, 606 insertions(+), 12 deletions(-) create mode 100644 libs/castmate-core/src/viewer-data/viewer-data.ts diff --git a/libs/castmate-core/package.json b/libs/castmate-core/package.json index 6b580c2b..4d6e6aec 100644 --- a/libs/castmate-core/package.json +++ b/libs/castmate-core/package.json @@ -36,6 +36,7 @@ "nanoid": "^5.0.7", "recursive-readdir-async": "^1.3.0", "semver": "^7.6.2", + "sqlite3": "^5.1.7", "winston": "^3.11.0", "ws": "^8.16.0", "yaml": "^2.2.1" diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts new file mode 100644 index 00000000..583700ac --- /dev/null +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -0,0 +1,184 @@ +import { IPCSchema, Schema, getTypeByConstructor, getTypeByName } from "castmate-schema" +import { Service } from "../util/service" +import sqlite from "sqlite3" +import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath } from "../io/file-system" +import { deserializeSchema, serializeSchema } from "../util/ipc-schema" +import { usePluginLogger } from "../logging/logging" + +interface ViewerVariable { + name: string + schema: Schema +} + +interface SerializedViewerVariable { + name: string + type: string + defaultValue?: any +} + +const logger = usePluginLogger("viewer-data") + +const sqlTypes: Record = { + String: "TEXT", + Number: "REAL", + Boolean: "INTEGER", +} + +function escapeSql(sql: string) { + return sql.replace(/\'/g, "''") +} + +export const ViewerData = Service( + class { + private db: sqlite.Database + + private variables: ViewerVariable[] = [] + + constructor() {} + + private createDb(): Promise { + return new Promise((resolve, reject) => { + const path = resolveProjectPath("/viewer-data/db.sqlite3") + this.db = new sqlite.Database(path, (err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) + } + + private run(sql: string, params?: any) { + return new Promise((resolve, reject) => { + this.db.run(sql, params, (err) => { + if (err) return reject(err) + resolve() + }) + }) + } + + private get(sql: string, params?: any) { + return new Promise((resolve, reject) => { + this.db.get(sql, params, (err, row) => { + if (err) return reject(err) + resolve(row as T) + }) + }) + } + + private ensureColumn(variable: ViewerVariable) { + try { + const schemaType = getTypeByConstructor(variable.schema.type) + if (!schemaType) return + + const sqlType = sqlTypes[schemaType.name] ?? "BLOB" + + this.run(`ALTER TABLE ViewerData ADD COLUMN ${variable.name} ${sqlType}`) + } catch {} + } + + private async loadVariables() { + await ensureYAML([], "/viewer-data/variables.yaml") + + const data: SerializedViewerVariable[] = await loadYAML("/viewer-data/variables.yaml") + + for (const varData of data) { + const type = getTypeByName(varData.type) + + if (!type) { + logger.error("Missing Viewer Var Type", varData.type) + continue + } + + const schema: Schema = { + type: type.constructor, + } + + if (varData.defaultValue != null) { + const defaultValue = await deserializeSchema(schema, varData.defaultValue) + + schema.default = defaultValue + } + + this.variables.push({ + name: varData.name, + schema, + }) + } + + for (const vari of this.variables) { + await this.ensureColumn(vari) + } + } + + getVariable(name: string) { + return this.variables.find((v) => v.name == name) + } + + async initialize() { + await ensureDirectory(resolveProjectPath("/viewer-data")) + + await this.createDb() + + await this.run("create table if not exists ViewerData (id text PRIMARY KEY)") + + await this.loadVariables() + } + + shutdown() { + return new Promise((resolve, reject) => { + this.db.close((err) => { + if (err) return reject(err) + resolve() + }) + }) + } + + async addViewerVariable(name: string, schema: Schema) { + const vari: ViewerVariable = { + name, + schema, + } + await this.ensureColumn(vari) + this.variables.push(vari) + } + + async removeViewerVariable(name: string) { + const idx = this.variables.findIndex((v) => v.name == name) + if (idx < 0) return + + await this.run("ALTER TABLE ViewerData DROP COLUMN ?", name) + + this.variables.splice(idx, 1) + } + + async setViewerValue(provider: string, id: string, name: string, value: any) { + const dbId = `${provider}.${id}` + + const vari = this.getVariable(name) + if (!vari) return + + const serialized = await serializeSchema(vari.schema, value) + + await this.run(`UPDATE ViewerData SET ? = ? WHERE id = ?`, [name, serialized, dbId]) + } + + async getViewerData(provider: string, id: string) { + const dbId = `${provider}.${id}` + + try { + const data = await this.get>("SELECT * FROM ViewerData WHERE id = ?", [dbId]) + + const result: Record = {} + + for (const vari of this.variables) { + result[vari.name] = await deserializeSchema(vari.schema, data[vari.name]) + } + + return result + } catch { + return undefined + } + } + } +) diff --git a/yarn.lock b/yarn.lock index 8284a692..89241ddf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -674,7 +674,7 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": +"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" checksum: 10/052dd232140fa60e81588000cbe729a40146579b361f1070bce63e2a761388a22a16d00beeffc504bd3601cb8e055c57b21a185448b3ed550cf50716f4fd442e @@ -871,6 +871,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/fs@npm:^1.0.0": + version: 1.1.1 + resolution: "@npmcli/fs@npm:1.1.1" + dependencies: + "@gar/promisify": "npm:^1.0.1" + semver: "npm:^7.3.5" + checksum: 10/8b5e6d75759b9f1a8b7885913df274c8cbbb1221176872615f2aecedf47b2c36e5dfbf4046ff1a905c9f3592fbd32051b3050b8a897bf03514a1a404b39af074 + languageName: node + linkType: hard + "@npmcli/fs@npm:^2.1.0": version: 2.1.2 resolution: "@npmcli/fs@npm:2.1.2" @@ -890,6 +900,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/move-file@npm:^1.0.1": + version: 1.1.2 + resolution: "@npmcli/move-file@npm:1.1.2" + dependencies: + mkdirp: "npm:^1.0.4" + rimraf: "npm:^3.0.2" + checksum: 10/c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7 + languageName: node + linkType: hard + "@npmcli/move-file@npm:^2.0.0": version: 2.0.1 resolution: "@npmcli/move-file@npm:2.0.1" @@ -1143,6 +1163,13 @@ __metadata: languageName: node linkType: hard +"@tootallnate/once@npm:1": + version: 1.1.2 + resolution: "@tootallnate/once@npm:1.1.2" + checksum: 10/e1fb1bbbc12089a0cb9433dc290f97bddd062deadb6178ce9bcb93bb7c1aecde5e60184bc7065aec42fe1663622a213493c48bbd4972d931aae48315f18e1be9 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -2073,7 +2100,7 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^1.0.0": +"abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 @@ -2142,7 +2169,7 @@ __metadata: languageName: node linkType: hard -"agentkeepalive@npm:^4.2.1": +"agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1": version: 4.5.0 resolution: "agentkeepalive@npm:4.5.0" dependencies: @@ -2455,6 +2482,15 @@ __metadata: languageName: node linkType: hard +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 10/593d5ae975ffba15fbbb4788fe5abd1e125afbab849ab967ab43691d27d6483751805d98cb92f7ac24a2439a8a8678cd0131c535d5d63de84e383b0ce2786133 + languageName: node + linkType: hard + "bindings@npm:~1.2.1": version: 1.2.1 resolution: "bindings@npm:1.2.1" @@ -2462,7 +2498,7 @@ __metadata: languageName: node linkType: hard -"bl@npm:^4.1.0": +"bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" dependencies: @@ -2691,6 +2727,32 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^15.2.0": + version: 15.3.0 + resolution: "cacache@npm:15.3.0" + dependencies: + "@npmcli/fs": "npm:^1.0.0" + "@npmcli/move-file": "npm:^1.0.1" + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + glob: "npm:^7.1.4" + infer-owner: "npm:^1.0.4" + lru-cache: "npm:^6.0.0" + minipass: "npm:^3.1.1" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.2" + mkdirp: "npm:^1.0.3" + p-map: "npm:^4.0.0" + promise-inflight: "npm:^1.0.1" + rimraf: "npm:^3.0.2" + ssri: "npm:^8.0.1" + tar: "npm:^6.0.2" + unique-filename: "npm:^1.1.1" + checksum: 10/1432d84f3f4b31421cf47c15e6956e5e736a93c65126b0fd69ae5f70643d29be8996f33d4995204f578850de5d556268540911c04ecc1c026375b18600534f08 + languageName: node + linkType: hard + "cacache@npm:^16.1.0": version: 16.1.3 resolution: "cacache@npm:16.1.3" @@ -2802,6 +2864,7 @@ __metadata: nanoid: "npm:^5.0.7" recursive-readdir-async: "npm:^1.3.0" semver: "npm:^7.6.2" + sqlite3: "npm:^5.1.7" ts-toolbelt: "npm:*" typescript: "npm:*" winston: "npm:^3.11.0" @@ -4348,6 +4411,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -4790,6 +4860,13 @@ __metadata: languageName: node linkType: hard +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10/7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -4870,6 +4947,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 10/b4ea018d623e077bd395f168a9e81db77370dde36a5b01d067f2ad7989924a81d31cb547ff764acb2aa25d50bb7fdde0b0a93bec02212b0cb430621623246d39 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.1": version: 2.0.2 resolution: "detect-libc@npm:2.0.2" @@ -5155,7 +5239,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.13": +"encoding@npm:^0.1.12, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -5164,7 +5248,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -5490,6 +5574,13 @@ __metadata: languageName: node linkType: hard +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10/588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -5674,6 +5765,13 @@ __metadata: languageName: node linkType: hard +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: 10/b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + "filelist@npm:^1.0.4": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -5814,6 +5912,13 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + "fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -5980,6 +6085,13 @@ __metadata: languageName: node linkType: hard +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10/2a091ba07fbce22205642543b4ea8aaf068397e1433c00ae0f9de36a3607baf5bcc14da97fbb798cfca6393b3c402031fca06d8b491a44206d6efef391c58537 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -6222,6 +6334,17 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:^4.0.1": + version: 4.0.1 + resolution: "http-proxy-agent@npm:4.0.1" + dependencies: + "@tootallnate/once": "npm:1" + agent-base: "npm:6" + debug: "npm:4" + checksum: 10/2e17f5519f2f2740b236d1d14911ea4be170c67419dc15b05ea9a860a22c5d9c6ff4da270972117067cc2cefeba9df5f7cd5e7818fdc6ae52b6acf2a533e5fdd + languageName: node + linkType: hard + "http-proxy-agent@npm:^5.0.0": version: 5.0.0 resolution: "http-proxy-agent@npm:5.0.0" @@ -6421,6 +6544,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10/314ae176e8d4deb3def56106da8002b462221c174ddb7ce0c49ee72c8cd1f9044f7b10cc555a7d8850982c3b9ca96fc212122749f5234bc2b6fb05fb942ed566 + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -7147,6 +7277,30 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^9.1.0": + version: 9.1.0 + resolution: "make-fetch-happen@npm:9.1.0" + dependencies: + agentkeepalive: "npm:^4.1.3" + cacache: "npm:^15.2.0" + http-cache-semantics: "npm:^4.1.0" + http-proxy-agent: "npm:^4.0.1" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^6.0.0" + minipass: "npm:^3.1.3" + minipass-collect: "npm:^1.0.2" + minipass-fetch: "npm:^1.3.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.2" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^6.0.0" + ssri: "npm:^8.0.0" + checksum: 10/a868e74fc223a78afb7a1f8115133befdffae84f07a5f5dd9317cbf9f784a8373f28829a73ae3f31060e1b0cb4944e73257733c3b10c314354060fab412b6028 + languageName: node + linkType: hard + "matcher@npm:^3.0.0": version: 3.0.0 resolution: "matcher@npm:3.0.0" @@ -7303,7 +7457,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.6": +"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -7328,6 +7482,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^1.3.2": + version: 1.4.1 + resolution: "minipass-fetch@npm:1.4.1" + dependencies: + encoding: "npm:^0.1.12" + minipass: "npm:^3.1.0" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.0.0" + dependenciesMeta: + encoding: + optional: true + checksum: 10/4c6f678d2c976c275ba35735aa18e341401d1fb94bbf38a36bb2c2d01835ac699f15b7ab1adaf4ee40a751361527d312a18853feaf9c0121f4904f811656575a + languageName: node + linkType: hard + "minipass-fetch@npm:^2.0.3": version: 2.1.2 resolution: "minipass-fetch@npm:2.1.2" @@ -7367,7 +7536,7 @@ __metadata: languageName: node linkType: hard -"minipass-pipeline@npm:^1.2.4": +"minipass-pipeline@npm:^1.2.2, minipass-pipeline@npm:^1.2.4": version: 1.2.4 resolution: "minipass-pipeline@npm:1.2.4" dependencies: @@ -7385,7 +7554,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -7415,7 +7584,7 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": +"minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" dependencies: @@ -7434,6 +7603,13 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + "mkdirp@npm:^0.5.1": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -7547,6 +7723,22 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^5.0.7": + version: 5.0.7 + resolution: "nanoid@npm:5.0.7" + bin: + nanoid: bin/nanoid.js + checksum: 10/25ab0b0cf9082ae6747f0f55cec930e6c1cc5975103aa3a5fda44be5720eff57d9b25a8a9850274bfdde8def964b49bf03def71c6aa7ad1cba32787819b79f60 + languageName: node + linkType: hard + +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 10/276feb8e30189fe18718e85b6f82e4f952822baa2e7696f771cc42571a235b789dc5907a14d9ffb6838c3e4ff4c25717c2575e5ce1cf6e02e496e204c11e57f6 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -7554,13 +7746,22 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.2, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: 10/2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 languageName: node linkType: hard +"node-abi@npm:^3.3.0": + version: 3.62.0 + resolution: "node-abi@npm:3.62.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10/4cb9d4e6d3501bd9868230187f9f1638d777d1d2ca357389a2d411675889ee44375acbeae973b9c501fca723c9657d84684856787988a6327187f5f1e9ab6aee + languageName: node + linkType: hard + "node-abi@npm:^3.45.0": version: 3.56.0 resolution: "node-abi@npm:3.56.0" @@ -7588,6 +7789,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.0 + resolution: "node-addon-api@npm:7.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10/e20487e98c76660f4957e81e85c45dfb667140d9be0bf872a3b3dfd86b4ea19c0275939116c90efebc0da7fc6af2c7b7b060512ceebe6417b1ed145a26910453 + languageName: node + linkType: hard + "node-api-version@npm:^0.2.0": version: 0.2.0 resolution: "node-api-version@npm:0.2.0" @@ -7631,6 +7841,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:8.x": + version: 8.4.1 + resolution: "node-gyp@npm:8.4.1" + dependencies: + env-paths: "npm:^2.2.0" + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^9.1.0" + nopt: "npm:^5.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^2.0.2" + bin: + node-gyp: bin/node-gyp.js + checksum: 10/5ac19a7f6212c787f33bb72f889fafb1ce9d80b7ecb87b3785aebb0ff94a70cd5dbb3ecb435a308eaeb26d037c6edaf173951a9edacaadf0f4c3ae189f1e5077 + languageName: node + linkType: hard + "node-gyp@npm:^9.0.0": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -7747,6 +7977,17 @@ __metadata: languageName: unknown linkType: soft +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: 10/00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd + languageName: node + linkType: hard + "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -8130,6 +8371,28 @@ __metadata: languageName: node linkType: hard +"prebuild-install@npm:^7.1.1": + version: 7.1.2 + resolution: "prebuild-install@npm:7.1.2" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^1.0.1" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10/32d5c026cc978dd02762b9ad3c765178aee8383aeac4303fed3cd226eff53100db038d4791b03ae1ebc7d213a7af392d26e32095579cedb8dba1d00ad08ecd46 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -8316,6 +8579,20 @@ __metadata: languageName: node linkType: hard +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10/5c4d72ae7eec44357171585938c85ce066da8ca79146b5635baf3d55d74584c92575fa4e2c9eac03efbed3b46a0b2e7c30634c012b4b4fa40d654353d3c163eb + languageName: node + linkType: hard + "read-binary-file-arch@npm:^1.0.6": version: 1.0.6 resolution: "read-binary-file-arch@npm:1.0.6" @@ -8382,7 +8659,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -8886,6 +9163,24 @@ __metadata: languageName: node linkType: hard +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10/4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10/93f1b32319782f78f2f2234e9ce34891b7ab6b990d19d8afefaa44423f5235ce2676aae42d6743fecac6c8dfff4b808d4c24fe5265be813d04769917a9a44f36 + languageName: node + linkType: hard + "simple-swizzle@npm:^0.2.2": version: 0.2.2 resolution: "simple-swizzle@npm:0.2.2" @@ -8922,6 +9217,17 @@ __metadata: languageName: node linkType: hard +"socks-proxy-agent@npm:^6.0.0": + version: 6.2.1 + resolution: "socks-proxy-agent@npm:6.2.1" + dependencies: + agent-base: "npm:^6.0.2" + debug: "npm:^4.3.3" + socks: "npm:^2.6.2" + checksum: 10/554749ba3bdba0742ec36493a907261c116dd0dafcd618ea5babdfc90ce5a5ae648d4ee4d2e26e7184afd854973d282372ce0af63e1fc6412bb9fa1a2b1f2d45 + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -9016,6 +9322,27 @@ __metadata: languageName: node linkType: hard +"sqlite3@npm:^5.1.7": + version: 5.1.7 + resolution: "sqlite3@npm:5.1.7" + dependencies: + bindings: "npm:^1.5.0" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:8.x" + prebuild-install: "npm:^7.1.1" + tar: "npm:^6.1.11" + peerDependencies: + node-gyp: 8.x + dependenciesMeta: + node-gyp: + optional: true + peerDependenciesMeta: + node-gyp: + optional: true + checksum: 10/84b1183d39791b00bc9d9aa6f0d74738084c9447e7cff7cf3cc2fa7458474159e1272eed675cd3a5c6c5100e97557578dbf0e77082f1043ab6356e6446314e32 + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -9025,6 +9352,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^8.0.0, ssri@npm:^8.0.1": + version: 8.0.1 + resolution: "ssri@npm:8.0.1" + dependencies: + minipass: "npm:^3.1.1" + checksum: 10/fde247b7107674d9a424a20f9c1a6e3ad88a139c2636b9d9ffa7df59e85e11a894cdae48fadd0ad6be41eb0d5b847fe094736513d333615c7eebc3d111abe0d2 + languageName: node + linkType: hard + "ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -9155,6 +9491,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10/1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + "sumchecker@npm:^3.0.1": version: 3.0.1 resolution: "sumchecker@npm:3.0.1" @@ -9180,6 +9523,31 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^2.0.0": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10/526deae025453e825f87650808969662fbb12eb0461d033e9b447de60ec951c6c4607d0afe7ce057defe9d4e45cf80399dd74bc15f9d9e0773d5e990a78ce4ac + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + languageName: node + linkType: hard + "tar-stream@npm:^3.0.0": version: 3.1.7 resolution: "tar-stream@npm:3.1.7" @@ -9191,6 +9559,20 @@ __metadata: languageName: node linkType: hard +"tar@npm:^6.0.2": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0 + languageName: node + linkType: hard + "tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": version: 6.2.0 resolution: "tar@npm:6.2.0" @@ -9383,6 +9765,15 @@ __metadata: languageName: node linkType: hard +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10/7f0d9ed5c22404072b2ae8edc45c071772affd2ed14a74f03b4e71b4dd1a14c3714d85aed64abcaaee5fec2efc79002ba81155c708f4df65821b444abb0cfade + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -9486,6 +9877,15 @@ __metadata: languageName: node linkType: hard +"unique-filename@npm:^1.1.1": + version: 1.1.1 + resolution: "unique-filename@npm:1.1.1" + dependencies: + unique-slug: "npm:^2.0.0" + checksum: 10/9b6969d649a2096755f19f793315465c6427453b66d67c2a1bee8f36ca7e1fc40725be2c028e974dec110d365bd30a4248e89b1044dc1dfe29663b6867d071ef + languageName: node + linkType: hard + "unique-filename@npm:^2.0.0": version: 2.0.1 resolution: "unique-filename@npm:2.0.1" @@ -9504,6 +9904,15 @@ __metadata: languageName: node linkType: hard +"unique-slug@npm:^2.0.0": + version: 2.0.2 + resolution: "unique-slug@npm:2.0.2" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10/6cfaf91976acc9c125fd0686c561ee9ca0784bb4b2b408972e6cd30e747b4ff0ca50264c01bcf5e711b463535ea611ffb84199e9f73088cd79ac9ddee8154042 + languageName: node + linkType: hard + "unique-slug@npm:^3.0.0": version: 3.0.0 resolution: "unique-slug@npm:3.0.0" From 5d3eabce582d5f33c68b38152c16988f8fae4421 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Mon, 29 Jul 2024 14:16:05 -0400 Subject: [PATCH 04/35] More Viewer Data DB WIP --- libs/castmate-core/src/index.ts | 2 + .../src/viewer-data/viewer-data.ts | 104 +++++++++++++++--- libs/castmate-schema/src/index.ts | 1 + libs/castmate-schema/src/types/viewer-data.ts | 11 ++ libs/castmate-ui-core/src/main.ts | 2 + .../src/viewer-data/viewer-data-store.ts | 69 ++++++++++++ .../components/viewer-data/ViewerDataPage.vue | 11 ++ plugins/twitch/main/src/viewer-cache.ts | 73 +++++++++--- 8 files changed, 242 insertions(+), 31 deletions(-) create mode 100644 libs/castmate-schema/src/types/viewer-data.ts create mode 100644 libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts create mode 100644 packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue diff --git a/libs/castmate-core/src/index.ts b/libs/castmate-core/src/index.ts index 399a17ca..140adb4a 100644 --- a/libs/castmate-core/src/index.ts +++ b/libs/castmate-core/src/index.ts @@ -51,3 +51,5 @@ export * from "./analytics/analytics-manager" export * from "./info/info-manager" export * from "./util/time-utils" + +export * from "./viewer-data/viewer-data" diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index 583700ac..5fafe451 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -1,14 +1,17 @@ -import { IPCSchema, Schema, getTypeByConstructor, getTypeByName } from "castmate-schema" +import { + IPCSchema, + Schema, + constructDefault, + filterPromiseAll, + getTypeByConstructor, + getTypeByName, +} from "castmate-schema" import { Service } from "../util/service" import sqlite from "sqlite3" import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath } from "../io/file-system" -import { deserializeSchema, serializeSchema } from "../util/ipc-schema" +import { deserializeSchema, exposeSchema, serializeSchema } from "../util/ipc-schema" import { usePluginLogger } from "../logging/logging" - -interface ViewerVariable { - name: string - schema: Schema -} +import { ViewerVariable } from "castmate-schema" interface SerializedViewerVariable { name: string @@ -28,11 +31,24 @@ function escapeSql(sql: string) { return sql.replace(/\'/g, "''") } +export interface ViewerProvider { + readonly id: string + onDataChanged(id: string, column: string, value: any): any + onColumnAdded(column: string, defaultValue: any): any + onColumnRemoved(column: string): any +} + export const ViewerData = Service( class { private db: sqlite.Database - private variables: ViewerVariable[] = [] + private _variables: ViewerVariable[] = [] + + get variables() { + return this._variables + } + + private providers = new Map() constructor() {} @@ -66,6 +82,15 @@ export const ViewerData = Service( }) } + private all(sql: string, params?: any) { + return new Promise((resolve, reject) => { + this.db.all(sql, params, (err, row) => { + if (err) return reject(err) + resolve(row as T[]) + }) + }) + } + private ensureColumn(variable: ViewerVariable) { try { const schemaType = getTypeByConstructor(variable.schema.type) @@ -73,7 +98,7 @@ export const ViewerData = Service( const sqlType = sqlTypes[schemaType.name] ?? "BLOB" - this.run(`ALTER TABLE ViewerData ADD COLUMN ${variable.name} ${sqlType}`) + this.run("ALTER TABLE ViewerData ADD COLUMN ? ?", [variable.name, sqlType]) } catch {} } @@ -100,7 +125,7 @@ export const ViewerData = Service( schema.default = defaultValue } - this.variables.push({ + this._variables.push({ name: varData.name, schema, }) @@ -120,11 +145,17 @@ export const ViewerData = Service( await this.createDb() - await this.run("create table if not exists ViewerData (id text PRIMARY KEY)") + await this.run("CREATE TABLE IF NOT EXISTS ViewerData (twitch TEXT UNIQUE)") await this.loadVariables() } + async registerProvider(provider: ViewerProvider) { + //TODO: Ensure a column with the provider id exists and has a unique index + + this.providers.set(provider.id, provider) + } + shutdown() { return new Promise((resolve, reject) => { this.db.close((err) => { @@ -139,8 +170,16 @@ export const ViewerData = Service( name, schema, } + const defaultValue = await constructDefault(schema) + await this.ensureColumn(vari) this.variables.push(vari) + + const exposedDefault = await exposeSchema(schema, defaultValue) + + for (const provider of this.providers.values()) { + provider.onColumnAdded(name, exposedDefault) + } } async removeViewerVariable(name: string) { @@ -150,24 +189,26 @@ export const ViewerData = Service( await this.run("ALTER TABLE ViewerData DROP COLUMN ?", name) this.variables.splice(idx, 1) + + for (const provider of this.providers.values()) { + provider.onColumnRemoved(name) + } } async setViewerValue(provider: string, id: string, name: string, value: any) { - const dbId = `${provider}.${id}` - const vari = this.getVariable(name) if (!vari) return const serialized = await serializeSchema(vari.schema, value) - await this.run(`UPDATE ViewerData SET ? = ? WHERE id = ?`, [name, serialized, dbId]) + await this.run(`UPDATE ViewerData SET ? = ? WHERE ? = ?`, [name, serialized, provider, id]) + + this.providers.get(provider)?.onDataChanged(id, name, value) } async getViewerData(provider: string, id: string) { - const dbId = `${provider}.${id}` - try { - const data = await this.get>("SELECT * FROM ViewerData WHERE id = ?", [dbId]) + const data = await this.get>("SELECT * FROM ViewerData WHERE ? = ?", [provider, id]) const result: Record = {} @@ -180,5 +221,34 @@ export const ViewerData = Service( return undefined } } + + async getMultipleViewerData(provider: string, ids: string[]) { + try { + const data = await this.all>("SELECT * FROM ViewerData WHERE ? = ?", [ + provider, + ids, + ]) + + const result: (Record | undefined)[] = [] + + await Promise.allSettled( + data.map(async (row) => { + const idx = ids.findIndex((id) => id == row[provider]) + if (idx < 0) return + + const viewerData: Record = {} + for (const vari of this.variables) { + viewerData[vari.name] = await deserializeSchema(vari.schema, row[vari.name]) + } + + result[idx] = viewerData + }) + ) + + return result + } catch { + return [] + } + } } ) diff --git a/libs/castmate-schema/src/index.ts b/libs/castmate-schema/src/index.ts index 5332f751..33d0ecd2 100644 --- a/libs/castmate-schema/src/index.ts +++ b/libs/castmate-schema/src/index.ts @@ -24,6 +24,7 @@ export * from "./types/automations" export * from "./types/stream-plan" export * from "./types/emotes" export * from "./types/info" +export * from "./types/viewer-data" export * from "./util/type-helpers" export * from "./util/promise-helpers" diff --git a/libs/castmate-schema/src/types/viewer-data.ts b/libs/castmate-schema/src/types/viewer-data.ts new file mode 100644 index 00000000..02e6f378 --- /dev/null +++ b/libs/castmate-schema/src/types/viewer-data.ts @@ -0,0 +1,11 @@ +import { IPCSchema, Schema } from "../schema" + +export interface ViewerVariable { + name: string + schema: Schema +} + +export interface IPCViewerVariable { + name: string + schema: IPCSchema +} diff --git a/libs/castmate-ui-core/src/main.ts b/libs/castmate-ui-core/src/main.ts index 9b7eaa42..124eb69e 100644 --- a/libs/castmate-ui-core/src/main.ts +++ b/libs/castmate-ui-core/src/main.ts @@ -82,6 +82,8 @@ export * from "./resources/resource-store" export * from "./docking/docking-store" export * from "./queue-system/action-queue-store" +export * from "./viewer-data/viewer-data-store" + export * from "./components/stream-plan/stream-plan-types" export * from "./util/panning" diff --git a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts new file mode 100644 index 00000000..55ab8ff3 --- /dev/null +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -0,0 +1,69 @@ +import { IPCViewerVariable, ViewerVariable } from "castmate-schema" +import { defineStore } from "pinia" +import { computed, ref } from "vue" +import { handleIpcMessage, ipcParseSchema, useIpcCaller } from "../main" + +function parseDefinition(def: IPCViewerVariable): ViewerVariable { + return { + name: def.name, + schema: ipcParseSchema(def.schema), + } +} + +export const useViewerDataStore = defineStore("viewer-data", () => { + const variables = ref(new Map()) + + const getVariables = useIpcCaller<() => IPCViewerVariable[]>("viewer-data", "getVariables") + + const subscribeToViewerData = useIpcCaller<(provider: string, id: string) => Record>( + "viewer-data", + "subscribeToViewerData" + ) + const unsubscribeToViewerData = useIpcCaller<(provider: string, id: string) => any>( + "viewer-data", + "unsubscribeToViewerData" + ) + + const viewerData = ref(new Map>()) + + async function initialize() { + const vars = await getVariables() + + for (const column of vars) { + variables.value.set(column.name, parseDefinition(column)) + } + + handleIpcMessage("viewer-data", "columnAdded", (event, ipcDef: IPCViewerVariable) => { + variables.value.set(ipcDef.name, parseDefinition(ipcDef)) + }) + + handleIpcMessage("viewer-data", "columnRemoved", (event, name: string) => { + variables.value.delete(name) + }) + + handleIpcMessage( + "viewer-data", + "viewerDataChanged", + (event, provider: string, id: string, data: Record) => { + viewerData.value.set(`${provider}-${id}`, data) + } + ) + } + + async function subscribeToViewer(provider: string, id: string) { + const initialData = await subscribeToViewerData(provider, id) + viewerData.value.set(`${provider}-${id}`, initialData) + } + + async function unsubscribeToViewer(provider: string, id: string) { + await unsubscribeToViewerData(provider, id) + viewerData.value.delete(`${provider}-${id}`) + } + + return { + initialize, + variables: computed(() => variables.value), + subscribeToViewer, + unsubscribeToViewer, + } +}) diff --git a/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue b/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue new file mode 100644 index 00000000..7385dd2c --- /dev/null +++ b/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/plugins/twitch/main/src/viewer-cache.ts b/plugins/twitch/main/src/viewer-cache.ts index 018b9904..6efe99b4 100644 --- a/plugins/twitch/main/src/viewer-cache.ts +++ b/plugins/twitch/main/src/viewer-cache.ts @@ -3,6 +3,7 @@ import { EventList, ReactiveRef, Service, + ViewerData, defineRendererCallable, measurePerf, measurePerfFunc, @@ -44,6 +45,7 @@ interface CachedTwitchViewer extends Partial { id: string [Symbol.toPrimitive](hint: "default" | "string" | "number"): any lastSeen?: number + [key: string]: any } function getNValues(set: Set, requiredValues: T[], n: number): T[] { @@ -157,13 +159,14 @@ export const ViewerCache = Service( private vips = new Set() private mods = new Set() - private _viewerLookup = new Map>() - private _nameLookup = new Map() + private viewerLookup = new Map>() + private nameLookup = new Map() //Colors and SubInfo could be too numerous to prime so we'll lazily collect ids to query private unknownColors = new Set() private unknownSubInfo = new Set() private unknownUserInfo = new Set() + private unknownViewerData = new Set() private chatters = new Map() private chatterQueryTimer: NodeJS.Timeout | undefined = undefined @@ -173,14 +176,33 @@ export const ViewerCache = Service( onViewerSeen = new EventList<(viewer: TwitchViewerUnresolved) => any>() - constructor() {} + constructor() { + ViewerData.getInstance().registerProvider({ + id: "twitch", + onDataChanged: async (id, column, value) => { + const cached = this.getOrCreate(id) + cached[column] = value + }, + onColumnAdded: async (column, defaultValue) => { + for (const cached of this.viewerLookup.values()) { + cached.value[column] = defaultValue + } + }, + onColumnRemoved: async (column) => { + for (const cached of this.viewerLookup.values()) { + delete cached.value[column] + } + }, + }) + } async resetCache() { - this._nameLookup = new Map() - this._viewerLookup = new Map() + this.nameLookup = new Map() + this.viewerLookup = new Map() this.unknownColors = new Set() this.unknownSubInfo = new Set() this.unknownUserInfo = new Set() + this.unknownViewerData = new Set() this.vips = new Set() this.mods = new Set() this.chatters = new Map() @@ -229,7 +251,7 @@ export const ViewerCache = Service( } private get(userId: string) { - const cached = this._viewerLookup.get(userId) + const cached = this.viewerLookup.get(userId) if (!cached) throw new Error("Tried to get user out of cache that hasn't been cached") return cached.value } @@ -247,7 +269,7 @@ export const ViewerCache = Service( if (userId == "") throw new Error("No empty IDs!") if (userId == "anonymous") throw new Error("No anonymous!") - let cached = this._viewerLookup.get(userId) + let cached = this.viewerLookup.get(userId) if (!cached) { //Store our users as reactive so if they get used in a condition or overlay template they will update it //when the cache is updated @@ -258,7 +280,7 @@ export const ViewerCache = Service( return 0 }, }) - this._viewerLookup.set(userId, cached) + this.viewerLookup.set(userId, cached) this.unknownColors.add(userId) this.unknownSubInfo.add(userId) this.unknownUserInfo.add(userId) @@ -307,12 +329,12 @@ export const ViewerCache = Service( if (viewer.displayName != name) { const nameLower = name.toLowerCase() if (viewer.displayName != null) { - this._nameLookup.delete(nameLower) + this.nameLookup.delete(nameLower) } viewer.displayName = name - this._nameLookup.set(nameLower, viewer) + this.nameLookup.set(nameLower, viewer) } } @@ -447,6 +469,24 @@ export const ViewerCache = Service( return cached.followDate }*/ + private async queryViewerData(...userIds: string[]) { + const data = await ViewerData.getInstance().getMultipleViewerData("twitch", userIds) + if (!data) return + + for (let i = 0; i < userIds.length; ++i) { + const id = userIds[i] + const userData = data[i] + + this.unknownViewerData.delete(id) + + if (userData == null) continue + + const cached = this.getOrCreate(id) + + Object.assign(cached, userData) + } + } + async getIsVIP(userId: string): Promise { return this.vips.has(userId) } @@ -562,6 +602,7 @@ export const ViewerCache = Service( const neededColorIds: string[] = [] const neededUserInfoIds: string[] = [] const neededFollowerIds: string[] = [] + const neededViewerDataIds: string[] = [] const cachedUsers = userIds.map((id) => { if (id == "anonymous") return TwitchViewer.anonymous @@ -608,6 +649,10 @@ export const ViewerCache = Service( queryPromises.push(this.queryUserInfo(...neededUserInfoIds)) } + if (neededViewerDataIds.length > 0) { + queryPromises.push(this.queryViewerData(...neededViewerDataIds)) + } + await Promise.all(queryPromises) //Safe to cast here since we've resolved everything @@ -726,12 +771,12 @@ export const ViewerCache = Service( } async validateUserId(userId: string) { - const cached = this._viewerLookup.get(userId) + const cached = this.viewerLookup.get(userId) if (cached) return true await this.queryUserInfo(userId) - const cached2 = this._viewerLookup.get(userId) + const cached2 = this.viewerLookup.get(userId) return cached2 != null } @@ -752,7 +797,7 @@ export const ViewerCache = Service( name = name.substring(1) } const nameLower = name.toLowerCase() - let existing = this._nameLookup.get(nameLower) + let existing = this.nameLookup.get(nameLower) if (existing) return existing.id const user = await TwitchAccount.channel.apiClient.users.getUserByName(name) @@ -765,7 +810,7 @@ export const ViewerCache = Service( } async fuzzyUserCacheQuery(query: string, max: number = 10) { - const viewers = [...this._nameLookup.values()].filter((v) => v.displayName != null) + const viewers = [...this.nameLookup.values()].filter((v) => v.displayName != null) const fuzzySearch = fuzzysort.go(query, viewers, { key: "displayName", limit: max }) const result = fuzzySearch.map((r) => r.obj) From d0ccbdaa58b5720ceba1022d909c9b13a39e43e3 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 2 Aug 2024 17:00:33 -0400 Subject: [PATCH 05/35] Switch to using better-sqlite3 --- libs/castmate-core/package.json | 2 + .../src/viewer-data/viewer-data.ts | 169 ++++++++++++------ yarn.lock | 31 +++- 3 files changed, 135 insertions(+), 67 deletions(-) diff --git a/libs/castmate-core/package.json b/libs/castmate-core/package.json index 4d6e6aec..6f559de8 100644 --- a/libs/castmate-core/package.json +++ b/libs/castmate-core/package.json @@ -11,6 +11,7 @@ "license": "ISC", "type": "module", "devDependencies": { + "@types/better-sqlite3": "^7.6.11", "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.14", "@types/lodash": "^4.14.192", @@ -25,6 +26,7 @@ "@colors/colors": "^1.6.0", "@joshyour/ffprobe-client": "^1.1.7", "@types/fluent-ffmpeg": "^2.1.21", + "better-sqlite3": "^11.1.2", "castmate-schema": "workspace:^", "chokidar": "^3.5.3", "electron": "29.4.5", diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index 5fafe451..bb86cf2d 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -7,13 +7,13 @@ import { getTypeByName, } from "castmate-schema" import { Service } from "../util/service" -import sqlite from "sqlite3" +import sqlite from "better-sqlite3" import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath } from "../io/file-system" import { deserializeSchema, exposeSchema, serializeSchema } from "../util/ipc-schema" import { usePluginLogger } from "../logging/logging" import { ViewerVariable } from "castmate-schema" -interface SerializedViewerVariable { +interface SerializedViewerVariableDesc { name: string type: string defaultValue?: any @@ -38,10 +38,61 @@ export interface ViewerProvider { onColumnRemoved(column: string): any } +function createAddColumnStatement(db: sqlite.Database) { + return db.prepare<{ + columnName: string + columnType: string + columnDefault: any + }>("ALTER TABLE ViewerData ADD COLUMN :columnName :columnType default :columnDefault") +} +type AddColumnStatement = ReturnType + +function createCreateTableStatement(db: sqlite.Database) { + return db.prepare<[]>("CREATE TABLE IF NOT EXISTS ViewerData (twitch TEXT UNIQUE, twitch_name TEXT)") +} +type CreateTableStatement = ReturnType + +function createRemoveColumnStatement(db: sqlite.Database) { + return db.prepare<{ columnName: string }>("ALTER TABLE ViewerData DROP COLUMN ?") +} +type RemoveTableStatement = ReturnType + +function createSetColumnValueStatement(db: sqlite.Database) { + return db.prepare<{ + provider: string + providerName: string + columnName: string + id: string + displayName: string + columnValue: any + }>( + `INSERT INTO ViewerData(:provider, :providerName, :columnName) VALUES(:id, :displayName, :columnValue) ON CONFLICT(:provider) DO UPDATE SET :columnName=:columnValue, :providerName=:displayName` + ) +} +type SetColumnValueStatement = ReturnType + +function createGetColumnValueStatement(db: sqlite.Database) { + return db.prepare< + { + provider: string + ids: string | string[] + }, + Record + >("SELECT * FROM ViewerData WHERE :provider = :ids") +} + +type GetColumnValueStatement = ReturnType + export const ViewerData = Service( class { private db: sqlite.Database + private addColumnStatement: AddColumnStatement + private createTableStatement: CreateTableStatement + private removeTableStatement: RemoveTableStatement + private setColumnValueStatement: SetColumnValueStatement + private getColumnValueStatement: GetColumnValueStatement + private _variables: ViewerVariable[] = [] get variables() { @@ -55,57 +106,33 @@ export const ViewerData = Service( private createDb(): Promise { return new Promise((resolve, reject) => { const path = resolveProjectPath("/viewer-data/db.sqlite3") - this.db = new sqlite.Database(path, (err) => { - if (err) { - return reject(err) - } - resolve() - }) - }) - } - - private run(sql: string, params?: any) { - return new Promise((resolve, reject) => { - this.db.run(sql, params, (err) => { - if (err) return reject(err) - resolve() - }) - }) - } - - private get(sql: string, params?: any) { - return new Promise((resolve, reject) => { - this.db.get(sql, params, (err, row) => { - if (err) return reject(err) - resolve(row as T) - }) + this.db = sqlite(path) + resolve() }) } - private all(sql: string, params?: any) { - return new Promise((resolve, reject) => { - this.db.all(sql, params, (err, row) => { - if (err) return reject(err) - resolve(row as T[]) - }) - }) - } - - private ensureColumn(variable: ViewerVariable) { + private async ensureColumn(variable: ViewerVariable) { try { const schemaType = getTypeByConstructor(variable.schema.type) if (!schemaType) return const sqlType = sqlTypes[schemaType.name] ?? "BLOB" - this.run("ALTER TABLE ViewerData ADD COLUMN ? ?", [variable.name, sqlType]) + const defaultValue = await constructDefault(variable.schema) + const serializedDefault = await serializeSchema(variable.schema, defaultValue) + + this.addColumnStatement.run({ + columnName: variable.name, + columnType: sqlType, + columnDefault: serializedDefault, + }) } catch {} } private async loadVariables() { await ensureYAML([], "/viewer-data/variables.yaml") - const data: SerializedViewerVariable[] = await loadYAML("/viewer-data/variables.yaml") + const data: SerializedViewerVariableDesc[] = await loadYAML("/viewer-data/variables.yaml") for (const varData of data) { const type = getTypeByName(varData.type) @@ -145,7 +172,11 @@ export const ViewerData = Service( await this.createDb() - await this.run("CREATE TABLE IF NOT EXISTS ViewerData (twitch TEXT UNIQUE)") + this.addColumnStatement = createAddColumnStatement(this.db) + this.removeTableStatement = createRemoveColumnStatement(this.db) + this.createTableStatement = createCreateTableStatement(this.db) + + await this.createTableStatement.run() await this.loadVariables() } @@ -156,13 +187,8 @@ export const ViewerData = Service( this.providers.set(provider.id, provider) } - shutdown() { - return new Promise((resolve, reject) => { - this.db.close((err) => { - if (err) return reject(err) - resolve() - }) - }) + async shutdown() { + this.db.close() } async addViewerVariable(name: string, schema: Schema) { @@ -170,6 +196,10 @@ export const ViewerData = Service( name, schema, } + + const existing = this.getVariable(name) + if (existing) throw new Error(`Viewer Variable with name ${name} already exists`) + const defaultValue = await constructDefault(schema) await this.ensureColumn(vari) @@ -186,7 +216,7 @@ export const ViewerData = Service( const idx = this.variables.findIndex((v) => v.name == name) if (idx < 0) return - await this.run("ALTER TABLE ViewerData DROP COLUMN ?", name) + await this.removeTableStatement.run({ columnName: name }) this.variables.splice(idx, 1) @@ -195,25 +225,49 @@ export const ViewerData = Service( } } - async setViewerValue(provider: string, id: string, name: string, value: any) { - const vari = this.getVariable(name) + async setViewerValue(provider: string, id: string, displayName: string, varname: string, value: any) { + const vari = this.getVariable(varname) if (!vari) return const serialized = await serializeSchema(vari.schema, value) - await this.run(`UPDATE ViewerData SET ? = ? WHERE ? = ?`, [name, serialized, provider, id]) + this.db.transaction(() => {}) + await this.setColumnValueStatement.run({ + provider, + providerName: `${provider}_name`, + columnName: varname, + columnValue: serialized, + id, + displayName, + }) + + this.providers.get(provider)?.onDataChanged(id, varname, value) + } + + private async getDefaultViewerData() { + let result: Record = {} - this.providers.get(provider)?.onDataChanged(id, name, value) + for (const vari of this.variables) { + const value = await constructDefault(vari.schema) + const exposed = await exposeSchema(vari.schema, value) + result[vari.name] = exposed + } + + return result } async getViewerData(provider: string, id: string) { try { - const data = await this.get>("SELECT * FROM ViewerData WHERE ? = ?", [provider, id]) + const data = await this.getColumnValueStatement.get({ provider, ids: id }) + + if (!data) return undefined const result: Record = {} for (const vari of this.variables) { - result[vari.name] = await deserializeSchema(vari.schema, data[vari.name]) + const deserialized = await deserializeSchema(vari.schema, data[vari.name]) + const exposed = await exposeSchema(vari.schema, deserialized) + result[vari.name] = exposed } return result @@ -224,10 +278,7 @@ export const ViewerData = Service( async getMultipleViewerData(provider: string, ids: string[]) { try { - const data = await this.all>("SELECT * FROM ViewerData WHERE ? = ?", [ - provider, - ids, - ]) + const data = await this.getColumnValueStatement.all({ provider, ids }) const result: (Record | undefined)[] = [] @@ -238,7 +289,9 @@ export const ViewerData = Service( const viewerData: Record = {} for (const vari of this.variables) { - viewerData[vari.name] = await deserializeSchema(vari.schema, row[vari.name]) + const deserialized = await deserializeSchema(vari.schema, row[vari.name]) + const exposed = await exposeSchema(vari.schema, deserialized) + viewerData[vari.name] = exposed } result[idx] = viewerData diff --git a/yarn.lock b/yarn.lock index 89241ddf..988eab0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1332,6 +1332,15 @@ __metadata: languageName: node linkType: hard +"@types/better-sqlite3@npm:^7.6.11": + version: 7.6.11 + resolution: "@types/better-sqlite3@npm:7.6.11" + dependencies: + "@types/node": "npm:*" + checksum: 10/660f29485803e8f1a443b6009c48d172130e5d91f5b2728d8868ed9b39650cf5835b6747fb61141ea4f706dd321fcbf442dac245437ed7f3511901d3578380d3 + languageName: node + linkType: hard + "@types/body-parser@npm:*": version: 1.19.5 resolution: "@types/body-parser@npm:1.19.5" @@ -2475,6 +2484,17 @@ __metadata: languageName: node linkType: hard +"better-sqlite3@npm:^11.1.2": + version: 11.1.2 + resolution: "better-sqlite3@npm:11.1.2" + dependencies: + bindings: "npm:^1.5.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + checksum: 10/0427f596149a8dead90d7e80d948a281292dc1bd88dfa7feaea1277e4673c75a724b70bcd460baf92a067118688d656bc4aa94f1fe977767722ad3e282488f03 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -2846,6 +2866,7 @@ __metadata: "@azure/web-pubsub-client": "npm:^1.0.0" "@colors/colors": "npm:^1.6.0" "@joshyour/ffprobe-client": "npm:^1.1.7" + "@types/better-sqlite3": "npm:^7.6.11" "@types/express": "npm:^4.17.21" "@types/fluent-ffmpeg": "npm:^2.1.21" "@types/http-proxy": "npm:^1.17.14" @@ -2853,6 +2874,7 @@ __metadata: "@types/node": "npm:*" "@types/semver": "npm:^7.5.8" "@types/yaml": "npm:^1.9.7" + better-sqlite3: "npm:^11.1.2" castmate-schema: "workspace:^" chokidar: "npm:^3.5.3" electron: "npm:29.4.5" @@ -7723,15 +7745,6 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^5.0.7": - version: 5.0.7 - resolution: "nanoid@npm:5.0.7" - bin: - nanoid: bin/nanoid.js - checksum: 10/25ab0b0cf9082ae6747f0f55cec930e6c1cc5975103aa3a5fda44be5720eff57d9b25a8a9850274bfdde8def964b49bf03def71c6aa7ad1cba32787819b79f60 - languageName: node - linkType: hard - "napi-build-utils@npm:^1.0.1": version: 1.0.2 resolution: "napi-build-utils@npm:1.0.2" From 810dbc482103b5f26ddf48c9fb0c8653f8737595 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Mon, 5 Aug 2024 16:10:45 -0400 Subject: [PATCH 06/35] Actually init all the statements --- libs/castmate-core/src/viewer-data/viewer-data.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index bb86cf2d..b6e23a36 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -175,6 +175,8 @@ export const ViewerData = Service( this.addColumnStatement = createAddColumnStatement(this.db) this.removeTableStatement = createRemoveColumnStatement(this.db) this.createTableStatement = createCreateTableStatement(this.db) + this.getColumnValueStatement = createGetColumnValueStatement(this.db) + this.setColumnValueStatement = createSetColumnValueStatement(this.db) await this.createTableStatement.run() From fa0bada29d4f941ff70bf461268ec88630b78895 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 16 Aug 2024 14:01:16 -0400 Subject: [PATCH 07/35] Actually Init the Viewer Data Service --- libs/castmate-core/src/system.ts | 3 +++ packages/castmate/src/renderer/index.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/libs/castmate-core/src/system.ts b/libs/castmate-core/src/system.ts index 1b526e4e..5a534e36 100644 --- a/libs/castmate-core/src/system.ts +++ b/libs/castmate-core/src/system.ts @@ -20,6 +20,7 @@ import { app } from "electron" import path from "path" import { InfoService } from "./info/info-manager" import { AnalyticsService } from "./analytics/analytics-manager" +import { ViewerData } from "./viewer-data/viewer-data" /* //This shit is dynamic and vite hates it. @@ -90,6 +91,8 @@ export async function initializeCastMate() { SequenceResolvers.initialize() EmoteCache.initialize() setupStreamPlans() + ViewerData.initialize() + await ViewerData.getInstance().initialize() //How do we load plugins??? //await loadPlugin("twitch") diff --git a/packages/castmate/src/renderer/index.ts b/packages/castmate/src/renderer/index.ts index 5a8d5fad..55dd2d04 100644 --- a/packages/castmate/src/renderer/index.ts +++ b/packages/castmate/src/renderer/index.ts @@ -11,6 +11,7 @@ import { useIpcCaller, initializeStreamPlans, useStreamPlanStore, + useViewerDataStore, } from "castmate-ui-core" import { createApp } from "vue" import App from "./App.vue" @@ -102,6 +103,7 @@ const actionQueueStore = useActionQueueStore() const dashboardStore = useDashboardStore() const mediaStore = useMediaStore() const planStore = useStreamPlanStore() +const viewerDataStore = useViewerDataStore() const uiLoadComplete = useIpcCaller("plugins", "uiLoadComplete") @@ -135,6 +137,8 @@ async function init() { initializeQueues() + await viewerDataStore.initialize() + await initOverlaysPlugin(app) await initVariablesPlugin() From ee750c5dafa7ee2ec83ef792797e62ae401c0403 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 16 Aug 2024 14:02:07 -0400 Subject: [PATCH 08/35] Add ViewerData IPC and Fix SQL Queries not compatible with prepared statements --- .../src/viewer-data/viewer-data.ts | 124 ++++++++++++++++-- .../src/viewer-data/viewer-data-store.ts | 88 ++++++++++--- 2 files changed, 180 insertions(+), 32 deletions(-) diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index b6e23a36..bfbfe675 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -1,5 +1,6 @@ import { IPCSchema, + IPCViewerVariable, Schema, constructDefault, filterPromiseAll, @@ -9,9 +10,10 @@ import { import { Service } from "../util/service" import sqlite from "better-sqlite3" import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath } from "../io/file-system" -import { deserializeSchema, exposeSchema, serializeSchema } from "../util/ipc-schema" +import { deserializeSchema, exposeSchema, ipcConvertSchema, serializeSchema } from "../util/ipc-schema" import { usePluginLogger } from "../logging/logging" import { ViewerVariable } from "castmate-schema" +import { defineCallableIPC, defineIPCFunc } from "../util/electron" interface SerializedViewerVariableDesc { name: string @@ -39,11 +41,16 @@ export interface ViewerProvider { } function createAddColumnStatement(db: sqlite.Database) { + return function (args: { columnName: string; columnType: string; columnDefault: any }) { + db.exec(`ALTER TABLE ViewerData ADD COLUMN ${args.columnName} ${args.columnType} default ${args.columnDefault}`) + } + /* return db.prepare<{ columnName: string columnType: string columnDefault: any - }>("ALTER TABLE ViewerData ADD COLUMN :columnName :columnType default :columnDefault") + }>("ALTER TABLE ViewerData ADD COLUMN @columnName @columnType default @columnDefault") +*/ } type AddColumnStatement = ReturnType @@ -53,11 +60,44 @@ function createCreateTableStatement(db: sqlite.Database) { type CreateTableStatement = ReturnType function createRemoveColumnStatement(db: sqlite.Database) { - return db.prepare<{ columnName: string }>("ALTER TABLE ViewerData DROP COLUMN ?") + return function (args: { columnName: string }) { + db.exec(`ALTER TABLE ViewerData DROP COLUMN ${args.columnName}`) + } + + //return db.prepare<{ columnName: string }>("ALTER TABLE ViewerData DROP COLUMN :columnName") } type RemoveTableStatement = ReturnType +function createPagedQueryStatement(db: sqlite.Database) { + return db.prepare<{ start: number; quantity: number }>("SELECT * FROM ViewerData LIMIT :quantity OFFSET :start") +} +type PagedQueryStatement = ReturnType + +function createPagedQueryOrderedStatement(db: sqlite.Database) { + return function (args: { start: number; quantity: number; orderBy: string; order: string }) { + return `SELECT * FROM ViewerData LIMIT ${args.quantity} OFFSET ${args.start} ORDER BY ${args.orderBy} ${args.order}` + } +} +type PagedQueryOrderedStatement = ReturnType + function createSetColumnValueStatement(db: sqlite.Database) { + return function (args: { + provider: string + columnName: string + id: string + displayName: string + columnValue: any + }) { + //TODO: ESCAPE! + db.exec( + `INSERT INTO ViewerData(${args.provider}, ${args.provider}_name, ${args.columnName}) VALUES(${ + args.id + }, "${escapeSql(args.displayName)}", ${args.columnValue}) ON CONFLICT(${args.provider}) DO UPDATE SET ${ + args.columnName + }=${args.columnValue}, ${args.provider}_name="${escapeSql(args.displayName)}"` + ) + } + /* return db.prepare<{ provider: string providerName: string @@ -67,7 +107,7 @@ function createSetColumnValueStatement(db: sqlite.Database) { columnValue: any }>( `INSERT INTO ViewerData(:provider, :providerName, :columnName) VALUES(:id, :displayName, :columnValue) ON CONFLICT(:provider) DO UPDATE SET :columnName=:columnValue, :providerName=:displayName` - ) + )*/ } type SetColumnValueStatement = ReturnType @@ -83,6 +123,13 @@ function createGetColumnValueStatement(db: sqlite.Database) { type GetColumnValueStatement = ReturnType +const rendererViewerDataChanged = defineCallableIPC< + (provider: string, id: string, varName: string, value: any) => void +>("viewer-data", "viewerDataChanged") + +const rendererColumnAdded = defineCallableIPC<(ipcDef: IPCViewerVariable) => void>("viewer-data", "columnAdded") +const rendererColumnRemoved = defineCallableIPC<(name: string) => void>("viewer-data", "columnRemoved") + export const ViewerData = Service( class { private db: sqlite.Database @@ -92,6 +139,8 @@ export const ViewerData = Service( private removeTableStatement: RemoveTableStatement private setColumnValueStatement: SetColumnValueStatement private getColumnValueStatement: GetColumnValueStatement + private pagedQueryStatement: PagedQueryStatement + private pagedQueryOrderedStatement: PagedQueryOrderedStatement private _variables: ViewerVariable[] = [] @@ -103,12 +152,11 @@ export const ViewerData = Service( constructor() {} - private createDb(): Promise { - return new Promise((resolve, reject) => { - const path = resolveProjectPath("/viewer-data/db.sqlite3") - this.db = sqlite(path) - resolve() - }) + private async createDb(): Promise { + await ensureDirectory(resolveProjectPath("viewer-data")) + const path = resolveProjectPath("viewer-data", "db.sqlite3") + logger.log("Creating ViewerData DB", path) + this.db = sqlite(path) } private async ensureColumn(variable: ViewerVariable) { @@ -172,15 +220,32 @@ export const ViewerData = Service( await this.createDb() + this.createTableStatement = createCreateTableStatement(this.db) + await this.createTableStatement.run() + this.addColumnStatement = createAddColumnStatement(this.db) this.removeTableStatement = createRemoveColumnStatement(this.db) - this.createTableStatement = createCreateTableStatement(this.db) this.getColumnValueStatement = createGetColumnValueStatement(this.db) this.setColumnValueStatement = createSetColumnValueStatement(this.db) - - await this.createTableStatement.run() + this.pagedQueryStatement = createPagedQueryStatement(this.db) + this.pagedQueryOrderedStatement = createPagedQueryOrderedStatement(this.db) await this.loadVariables() + + defineIPCFunc("viewer-data", "getVariables", () => { + return this.variables.map((vari) => ({ + name: vari.name, + schema: ipcConvertSchema(vari.schema, `viewerData_${vari.name}`), + })) + }) + + defineIPCFunc( + "viewer-data", + "queryPagedData", + (start: number, end: number, sortBy: string | undefined, sortOrder: number | undefined) => { + this.getPagedViewerData(start, end, sortBy, sortOrder) + } + ) } async registerProvider(provider: ViewerProvider) { @@ -212,6 +277,11 @@ export const ViewerData = Service( for (const provider of this.providers.values()) { provider.onColumnAdded(name, exposedDefault) } + + rendererColumnAdded({ + name, + schema: ipcConvertSchema(schema, `viewerData_${name}`), + }) } async removeViewerVariable(name: string) { @@ -225,6 +295,8 @@ export const ViewerData = Service( for (const provider of this.providers.values()) { provider.onColumnRemoved(name) } + + rendererColumnRemoved(name) } async setViewerValue(provider: string, id: string, displayName: string, varname: string, value: any) { @@ -233,7 +305,6 @@ export const ViewerData = Service( const serialized = await serializeSchema(vari.schema, value) - this.db.transaction(() => {}) await this.setColumnValueStatement.run({ provider, providerName: `${provider}_name`, @@ -244,6 +315,8 @@ export const ViewerData = Service( }) this.providers.get(provider)?.onDataChanged(id, varname, value) + //Notify the UI + rendererViewerDataChanged(provider, id, varname, value) } private async getDefaultViewerData() { @@ -305,5 +378,28 @@ export const ViewerData = Service( return [] } } + + async getPagedViewerData( + start: number, + end: number, + sortBy: string | undefined, + sortOrder: number | undefined + ) { + if (!sortBy) { + const result = this.pagedQueryStatement.all({ + start, + quantity: end - start, + }) as Record[] + return result + } else { + const result = this.pagedQueryOrderedStatement.all({ + start, + quantity: end - start, + orderBy: sortBy, + order: sortOrder == null || sortOrder >= 0 ? "ASC" : "DESC", + }) as Record[] + return result + } + } } ) diff --git a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts index 55ab8ff3..c2ccb8ab 100644 --- a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -1,7 +1,8 @@ import { IPCViewerVariable, ViewerVariable } from "castmate-schema" import { defineStore } from "pinia" import { computed, ref } from "vue" -import { handleIpcMessage, ipcParseSchema, useIpcCaller } from "../main" +import { handleIpcMessage, ipcParseSchema, ProjectItem, useDockingStore, useIpcCaller, useProjectStore } from "../main" +import ViewerDataPage from "../components/viewer-data/ViewerDataPage.vue" function parseDefinition(def: IPCViewerVariable): ViewerVariable { return { @@ -10,21 +11,24 @@ function parseDefinition(def: IPCViewerVariable): ViewerVariable { } } +interface SubscribedViewerData { + refCount: number + data: Record +} + export const useViewerDataStore = defineStore("viewer-data", () => { const variables = ref(new Map()) + const dockingStore = useDockingStore() + const projectStore = useProjectStore() + const getVariables = useIpcCaller<() => IPCViewerVariable[]>("viewer-data", "getVariables") - const subscribeToViewerData = useIpcCaller<(provider: string, id: string) => Record>( - "viewer-data", - "subscribeToViewerData" - ) - const unsubscribeToViewerData = useIpcCaller<(provider: string, id: string) => any>( - "viewer-data", - "unsubscribeToViewerData" - ) + const queryPagedViewerData = useIpcCaller< + (start: number, end: number, sortBy: string | undefined, sortOrder: number | undefined) => Record[] + >("viewer-data", "queryPagedData") - const viewerData = ref(new Map>()) + const viewerData = ref(new Map()) async function initialize() { const vars = await getVariables() @@ -44,26 +48,74 @@ export const useViewerDataStore = defineStore("viewer-data", () => { handleIpcMessage( "viewer-data", "viewerDataChanged", - (event, provider: string, id: string, data: Record) => { - viewerData.value.set(`${provider}-${id}`, data) + (event, provider: string, id: string, varName: string, value: any) => { + const slug = `${provider}-${id}` + const existing = viewerData.value.get(slug) + if (existing) { + existing.data[varName] = value + } } ) + + const projectItem = computed(() => { + return { + id: "viewer-data", + title: "Viewer Data", + icon: "mdi mdi-table-account", + open() { + dockingStore.openPage("viewer-data", "Viewer Data", ViewerDataPage) + }, + } + }) + + projectStore.registerProjectGroupItem(projectItem) } - async function subscribeToViewer(provider: string, id: string) { - const initialData = await subscribeToViewerData(provider, id) - viewerData.value.set(`${provider}-${id}`, initialData) + function subscribeToViewer(provider: string, id: string, initialData: Record) { + const slug = `${provider}-${id}` + const existing = viewerData.value.get(slug) + if (existing) { + Object.assign(existing.data, initialData) + existing.refCount++ + return existing.data + } else { + viewerData.value.set(slug, { refCount: 1, data: initialData }) + return initialData + } } async function unsubscribeToViewer(provider: string, id: string) { - await unsubscribeToViewerData(provider, id) - viewerData.value.delete(`${provider}-${id}`) + const slug = `${provider}-${id}` + const existing = viewerData.value.get(slug) + if (existing) { + const remaining = --existing.refCount + if (remaining <= 0) { + viewerData.value.delete(slug) + } + } + } + + async function queryViewersPaged( + provider: string, + start: number, + end: number, + sortBy: string | undefined, + sortOrder: number | undefined + ) { + const data = await queryPagedViewerData(start, end, sortBy, sortOrder) + + for (let i = 0; i < data.length; ++i) { + const viewerData = data[i] + data[i] = subscribeToViewer(provider, viewerData[provider], viewerData) + } + + return data } return { initialize, variables: computed(() => variables.value), - subscribeToViewer, + queryViewersPaged, unsubscribeToViewer, } }) From 7e985cf2a1d81bb8c7b598c3c7f9c3f67fb6cf88 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 16 Aug 2024 14:31:52 -0400 Subject: [PATCH 09/35] Add Electron Rebuild so better-sqlite3 gets rebuilt for electron --- libs/castmate-core/package.json | 4 +++- packages/castmate/vite.config.mts | 1 + yarn.lock | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/castmate-core/package.json b/libs/castmate-core/package.json index 6f559de8..46e59e1e 100644 --- a/libs/castmate-core/package.json +++ b/libs/castmate-core/package.json @@ -5,12 +5,14 @@ "main": "src/index.ts", "devMain": "src/index.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "rebuild": "electron-rebuild -f -w better-sqlite3" }, "author": "", "license": "ISC", "type": "module", "devDependencies": { + "@electron/rebuild": "^3.6.0", "@types/better-sqlite3": "^7.6.11", "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.14", diff --git a/packages/castmate/vite.config.mts b/packages/castmate/vite.config.mts index 09731a9a..2c756d16 100644 --- a/packages/castmate/vite.config.mts +++ b/packages/castmate/vite.config.mts @@ -39,6 +39,7 @@ export default defineConfig({ "castmate-plugin-sound-native", "castmate-plugin-input-native", "node-screenshots", + "better-sqlite3", ], }, }, diff --git a/yarn.lock b/yarn.lock index 988eab0f..5ab2ea51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2865,6 +2865,7 @@ __metadata: dependencies: "@azure/web-pubsub-client": "npm:^1.0.0" "@colors/colors": "npm:^1.6.0" + "@electron/rebuild": "npm:^3.6.0" "@joshyour/ffprobe-client": "npm:^1.1.7" "@types/better-sqlite3": "npm:^7.6.11" "@types/express": "npm:^4.17.21" From f14b2dd3f7429d41f3036584fe470a8cc62e0cc2 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 16 Aug 2024 14:32:20 -0400 Subject: [PATCH 10/35] Move ViewerDataPage to main package --- .../components/viewer-data/ViewerDataPage.vue | 49 +++++++++++++++++++ .../components/viewer-data/ViewerDataPage.vue | 11 ----- 2 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue delete mode 100644 packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue diff --git a/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue b/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue new file mode 100644 index 00000000..b9e07a1d --- /dev/null +++ b/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue b/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue deleted file mode 100644 index 7385dd2c..00000000 --- a/packages/castmate/src/renderer/components/viewer-data/ViewerDataPage.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - From 950ae4681345ccf8308cff8a9ef9aa0666c7716b Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 16 Aug 2024 18:21:34 -0400 Subject: [PATCH 11/35] WIP Viewer Variables Added query for number of rows Fixed LazyLoad not running Fixed weird data table sizing CSS issue Added Set Viewer Variable Action to twitch plugin --- .../src/viewer-data/viewer-data.ts | 102 ++++++++++--- .../components/viewer-data/ViewerDataPage.vue | 139 +++++++++++++++--- .../viewer-data/ViewerVariableEditDialog.vue | 131 +++++++++++++++++ .../src/viewer-data/viewer-data-store.ts | 30 +++- plugins/twitch/main/src/main.ts | 2 + plugins/twitch/main/src/viewer-variables.ts | 64 ++++++++ .../src/components/VariableEditDialog.vue | 2 +- 7 files changed, 428 insertions(+), 42 deletions(-) create mode 100644 libs/castmate-ui-core/src/components/viewer-data/ViewerVariableEditDialog.vue create mode 100644 plugins/twitch/main/src/viewer-variables.ts diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index bfbfe675..c27d604e 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -9,11 +9,12 @@ import { } from "castmate-schema" import { Service } from "../util/service" import sqlite from "better-sqlite3" -import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath } from "../io/file-system" -import { deserializeSchema, exposeSchema, ipcConvertSchema, serializeSchema } from "../util/ipc-schema" +import { ensureDirectory, ensureYAML, loadYAML, resolveProjectPath, writeYAML } from "../io/file-system" +import { deserializeSchema, exposeSchema, ipcConvertSchema, ipcParseSchema, serializeSchema } from "../util/ipc-schema" import { usePluginLogger } from "../logging/logging" import { ViewerVariable } from "castmate-schema" import { defineCallableIPC, defineIPCFunc } from "../util/electron" +import { startPerfTime } from "../util/time-utils" interface SerializedViewerVariableDesc { name: string @@ -75,7 +76,10 @@ type PagedQueryStatement = ReturnType function createPagedQueryOrderedStatement(db: sqlite.Database) { return function (args: { start: number; quantity: number; orderBy: string; order: string }) { - return `SELECT * FROM ViewerData LIMIT ${args.quantity} OFFSET ${args.start} ORDER BY ${args.orderBy} ${args.order}` + const statement = db.prepare( + `SELECT * FROM ViewerData LIMIT ${args.quantity} OFFSET ${args.start} ORDER BY ${args.orderBy} ${args.order}` + ) + return statement.all() } } type PagedQueryOrderedStatement = ReturnType @@ -89,13 +93,18 @@ function createSetColumnValueStatement(db: sqlite.Database) { columnValue: any }) { //TODO: ESCAPE! - db.exec( - `INSERT INTO ViewerData(${args.provider}, ${args.provider}_name, ${args.columnName}) VALUES(${ - args.id - }, "${escapeSql(args.displayName)}", ${args.columnValue}) ON CONFLICT(${args.provider}) DO UPDATE SET ${ - args.columnName - }=${args.columnValue}, ${args.provider}_name="${escapeSql(args.displayName)}"` - ) + logger.log("SetColumnValue!") + logger.log(args) + + const query = `INSERT INTO ViewerData (${args.provider}, ${args.provider}_name, ${args.columnName}) VALUES('${ + args.id + }', '${escapeSql(args.displayName)}', ${args.columnValue}) ON CONFLICT(${args.provider}) DO UPDATE SET ${ + args.columnName + }=${args.columnValue}, ${args.provider}_name='${escapeSql(args.displayName)}'` + + logger.log(" ", query) + + db.exec(query) } /* return db.prepare<{ @@ -123,6 +132,13 @@ function createGetColumnValueStatement(db: sqlite.Database) { type GetColumnValueStatement = ReturnType +function createQueryNumViewers(db: sqlite.Database) { + //TODO: "IS THIS FAST?" + return db.prepare<[], { "COUNT(*)": number }>("SELECT COUNT(*) FROM ViewerData") +} + +type QueryNumViewersStatement = ReturnType + const rendererViewerDataChanged = defineCallableIPC< (provider: string, id: string, varName: string, value: any) => void >("viewer-data", "viewerDataChanged") @@ -141,6 +157,7 @@ export const ViewerData = Service( private getColumnValueStatement: GetColumnValueStatement private pagedQueryStatement: PagedQueryStatement private pagedQueryOrderedStatement: PagedQueryOrderedStatement + private queryNumViewerStatement: QueryNumViewersStatement private _variables: ViewerVariable[] = [] @@ -169,7 +186,7 @@ export const ViewerData = Service( const defaultValue = await constructDefault(variable.schema) const serializedDefault = await serializeSchema(variable.schema, defaultValue) - this.addColumnStatement.run({ + this.addColumnStatement({ columnName: variable.name, columnType: sqlType, columnDefault: serializedDefault, @@ -178,9 +195,9 @@ export const ViewerData = Service( } private async loadVariables() { - await ensureYAML([], "/viewer-data/variables.yaml") + await ensureYAML([], "viewer-data", "variables.yaml") - const data: SerializedViewerVariableDesc[] = await loadYAML("/viewer-data/variables.yaml") + const data: SerializedViewerVariableDesc[] = await loadYAML("viewer-data", "variables.yaml") for (const varData of data) { const type = getTypeByName(varData.type) @@ -211,6 +228,28 @@ export const ViewerData = Service( } } + private async saveVariables() { + const data = new Array() + + for (const vari of this.variables) { + const type = getTypeByConstructor(vari.schema.type) + if (!type) continue + + const serializedVar: SerializedViewerVariableDesc = { + name: vari.name, + type: type.name, + } + + if (vari.schema.default != null) { + serializedVar.defaultValue = await serializeSchema(vari.schema, vari.schema.default) + } + + data.push(serializedVar) + } + + await writeYAML(data, "viewer-data", "variables.yaml") + } + getVariable(name: string) { return this.variables.find((v) => v.name == name) } @@ -229,6 +268,7 @@ export const ViewerData = Service( this.setColumnValueStatement = createSetColumnValueStatement(this.db) this.pagedQueryStatement = createPagedQueryStatement(this.db) this.pagedQueryOrderedStatement = createPagedQueryOrderedStatement(this.db) + this.queryNumViewerStatement = createQueryNumViewers(this.db) await this.loadVariables() @@ -242,10 +282,20 @@ export const ViewerData = Service( defineIPCFunc( "viewer-data", "queryPagedData", - (start: number, end: number, sortBy: string | undefined, sortOrder: number | undefined) => { - this.getPagedViewerData(start, end, sortBy, sortOrder) + async (start: number, end: number, sortBy: string | undefined, sortOrder: number | undefined) => { + return await this.getPagedViewerData(start, end, sortBy, sortOrder) } ) + + defineIPCFunc("viewer-data", "createVariable", async (ipcVarDesc: IPCViewerVariable) => { + const schema = ipcParseSchema(ipcVarDesc.schema) + + await this.addViewerVariable(ipcVarDesc.name, schema) + }) + + defineIPCFunc("viewer-data", "getNumRows", async () => { + return await this.getNumRows() + }) } async registerProvider(provider: ViewerProvider) { @@ -271,6 +321,7 @@ export const ViewerData = Service( await this.ensureColumn(vari) this.variables.push(vari) + await this.saveVariables() const exposedDefault = await exposeSchema(schema, defaultValue) @@ -288,10 +339,12 @@ export const ViewerData = Service( const idx = this.variables.findIndex((v) => v.name == name) if (idx < 0) return - await this.removeTableStatement.run({ columnName: name }) + await this.removeTableStatement({ columnName: name }) this.variables.splice(idx, 1) + await this.saveVariables() + for (const provider of this.providers.values()) { provider.onColumnRemoved(name) } @@ -305,16 +358,21 @@ export const ViewerData = Service( const serialized = await serializeSchema(vari.schema, value) - await this.setColumnValueStatement.run({ + const perf = startPerfTime("Set Column Value") + await this.setColumnValueStatement({ provider, - providerName: `${provider}_name`, columnName: varname, columnValue: serialized, id, displayName, }) + perf.stop(logger) - this.providers.get(provider)?.onDataChanged(id, varname, value) + try { + await this.providers.get(provider)?.onDataChanged(id, varname, value) + } catch (err) { + logger.error("Error Updating Provider Data", id, varname, value, err) + } //Notify the UI rendererViewerDataChanged(provider, id, varname, value) } @@ -379,6 +437,10 @@ export const ViewerData = Service( } } + async getNumRows() { + return this.queryNumViewerStatement.get()?.["COUNT(*)"] ?? 0 + } + async getPagedViewerData( start: number, end: number, @@ -392,7 +454,7 @@ export const ViewerData = Service( }) as Record[] return result } else { - const result = this.pagedQueryOrderedStatement.all({ + const result = this.pagedQueryOrderedStatement({ start, quantity: end - start, orderBy: sortBy, diff --git a/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue b/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue index b9e07a1d..f092d457 100644 --- a/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue +++ b/libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue @@ -1,21 +1,39 @@ - - - + + + @@ -66,6 +72,8 @@ const sortOrder = ref() const { viewers, updateRange, loading } = useLazyViewerQuery(sortField, sortOrder) effect(() => { + console.log(sortField.value, " -> ", sortOrder.value) + for (const v of viewers.value) { console.log(v) } diff --git a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts index 58de33b6..445a275c 100644 --- a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -280,6 +280,15 @@ export function useLazyViewerQuery( viewerDataStore.unobserveViewers(observer) }) + watch( + () => ({ sortField: toValue(sortField), sortOrder: toValue(sortOrder) }), + () => { + console.log("RELOADING FOR SORT", toValue(sortField), toValue(sortOrder)) + lazyViewers.value = new Array(totalDataRows.value) + loadRange(toValue(first), toValue(last)) + } + ) + /*watch( () => ({ first: toValue(first), From 4d1d4db0a1d9d2f6e8841fceba16e698d92c3062 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Mon, 19 Aug 2024 17:10:33 -0400 Subject: [PATCH 15/35] Factor out force reloads into their own function --- .../src/viewer-data/viewer-data-store.ts | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts index 445a275c..e656b03b 100644 --- a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -266,13 +266,16 @@ export function useLazyViewerQuery( }, } + async function forceReload() { + lazyViewers.value = new Array(totalDataRows.value) + await loadRange(first, last) + } + onMounted(async () => { viewerDataStore.observeViewers(observer) totalDataRows.value = await getNumRows() lazyViewers.value = new Array(totalDataRows.value) - - //await loadRange(first, last) }) onBeforeUnmount(() => { @@ -283,22 +286,10 @@ export function useLazyViewerQuery( watch( () => ({ sortField: toValue(sortField), sortOrder: toValue(sortOrder) }), () => { - console.log("RELOADING FOR SORT", toValue(sortField), toValue(sortOrder)) - lazyViewers.value = new Array(totalDataRows.value) - loadRange(toValue(first), toValue(last)) + forceReload() } ) - /*watch( - () => ({ - first: toValue(first), - last: toValue(last), - }), - async (newRange, oldRange) => { - await updateRange(newRange.first, oldRange.first, newRange.last, oldRange.last) - } - )*/ - async function updateRange(newFirst: number, newLast: number) { console.log("UpdateRange!", newFirst, newLast) From d0832dc0334a8a438c3bd3fbed1be7d17d4d00ea Mon Sep 17 00:00:00 2001 From: LordTocs Date: Mon, 26 Aug 2024 17:18:05 -0400 Subject: [PATCH 16/35] Don't issue viewer cache queries on template viewers --- .../renderer/src/components/viewer/TwitchViewerInput.vue | 5 ++++- .../renderer/src/components/viewer/TwitchViewerView.vue | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/twitch/renderer/src/components/viewer/TwitchViewerInput.vue b/plugins/twitch/renderer/src/components/viewer/TwitchViewerInput.vue index 18b10b5e..534d30ac 100644 --- a/plugins/twitch/renderer/src/components/viewer/TwitchViewerInput.vue +++ b/plugins/twitch/renderer/src/components/viewer/TwitchViewerInput.vue @@ -65,6 +65,7 @@ import { TemplateToggle, DataInputBase, stopPropagation, + defaultStringIsTemplate, } from "castmate-ui-core" import { TwitchViewerUnresolved, SchemaTwitchViewer, TwitchViewerDisplayData } from "castmate-plugin-twitch-shared" import { computed, onMounted, ref, useModel, watch, nextTick } from "vue" @@ -97,8 +98,10 @@ const viewerStore = useViewerStore() async function queryDisplay() { if (!props.modelValue) { selectedDisplayData.value = undefined - } else { + } else if (!defaultStringIsTemplate(props.modelValue)) { selectedDisplayData.value = await viewerStore.getUserById(props.modelValue) + } else { + selectedDisplayData.value = undefined } } diff --git a/plugins/twitch/renderer/src/components/viewer/TwitchViewerView.vue b/plugins/twitch/renderer/src/components/viewer/TwitchViewerView.vue index 5ff960a2..59332491 100644 --- a/plugins/twitch/renderer/src/components/viewer/TwitchViewerView.vue +++ b/plugins/twitch/renderer/src/components/viewer/TwitchViewerView.vue @@ -4,11 +4,12 @@ {{ viewerDisplayData.displayName }} + {{ modelValue }} From de4c2bba37a01c26674e31149404b890dd508ec7 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Mon, 26 Aug 2024 17:18:42 -0400 Subject: [PATCH 17/35] Add Offset Viewer Variable action * Better handle sqlizing and desqlizing values (Could still be improved) --- .../src/viewer-data/viewer-data.ts | 181 +++++++++++++----- .../src/viewer-data/viewer-data-store.ts | 1 - plugins/twitch/main/src/viewer-variables.ts | 58 ++++++ 3 files changed, 193 insertions(+), 47 deletions(-) diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index 63e2a67b..823ccb51 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -28,6 +28,9 @@ const sqlTypes: Record = { String: "TEXT", Number: "REAL", Boolean: "INTEGER", + TwitchViewer: "TEXT", + Color: "TEXT", + LightColor: "TEXT", } function escapeSql(sql: string) { @@ -84,52 +87,27 @@ function createPagedQueryOrderedStatement(db: sqlite.Database) { } type PagedQueryOrderedStatement = ReturnType -function createSetColumnValueStatement(db: sqlite.Database) { - return function (args: { - provider: string - columnName: string - id: string - displayName: string - columnValue: any - }) { - const query = `INSERT INTO ViewerData (${args.provider}, ${args.provider}_name, ${args.columnName}) VALUES('${ - args.id - }', '${escapeSql(args.displayName)}', ${args.columnValue}) ON CONFLICT(${args.provider}) DO UPDATE SET ${ - args.columnName - }=${args.columnValue}, ${args.provider}_name='${escapeSql(args.displayName)}'` - - logger.log(" ", query) - - const statement = db.prepare(query) +function createUpdateColumnValue(db: sqlite.Database) { + return function (args: { provider: string; id: string; columnName: string; columnValue: any }) { + const query = `UPDATE ViewerData SET ${args.columnName}=${args.columnValue} WHERE ${args.provider}='${args.id}'` - const result = statement.all() + logger.log("Updating w/", query) - logger.log(" ", result) + db.exec(query) } - /* - return db.prepare<{ - provider: string - providerName: string - columnName: string - id: string - displayName: string - columnValue: any - }>( - `INSERT INTO ViewerData(:provider, :providerName, :columnName) VALUES(:id, :displayName, :columnValue) ON CONFLICT(:provider) DO UPDATE SET :columnName=:columnValue, :providerName=:displayName` - )*/ } -type SetColumnValueStatement = ReturnType +type UpdateColumnValue = ReturnType -function createUpdateColumnValue(db: sqlite.Database) { - return function (args: { provider: string; id: string; columnName: string; columnValue: any }) { - const query = `UPDATE ViewerData SET ${args.columnName}=${args.columnValue} WHERE ${args.provider}='${args.id}'` +function createOffsetColumnValue(db: sqlite.Database) { + return function (args: { provider: string; id: string; columnName: string; columnOffset: number }) { + const query = `UPDATE ViewerData SET ${args.columnName}=coalesce(${args.columnName}, 0)+(${args.columnOffset}) WHERE ${args.provider}='${args.id}'` logger.log("Updating w/", query) db.exec(query) } } -type UpdateColumnValue = ReturnType +type OffsetColumnValue = ReturnType function createInsertWithColumnValue(db: sqlite.Database) { return function (args: { @@ -150,7 +128,7 @@ function createInsertWithColumnValue(db: sqlite.Database) { } type InsertColumnValue = ReturnType -function createGetColumnValueStatement(db: sqlite.Database) { +function createGetAllValuesStatement(db: sqlite.Database) { return db.prepare< { provider: string @@ -160,6 +138,20 @@ function createGetColumnValueStatement(db: sqlite.Database) { >("SELECT * FROM ViewerData WHERE :provider = :ids") } +type GetAllValuesStatement = ReturnType + +function createGetColumnValueStatement(db: sqlite.Database) { + return function (args: { provider: string; id: string; columnName: string }) { + const query = `SELECT ${args.columnName} FROM ViewerData WHERE ${args.provider}='${args.id}'` + + logger.log("Getting Column Value w/", query) + + const statement = db.prepare>(query) + + return statement.get() + } +} + type GetColumnValueStatement = ReturnType function createQueryNumViewers(db: sqlite.Database) { @@ -188,13 +180,14 @@ export const ViewerData = Service( private addColumnStatement: AddColumnStatement private createTableStatement: CreateTableStatement private removeTableStatement: RemoveTableStatement - private setColumnValueStatement: SetColumnValueStatement - private getColumnValueStatement: GetColumnValueStatement + private getAllValuesStatement: GetAllValuesStatement private pagedQueryStatement: PagedQueryStatement private pagedQueryOrderedStatement: PagedQueryOrderedStatement private queryNumViewerStatement: QueryNumViewersStatement + private getValue: GetColumnValueStatement private updateValue: UpdateColumnValue + private offsetValue: OffsetColumnValue private insertValue: InsertColumnValue private _variables: ViewerVariable[] = [] @@ -302,14 +295,15 @@ export const ViewerData = Service( this.addColumnStatement = createAddColumnStatement(this.db) this.removeTableStatement = createRemoveColumnStatement(this.db) - this.getColumnValueStatement = createGetColumnValueStatement(this.db) - this.setColumnValueStatement = createSetColumnValueStatement(this.db) + this.getAllValuesStatement = createGetAllValuesStatement(this.db) this.pagedQueryStatement = createPagedQueryStatement(this.db) this.pagedQueryOrderedStatement = createPagedQueryOrderedStatement(this.db) this.queryNumViewerStatement = createQueryNumViewers(this.db) + this.getValue = createGetColumnValueStatement(this.db) this.insertValue = createInsertWithColumnValue(this.db) this.updateValue = createUpdateColumnValue(this.db) + this.offsetValue = createOffsetColumnValue(this.db) await this.loadVariables() @@ -398,12 +392,20 @@ export const ViewerData = Service( if (!vari) return const serialized = await serializeSchema(vari.schema, value) + let sqlized: string + if (typeof serialized == "number") { + sqlized = String(serialized) + } else if (typeof serialized == "string") { + sqlized = `'${escapeSql(serialized)}'` + } else { + sqlized = `'${escapeSql(JSON.stringify(serialized))}'` + } try { this.insertValue({ provider, columnName: varname, - columnValue: serialized, + columnValue: sqlized, id, displayName, }) @@ -425,7 +427,7 @@ export const ViewerData = Service( provider, id, columnName: varname, - columnValue: serialized, + columnValue: sqlized, }) try { @@ -436,7 +438,73 @@ export const ViewerData = Service( rendererViewerDataChanged(provider, id, varname, value) } catch (err) { - logger.error("Error Inserting New Viewer Data", id, varname, value, err) + logger.error("Error Updating Viewer Data", id, varname, value, err) + } + } + } + + async offsetViewerValue(provider: string, id: string, displayName: string, varname: string, offset: number) { + const vari = this.getVariable(varname) + if (!vari) return + if (vari.schema.type != Number) { + throw new Error("Can't offset a variable that's not a number!") + } + if (typeof offset != "number") { + throw new Error(`Can't use a non number offset (${offset})`) + } + if (isNaN(offset)) { + throw new Error(`Can't use a NaN as offset`) + } + + try { + const defaultNumber = await constructDefault(vari.schema) + const offsetDefault = (defaultNumber ?? 0) + offset + + this.insertValue({ + provider, + columnName: varname, + columnValue: offsetDefault, + id, + displayName, + }) + + try { + await this.providers.get(provider)?.onDataChanged(id, varname, offsetDefault) + } catch (err) { + logger.error("Error Updating Provider Data", id, varname, offsetDefault, err) + } + + const defaultValue = await this.getDefaultViewerData() + + defaultValue[varname] = offsetDefault + + rendererViewerDataAdded(provider, id, defaultValue) + } catch (err) { + try { + const value = this.db.transaction<() => number | undefined>(() => { + this.offsetValue({ + provider, + id, + columnName: varname, + columnOffset: offset, + }) + + const row = this.getValue({ provider, id, columnName: varname }) + + return row?.[varname] + })() + + logger.log("Offset Value", value) + + try { + await this.providers.get(provider)?.onDataChanged(id, varname, value) + } catch (err) { + logger.error("Error Updating Provider Data", id, varname, value, err) + } + + rendererViewerDataChanged(provider, id, varname, value) + } catch (err) { + logger.error("Error Offseting Viewer Data", id, varname, offset, err) } } } @@ -455,7 +523,7 @@ export const ViewerData = Service( async getViewerData(provider: string, id: string) { try { - const data = await this.getColumnValueStatement.get({ provider, ids: id }) + const data = await this.getAllValuesStatement.get({ provider, ids: id }) if (!data) return undefined @@ -475,7 +543,7 @@ export const ViewerData = Service( async getMultipleViewerData(provider: string, ids: string[]) { try { - const data = await this.getColumnValueStatement.all({ provider, ids }) + const data = await this.getAllValuesStatement.all({ provider, ids }) const result: (Record | undefined)[] = [] @@ -486,7 +554,28 @@ export const ViewerData = Service( const viewerData: Record = {} for (const vari of this.variables) { - const deserialized = await deserializeSchema(vari.schema, row[vari.name]) + const schemaType = getTypeByConstructor(vari.schema.type) + if (!schemaType) continue + + const sqlized = row[vari.name] + let desqlized: any + + if (typeof sqlized == "number") { + if (vari.schema.type == Boolean) { + desqlized = sqlized != 0 + } else { + desqlized = sqlized + } + } else if (typeof sqlized == "string") { + const sqlType = sqlTypes[schemaType.name] + if (sqlType == "TEXT") { + desqlized = sqlized + } else { + desqlized = JSON.parse(sqlized) + } + } + + const deserialized = await deserializeSchema(vari.schema, desqlized) const exposed = await exposeSchema(vari.schema, deserialized) viewerData[vari.name] = exposed } diff --git a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts index e656b03b..43383035 100644 --- a/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -242,7 +242,6 @@ export function useLazyViewerQuery( onViewerDataChanged(provider, id, varName, value) { console.log("changed", provider, id, varName, value) const existing = loadedViewers.value.get(`${provider}-${id}`) - console.log(existing, loadedViewers) if (existing) { existing[varName] = value } diff --git a/plugins/twitch/main/src/viewer-variables.ts b/plugins/twitch/main/src/viewer-variables.ts index d7875d91..666fd4da 100644 --- a/plugins/twitch/main/src/viewer-variables.ts +++ b/plugins/twitch/main/src/viewer-variables.ts @@ -61,4 +61,62 @@ export function setupViewerVariables() { ) }, }) + + defineAction({ + id: "offsetViewerVar", + name: "Offset Viewer Variable", + icon: "mdi mdi-account-alert", + config: { + type: Object, + properties: { + viewer: { type: TwitchViewer, required: true, name: "Viewer", default: "{{ viewer }}", template: true }, + variable: { + type: String, + name: "Variable", + required: true, + async enum() { + return ViewerData.getInstance() + .variables.filter((v) => v.schema.type == Number) + .map((d) => d.name) + }, + }, + offset: { + type: DynamicType, + template: true, + async dynamicType(context: { variable: string }) { + const variable = ViewerData.getInstance().getVariable(context.variable) + + if (!variable) { + return { + type: String, + name: "Value", + required: true, + } + } + + return { + ...variable.schema, + name: "Value", + template: true, + } + }, + }, + }, + }, + async invoke(config, contextData, abortSignal) { + const viewerDisp = await ViewerCache.getInstance().getDisplayDataById(config.viewer) + + if (!viewerDisp) throw new Error(`Unable to Resolve Twitch Viewer ${config.viewer}`) + + logger.log("Set viewer var", config.variable, config.offset, viewerDisp) + + await ViewerData.getInstance().offsetViewerValue( + "twitch", + config.viewer, + viewerDisp.displayName, + config.variable, + config.offset + ) + }, + }) } From 5bdf12d7996a4942dc9c3e2bc361238ed4910185 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 30 Aug 2024 17:11:37 -0400 Subject: [PATCH 18/35] Move Viewer Variable actions to the variables plugin --- plugins/twitch/main/src/main.ts | 4 +--- plugins/variables/main/package.json | 2 ++ plugins/variables/main/src/variable-plugin.ts | 2 ++ plugins/{twitch => variables}/main/src/viewer-variables.ts | 6 +++--- yarn.lock | 2 ++ 5 files changed, 10 insertions(+), 6 deletions(-) rename plugins/{twitch => variables}/main/src/viewer-variables.ts (91%) diff --git a/plugins/twitch/main/src/main.ts b/plugins/twitch/main/src/main.ts index 14e5ae8b..47a2d987 100644 --- a/plugins/twitch/main/src/main.ts +++ b/plugins/twitch/main/src/main.ts @@ -29,7 +29,6 @@ import { setup7tv } from "./seventv" import { setupCategoryCache } from "./category-cache" import { setupInfoManager } from "./info-manager" import { setupWalkOns } from "./walk-on" -import { setupViewerVariables } from "./viewer-variables" export default definePlugin( { @@ -91,7 +90,6 @@ export default definePlugin( setupEmotes() setup7tv() setupWalkOns() - setupViewerVariables() onLoad(async () => { await TwitchAPIService.getInstance().finalize() @@ -99,4 +97,4 @@ export default definePlugin( } ) -export { TwitchAccount, onChannelAuth, ChannelPointReward } +export { TwitchAccount, onChannelAuth, ChannelPointReward, ViewerCache } diff --git a/plugins/variables/main/package.json b/plugins/variables/main/package.json index 981f6de7..ad8f0372 100644 --- a/plugins/variables/main/package.json +++ b/plugins/variables/main/package.json @@ -14,6 +14,8 @@ }, "dependencies": { "castmate-core": "workspace:^", + "castmate-plugin-twitch-main": "workspace:^", + "castmate-plugin-twitch-shared": "workspace:^", "castmate-plugin-variables-shared": "workspace:^", "castmate-schema": "workspace:^" } diff --git a/plugins/variables/main/src/variable-plugin.ts b/plugins/variables/main/src/variable-plugin.ts index aa602a1c..be6c5fd6 100644 --- a/plugins/variables/main/src/variable-plugin.ts +++ b/plugins/variables/main/src/variable-plugin.ts @@ -1,5 +1,6 @@ import { definePlugin, usePluginLogger } from "castmate-core" import { setupVariableActions } from "./actions" +import { setupViewerVariables } from "./viewer-variables" export default definePlugin( { @@ -10,5 +11,6 @@ export default definePlugin( }, () => { setupVariableActions() + setupViewerVariables() } ) diff --git a/plugins/twitch/main/src/viewer-variables.ts b/plugins/variables/main/src/viewer-variables.ts similarity index 91% rename from plugins/twitch/main/src/viewer-variables.ts rename to plugins/variables/main/src/viewer-variables.ts index 666fd4da..4ee2b225 100644 --- a/plugins/twitch/main/src/viewer-variables.ts +++ b/plugins/variables/main/src/viewer-variables.ts @@ -1,7 +1,7 @@ import { defineAction, usePluginLogger, ViewerData } from "castmate-core" import { TwitchViewer } from "castmate-plugin-twitch-shared" import { DynamicType } from "castmate-schema" -import { ViewerCache } from "./viewer-cache" +import { ViewerCache as TwitchViewerCache } from "castmate-plugin-twitch-main" export function setupViewerVariables() { const logger = usePluginLogger() @@ -46,7 +46,7 @@ export function setupViewerVariables() { }, }, async invoke(config, contextData, abortSignal) { - const viewerDisp = await ViewerCache.getInstance().getDisplayDataById(config.viewer) + const viewerDisp = await TwitchViewerCache.getInstance().getDisplayDataById(config.viewer) if (!viewerDisp) throw new Error(`Unable to Resolve Twitch Viewer ${config.viewer}`) @@ -104,7 +104,7 @@ export function setupViewerVariables() { }, }, async invoke(config, contextData, abortSignal) { - const viewerDisp = await ViewerCache.getInstance().getDisplayDataById(config.viewer) + const viewerDisp = await TwitchViewerCache.getInstance().getDisplayDataById(config.viewer) if (!viewerDisp) throw new Error(`Unable to Resolve Twitch Viewer ${config.viewer}`) diff --git a/yarn.lock b/yarn.lock index 87e2d1a6..b85dd2cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4114,6 +4114,8 @@ __metadata: resolution: "castmate-plugin-variables-main@workspace:plugins/variables/main" dependencies: castmate-core: "workspace:^" + castmate-plugin-twitch-main: "workspace:^" + castmate-plugin-twitch-shared: "workspace:^" castmate-plugin-variables-shared: "workspace:^" castmate-schema: "workspace:^" typescript: "npm:*" From abb281e04b2733f42b2fb4d6aa212d4e7b2a288c Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 30 Aug 2024 18:39:01 -0400 Subject: [PATCH 19/35] Add ValueDisplayEdit Component and Use With Variables and Viewer Variables --- .../src/viewer-data/viewer-data.ts | 61 ++++++++++++----- libs/castmate-ui-core/src/main.ts | 2 - packages/castmate/src/renderer/index.ts | 4 -- .../src/components/VariableDisplayEdit.vue | 61 ++++------------- .../renderer/src/components/VariablesPage.vue | 2 - .../src/components/util/ValueDisplayEdit.vue | 66 +++++++++++++++++++ .../viewer-data/ViewerVariableEditDialog.vue | 2 +- .../viewer-data/ViewerVariablePage.vue | 14 +++- plugins/variables/renderer/src/main.ts | 4 ++ .../renderer/src}/viewer-data-store.ts | 39 ++++++----- 10 files changed, 163 insertions(+), 92 deletions(-) create mode 100644 plugins/variables/renderer/src/components/util/ValueDisplayEdit.vue rename {libs/castmate-ui-core => plugins/variables/renderer}/src/components/viewer-data/ViewerVariableEditDialog.vue (98%) rename libs/castmate-ui-core/src/components/viewer-data/ViewerDataPage.vue => plugins/variables/renderer/src/components/viewer-data/ViewerVariablePage.vue (85%) rename {libs/castmate-ui-core/src/viewer-data => plugins/variables/renderer/src}/viewer-data-store.ts (94%) diff --git a/libs/castmate-core/src/viewer-data/viewer-data.ts b/libs/castmate-core/src/viewer-data/viewer-data.ts index 823ccb51..4fe49b1b 100644 --- a/libs/castmate-core/src/viewer-data/viewer-data.ts +++ b/libs/castmate-core/src/viewer-data/viewer-data.ts @@ -314,6 +314,27 @@ export const ViewerData = Service( })) }) + defineIPCFunc( + "viewer-data", + "setVariable", + async (provider: string, id: string, varname: string, value: any) => { + const vari = this.getVariable(varname) + if (!vari) return + + const serialized = await serializeSchema(vari.schema, value) + let sqlized: string + if (typeof serialized == "number") { + sqlized = String(serialized) + } else if (typeof serialized == "string") { + sqlized = `'${escapeSql(serialized)}'` + } else { + sqlized = `'${escapeSql(JSON.stringify(serialized))}'` + } + + await this.updateViewerValue(provider, id, varname, value, sqlized) + } + ) + defineIPCFunc( "viewer-data", "queryPagedData", @@ -387,6 +408,27 @@ export const ViewerData = Service( rendererColumnRemoved(name) } + private async updateViewerValue(provider: string, id: string, varname: string, value: any, sqlized: any) { + try { + this.updateValue({ + provider, + id, + columnName: varname, + columnValue: sqlized, + }) + + try { + await this.providers.get(provider)?.onDataChanged(id, varname, value) + } catch (err) { + logger.error("Error Updating Provider Data", id, varname, value, err) + } + + rendererViewerDataChanged(provider, id, varname, value) + } catch (err) { + logger.error("Error Updating Viewer Data", id, varname, value, err) + } + } + async setViewerValue(provider: string, id: string, displayName: string, varname: string, value: any) { const vari = this.getVariable(varname) if (!vari) return @@ -422,24 +464,7 @@ export const ViewerData = Service( rendererViewerDataAdded(provider, id, defaultValue) } catch (err) { - try { - this.updateValue({ - provider, - id, - columnName: varname, - columnValue: sqlized, - }) - - try { - await this.providers.get(provider)?.onDataChanged(id, varname, value) - } catch (err) { - logger.error("Error Updating Provider Data", id, varname, value, err) - } - - rendererViewerDataChanged(provider, id, varname, value) - } catch (err) { - logger.error("Error Updating Viewer Data", id, varname, value, err) - } + this.updateViewerValue(provider, id, varname, value, sqlized) } } diff --git a/libs/castmate-ui-core/src/main.ts b/libs/castmate-ui-core/src/main.ts index 124eb69e..9b7eaa42 100644 --- a/libs/castmate-ui-core/src/main.ts +++ b/libs/castmate-ui-core/src/main.ts @@ -82,8 +82,6 @@ export * from "./resources/resource-store" export * from "./docking/docking-store" export * from "./queue-system/action-queue-store" -export * from "./viewer-data/viewer-data-store" - export * from "./components/stream-plan/stream-plan-types" export * from "./util/panning" diff --git a/packages/castmate/src/renderer/index.ts b/packages/castmate/src/renderer/index.ts index 55dd2d04..5a8d5fad 100644 --- a/packages/castmate/src/renderer/index.ts +++ b/packages/castmate/src/renderer/index.ts @@ -11,7 +11,6 @@ import { useIpcCaller, initializeStreamPlans, useStreamPlanStore, - useViewerDataStore, } from "castmate-ui-core" import { createApp } from "vue" import App from "./App.vue" @@ -103,7 +102,6 @@ const actionQueueStore = useActionQueueStore() const dashboardStore = useDashboardStore() const mediaStore = useMediaStore() const planStore = useStreamPlanStore() -const viewerDataStore = useViewerDataStore() const uiLoadComplete = useIpcCaller("plugins", "uiLoadComplete") @@ -137,8 +135,6 @@ async function init() { initializeQueues() - await viewerDataStore.initialize() - await initOverlaysPlugin(app) await initVariablesPlugin() diff --git a/plugins/variables/renderer/src/components/VariableDisplayEdit.vue b/plugins/variables/renderer/src/components/VariableDisplayEdit.vue index 04a5158a..e594eef2 100644 --- a/plugins/variables/renderer/src/components/VariableDisplayEdit.vue +++ b/plugins/variables/renderer/src/components/VariableDisplayEdit.vue @@ -1,60 +1,27 @@ - - diff --git a/plugins/variables/renderer/src/components/VariablesPage.vue b/plugins/variables/renderer/src/components/VariablesPage.vue index f8023039..ab0319c1 100644 --- a/plugins/variables/renderer/src/components/VariablesPage.vue +++ b/plugins/variables/renderer/src/components/VariablesPage.vue @@ -83,8 +83,6 @@ const variables = useVariableList() const dialog = useDialog() const confirm = useConfirm() -const pluginStore = usePluginStore() - function createNew() { dialog.open(VariableEditDialog, { props: { diff --git a/plugins/variables/renderer/src/components/util/ValueDisplayEdit.vue b/plugins/variables/renderer/src/components/util/ValueDisplayEdit.vue new file mode 100644 index 00000000..d8c5c4ca --- /dev/null +++ b/plugins/variables/renderer/src/components/util/ValueDisplayEdit.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/libs/castmate-ui-core/src/components/viewer-data/ViewerVariableEditDialog.vue b/plugins/variables/renderer/src/components/viewer-data/ViewerVariableEditDialog.vue similarity index 98% rename from libs/castmate-ui-core/src/components/viewer-data/ViewerVariableEditDialog.vue rename to plugins/variables/renderer/src/components/viewer-data/ViewerVariableEditDialog.vue index 8e17d090..d2d7ac3e 100644 --- a/libs/castmate-ui-core/src/components/viewer-data/ViewerVariableEditDialog.vue +++ b/plugins/variables/renderer/src/components/viewer-data/ViewerVariableEditDialog.vue @@ -37,7 +37,7 @@ - + From 3ac94126e0c90f52c230f9f3d3abe329a45b7c98 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 30 Aug 2024 22:50:58 -0400 Subject: [PATCH 26/35] Add Helper Function for Boolean Evaluation of Viewer Values --- libs/castmate-core/src/util/boolean-helpers.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/castmate-core/src/util/boolean-helpers.ts b/libs/castmate-core/src/util/boolean-helpers.ts index 97afacdc..778c3edb 100644 --- a/libs/castmate-core/src/util/boolean-helpers.ts +++ b/libs/castmate-core/src/util/boolean-helpers.ts @@ -99,7 +99,15 @@ function baseCompare(left: any, right: any, operator: ValueCompareOperator) { async function evaluateValueExpression(expression: BooleanValueExpression) { const left = await getExpressionValueAndSchema(expression.lhs) - const right = await getExpressionValueAndSchema(expression.rhs) + return await evaluateHalfBooleanExpression(left, expression.rhs, expression.operator) +} + +export async function evaluateHalfBooleanExpression( + left: { value: any; schema: Schema } | undefined, + rhs: ExpressionValue, + operator: ValueCompareOperator +) { + const right = await getExpressionValueAndSchema(rhs) let compareFunc = baseCompare @@ -113,7 +121,7 @@ async function evaluateValueExpression(expression: BooleanValueExpression) { } } - return compareFunc(leftValue, rightValue, expression.operator) + return compareFunc(leftValue, rightValue, operator) } function inRangeCompare( From 5e61e2b460ca6102b8161205831f363ef0412f8f Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 30 Aug 2024 22:51:47 -0400 Subject: [PATCH 27/35] Move Viewer Data Store to ui core We need it in cyclic packages --- .../src/viewer-data}/viewer-data-store.ts | 16 +--------------- .../viewer-data/ViewerVariablePage.vue | 4 +--- 2 files changed, 2 insertions(+), 18 deletions(-) rename {plugins/variables/renderer/src => libs/castmate-ui-core/src/viewer-data}/viewer-data-store.ts (95%) diff --git a/plugins/variables/renderer/src/viewer-data-store.ts b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts similarity index 95% rename from plugins/variables/renderer/src/viewer-data-store.ts rename to libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts index 824553ba..2169ff01 100644 --- a/plugins/variables/renderer/src/viewer-data-store.ts +++ b/libs/castmate-ui-core/src/viewer-data/viewer-data-store.ts @@ -9,8 +9,7 @@ import { useDockingStore, useIpcCaller, useProjectStore, -} from "castmate-ui-core" -import ViewerVariablePage from "./components/viewer-data/ViewerVariablePage.vue" +} from "../main" function parseDefinition(def: IPCViewerVariable): ViewerVariable { return { @@ -61,19 +60,6 @@ export const useViewerDataStore = defineStore("viewer-data", () => { } async function initialize() { - const projectItem = computed(() => { - return { - id: "viewer-variables", - title: "Viewer Variables", - icon: "mdi mdi-table-account", - open() { - dockingStore.openPage("viewer-data", "Viewer Variables", ViewerVariablePage) - }, - } - }) - - projectStore.registerProjectGroupItem(projectItem) - const vars = await getVariables() for (const column of vars) { diff --git a/plugins/variables/renderer/src/components/viewer-data/ViewerVariablePage.vue b/plugins/variables/renderer/src/components/viewer-data/ViewerVariablePage.vue index 72a03cae..72409c7e 100644 --- a/plugins/variables/renderer/src/components/viewer-data/ViewerVariablePage.vue +++ b/plugins/variables/renderer/src/components/viewer-data/ViewerVariablePage.vue @@ -60,14 +60,12 @@ import { import PDataTable from "primevue/datatable" import PColumn from "primevue/column" import PButton from "primevue/button" -import { DataView, useIpcCaller } from "castmate-ui-core" +import { DataView, useIpcCaller, useLazyViewerQuery, useViewerDataStore } from "castmate-ui-core" import { computed, ref, watch, onMounted, effect } from "vue" import { useDialog } from "primevue/usedialog" import ViewerVariableEditDialog from "./ViewerVariableEditDialog.vue" import ValueDisplayEdit from "../util/ValueDisplayEdit.vue" -import { useLazyViewerQuery, useViewerDataStore } from "../../viewer-data-store" - import { useElementSize } from "@vueuse/core" const dialog = useDialog() From 03ee677efe65f5f162cb1d4db6e3644a4c704821 Mon Sep 17 00:00:00 2001 From: LordTocs Date: Fri, 30 Aug 2024 22:52:29 -0400 Subject: [PATCH 28/35] Add WIP Conditionals to Viewer Groups based on Viewer Variables --- .../viewer-data/ViewerVariableSelector.vue | 30 ++++++ libs/castmate-ui-core/src/main.ts | 7 ++ plugins/twitch/main/src/group.ts | 20 +++- plugins/twitch/main/src/viewer-cache.ts | 13 +++ .../src/components/TwitchViewerGroupEdit.vue | 2 +- .../groups/TwitchViewerGroupConditionEdit.vue | 99 +++++++++++++++++++ .../groups/TwitchViewerGroupLogicOp.vue | 9 ++ .../groups/TwitchViewerGroupRuleNegator.vue | 9 ++ plugins/twitch/shared/src/resources/group.ts | 14 ++- plugins/variables/renderer/src/main.ts | 17 +++- 10 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 libs/castmate-ui-core/src/components/viewer-data/ViewerVariableSelector.vue create mode 100644 plugins/twitch/renderer/src/components/groups/TwitchViewerGroupConditionEdit.vue diff --git a/libs/castmate-ui-core/src/components/viewer-data/ViewerVariableSelector.vue b/libs/castmate-ui-core/src/components/viewer-data/ViewerVariableSelector.vue new file mode 100644 index 00000000..ca572466 --- /dev/null +++ b/libs/castmate-ui-core/src/components/viewer-data/ViewerVariableSelector.vue @@ -0,0 +1,30 @@ + + + diff --git a/libs/castmate-ui-core/src/main.ts b/libs/castmate-ui-core/src/main.ts index 9b7eaa42..557d8f34 100644 --- a/libs/castmate-ui-core/src/main.ts +++ b/libs/castmate-ui-core/src/main.ts @@ -60,6 +60,11 @@ export { default as StreamPlanDashboardWidget } from "./components/stream-plan/S export { default as CancellableDynamicDialog } from "./components/dialogs/CancellableDynamicDialog.vue" +export { default as ValueCompareOperatorSelector } from "./components/data/base-components/booleans/ValueCompareOperatorSelector.vue" +export { default as BooleanExpressionValueEdit } from "./components/data/base-components/booleans/ExpressionValueEdit.vue" + +export { default as ViewerVariableSelector } from "./components/viewer-data/ViewerVariableSelector.vue" + export * from "./components/data/DataInputTypes" export * from "./util/diff" @@ -84,6 +89,8 @@ export * from "./queue-system/action-queue-store" export * from "./components/stream-plan/stream-plan-types" +export * from "./viewer-data/viewer-data-store" + export * from "./util/panning" export * from "./util/electron" diff --git a/plugins/twitch/main/src/group.ts b/plugins/twitch/main/src/group.ts index d7eb45ee..141d1b06 100644 --- a/plugins/twitch/main/src/group.ts +++ b/plugins/twitch/main/src/group.ts @@ -3,6 +3,7 @@ import { Resource, ResourceRegistry, ResourceStorage, + ViewerData, defineAction, definePluginResource, onLoad, @@ -13,10 +14,15 @@ import { TwitchViewerGroup, TwitchViewerGroupRule, TwitchViewer, + isViewerGroupPropertyRule, + isGroupResourceRef, + isInlineViewerGroup, + isGroupCondition, } from "castmate-plugin-twitch-shared" import { nanoid } from "nanoid/non-secure" import { ViewerCache } from "./viewer-cache" import { TwitchAccount } from "./twitch-auth" +import { evaluateHalfBooleanExpression } from "castmate-core/src/util/boolean-helpers" const logger = usePluginLogger("twitch") @@ -161,7 +167,7 @@ async function satisfiesRule(userId: string, rule: TwitchViewerGroupRule): Promi } } return false - } else if ("properties" in rule) { + } else if (isViewerGroupPropertyRule(rule)) { //Todo: Make this not silly hardcoded if (rule.properties.anonymous && userId == "anonymous") return true @@ -190,13 +196,21 @@ async function satisfiesRule(userId: string, rule: TwitchViewerGroupRule): Promi if (userId === TwitchAccount.channel.twitchId) return true } return false - } else if ("group" in rule) { + } else if (isGroupResourceRef(rule)) { if (!rule.group) return false const group = CustomTwitchViewerGroup.storage.getById(rule.group) if (!group) return false return group.contains(userId) - } else if ("userIds" in rule) { + } else if (isInlineViewerGroup(rule)) { return rule.userIds.includes(userId) + } else if (isGroupCondition(rule)) { + const vari = ViewerData.getInstance().getVariable(rule.varname) + if (!vari) return false + + const data = await ViewerCache.getInstance().getViewerData(userId) + const value = data[vari.name] + + return await evaluateHalfBooleanExpression({ value, schema: vari.schema }, rule.operand, rule.operator) } logger.log("Unknown Group Rule", rule) return false diff --git a/plugins/twitch/main/src/viewer-cache.ts b/plugins/twitch/main/src/viewer-cache.ts index 6efe99b4..5bf90396 100644 --- a/plugins/twitch/main/src/viewer-cache.ts +++ b/plugins/twitch/main/src/viewer-cache.ts @@ -284,6 +284,7 @@ export const ViewerCache = Service( this.unknownColors.add(userId) this.unknownSubInfo.add(userId) this.unknownUserInfo.add(userId) + this.unknownViewerData.add(userId) } return cached.value } @@ -495,6 +496,18 @@ export const ViewerCache = Service( return this.mods.has(userId) } + async getViewerData(userId: string): Promise> { + if (userId == "anonymous") return await ViewerData.getInstance().getDefaultViewerData() + + const cached = this.getOrCreate(userId) + + if (this.unknownViewerData.has(userId)) { + await this.queryViewerData(userId) + } + + return cached + } + setIsMod(userId: string, isMod: boolean) { enforce(this.mods, userId, isMod) } diff --git a/plugins/twitch/renderer/src/components/TwitchViewerGroupEdit.vue b/plugins/twitch/renderer/src/components/TwitchViewerGroupEdit.vue index 73b34742..1930a38f 100644 --- a/plugins/twitch/renderer/src/components/TwitchViewerGroupEdit.vue +++ b/plugins/twitch/renderer/src/components/TwitchViewerGroupEdit.vue @@ -41,7 +41,7 @@ function addEither() { diff --git a/plugins/twitch/renderer/src/components/groups/TwitchViewerGroupLogicOp.vue b/plugins/twitch/renderer/src/components/groups/TwitchViewerGroupLogicOp.vue index 2a315d9f..43a9fd33 100644 --- a/plugins/twitch/renderer/src/components/groups/TwitchViewerGroupLogicOp.vue +++ b/plugins/twitch/renderer/src/components/groups/TwitchViewerGroupLogicOp.vue @@ -35,6 +35,7 @@ Viewers Properties Category + Condition @@ -123,6 +124,14 @@ function addProperties() { function addCategory() { rules.value.push({ or: [{ properties: {} }] }) } + +function addCondition() { + rules.value.push({ + varname: undefined, + operator: "equal", + operand: { type: "state", plugin: undefined, state: undefined }, + }) +}