From ad153f44680e3031ae01ad5983c967736e1a72e9 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 7 Jul 2023 16:20:25 +0100 Subject: [PATCH 01/11] wip --- meteor/lib/api/methods.ts | 21 +++ meteor/package.json | 1 + .../blueprints-integration/src/api/studio.ts | 5 +- packages/blueprints-proxy/.eslintrc.json | 3 + packages/blueprints-proxy/.gitignore | 13 ++ packages/blueprints-proxy/.prettierignore | 3 + packages/blueprints-proxy/LICENSE | 21 +++ packages/blueprints-proxy/README.md | 11 ++ packages/blueprints-proxy/jest.config.js | 27 +++ packages/blueprints-proxy/package.json | 55 +++++++ packages/blueprints-proxy/src/index.ts | 14 ++ packages/blueprints-proxy/tsconfig.build.json | 16 ++ packages/blueprints-proxy/tsconfig.json | 7 + packages/job-worker/package.json | 7 +- .../src/blueprints/ProxiedStudioBlueprint.ts | 154 ++++++++++++++++++ packages/job-worker/src/blueprints/cache.ts | 7 + .../src/blueprints/context/CommonContext.ts | 2 +- packages/job-worker/src/playout/upgrade.ts | 2 +- packages/job-worker/src/workers/caches.ts | 12 +- packages/package.json | 1 + packages/yarn.lock | 95 ++++++++++- 21 files changed, 467 insertions(+), 10 deletions(-) create mode 100644 packages/blueprints-proxy/.eslintrc.json create mode 100644 packages/blueprints-proxy/.gitignore create mode 100644 packages/blueprints-proxy/.prettierignore create mode 100644 packages/blueprints-proxy/LICENSE create mode 100644 packages/blueprints-proxy/README.md create mode 100644 packages/blueprints-proxy/jest.config.js create mode 100644 packages/blueprints-proxy/package.json create mode 100644 packages/blueprints-proxy/src/index.ts create mode 100644 packages/blueprints-proxy/tsconfig.build.json create mode 100644 packages/blueprints-proxy/tsconfig.json create mode 100644 packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index f6aa1ced79..6160ff7d45 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -65,6 +65,27 @@ export const MeteorCall: IMeteorCall = { organization: makeMethods(OrganizationAPIMethods), system: makeMethods(SystemAPIMethods), } + +/** + * Convenience method to convert a Meteor.apply() into a Promise + * @param callName {string} Method name + * @param args {Array} An array of arguments for the method call + * @param options (Optional) An object with options for the call. See Meteor documentation. + * @returns {Promise} A promise containing the result of the called method. + */ +async function MeteorPromiseApply( + callName: Parameters[0], + args: Parameters[1], + options?: Parameters[2] +): Promise { + return new Promise((resolve, reject) => { + Meteor.apply(callName, args, options, (err, res) => { + if (err) reject(err) + else resolve(res) + }) + }) +} + function makeMethods( methods: Enum, /** (Optional) An array of methodnames. Calls to these methods won't be retried in the case of a loss-of-connection for the client. */ diff --git a/meteor/package.json b/meteor/package.json index 25ccb6fa87..07df2eee46 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -201,6 +201,7 @@ }, "resolutions": { "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", + "@sofie-automation/blueprints-proxy": "portal:../packages/blueprints-proxy", "@sofie-automation/corelib": "portal:../packages/corelib", "@sofie-automation/job-worker": "portal:../packages/job-worker", "@sofie-automation/shared-lib": "portal:../packages/shared-lib" diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 5593f7fffb..fea9484bd2 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -49,7 +49,10 @@ export interface StudioBlueprintManifest Array + validateConfig?: ( + context: ICommonContext, + config: TRawConfig + ) => Array | Promise> /** * Apply the config by generating the data to be saved into the db. diff --git a/packages/blueprints-proxy/.eslintrc.json b/packages/blueprints-proxy/.eslintrc.json new file mode 100644 index 0000000000..3b809efa88 --- /dev/null +++ b/packages/blueprints-proxy/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../node_modules/@sofie-automation/code-standard-preset/eslint/main" +} diff --git a/packages/blueprints-proxy/.gitignore b/packages/blueprints-proxy/.gitignore new file mode 100644 index 0000000000..7902085997 --- /dev/null +++ b/packages/blueprints-proxy/.gitignore @@ -0,0 +1,13 @@ +node_modules +dist +test +src/**.js + +/coverage +/docs +.nyc_output +*.log + +wallaby.conf.js + +.DS_Store diff --git a/packages/blueprints-proxy/.prettierignore b/packages/blueprints-proxy/.prettierignore new file mode 100644 index 0000000000..02a4d48226 --- /dev/null +++ b/packages/blueprints-proxy/.prettierignore @@ -0,0 +1,3 @@ +package.json +src/copy +CHANGELOG.md \ No newline at end of file diff --git a/packages/blueprints-proxy/LICENSE b/packages/blueprints-proxy/LICENSE new file mode 100644 index 0000000000..78f0f2dbb8 --- /dev/null +++ b/packages/blueprints-proxy/LICENSE @@ -0,0 +1,21 @@ +MIT License (MIT) + +Copyright (c) 2018 Norsk rikskringkasting AS (NRK) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/blueprints-proxy/README.md b/packages/blueprints-proxy/README.md new file mode 100644 index 0000000000..21f6843138 --- /dev/null +++ b/packages/blueprints-proxy/README.md @@ -0,0 +1,11 @@ +# Sofie: The Modern TV News Studio Automation System (Blueprint Proxy) + +[![npm](https://img.shields.io/npm/v/@sofie-automation/blueprints-proxy)](https://www.npmjs.com/package/@sofie-automation/blueprints-proxy) + +This library is used as part of [**Sofie Server Core**](https://github.com/nrkno/sofie-core). + +This is a part of the [**Sofie** TV News Studio Automation System](https://github.com/nrkno/Sofie-TV-automation/). + +## Purpose + +This is a helper library to run blueprints locally with Sofie connecting back over socket.io to allow for easier debugging of just blueprints. diff --git a/packages/blueprints-proxy/jest.config.js b/packages/blueprints-proxy/jest.config.js new file mode 100644 index 0000000000..2fe89196ee --- /dev/null +++ b/packages/blueprints-proxy/jest.config.js @@ -0,0 +1,27 @@ +module.exports = { + globals: {}, + moduleFileExtensions: ['js', 'ts'], + transform: { + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.json', + }, + ], + }, + testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)'], + testPathIgnorePatterns: ['integrationTests'], + testEnvironment: 'node', + // coverageThreshold: { + // global: { + // branches: 80, + // functions: 100, + // lines: 95, + // statements: 90, + // }, + // }, + coverageDirectory: './coverage/', + coverageProvider: 'v8', + collectCoverage: true, + preset: 'ts-jest', +} diff --git a/packages/blueprints-proxy/package.json b/packages/blueprints-proxy/package.json new file mode 100644 index 0000000000..7c86e635d1 --- /dev/null +++ b/packages/blueprints-proxy/package.json @@ -0,0 +1,55 @@ +{ + "name": "@sofie-automation/blueprints-proxy", + "version": "1.51.0-in-development", + "description": "Library for blueprint debugging", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/nrkno/tv-automation-server-core.git", + "directory": "packages/blueprints-proxy" + }, + "bugs": { + "url": "https://github.com/nrkno/tv-automation-server-core/issues" + }, + "homepage": "https://github.com/nrkno/tv-automation-server-core/blob/master/packages/blueprints-proxy#readme", + "scripts": { + "build": "run -T rimraf dist && run build:main", + "build:main": "run -T tsc -p tsconfig.build.json", + "lint:raw": "run -T eslint --ext .ts --ext .js --ignore-pattern dist", + "lint": "run lint:raw .", + "unit": "run -T jest", + "test": "run lint && run unit", + "watch": "run -T jest --watch", + "cov": "run -T jest --coverage; open-cli coverage/lcov-report/index.html", + "cov-open": "open-cli coverage/lcov-report/index.html", + "validate:dependencies": "yarn npm audit --environment production && run license-validate", + "validate:dev-dependencies": "yarn npm audit --environment development", + "license-validate": "run -T sofie-licensecheck" + }, + "engines": { + "node": ">=14.19" + }, + "files": [ + "/dist", + "/CHANGELOG.md", + "/README.md", + "/LICENSE" + ], + "dependencies": { + "@sofie-automation/blueprints-integration": "1.51.0-in-development", + "tslib": "^2.6.0", + "type-fest": "^3.10.0" + }, + "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", + "lint-staged": { + "*.{js,css,json,md,scss}": [ + "yarn run -T prettier" + ], + "*.{ts,tsx}": [ + "yarn lint:raw" + ] + }, + "packageManager": "yarn@3.5.0" +} diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts new file mode 100644 index 0000000000..70baae8cd4 --- /dev/null +++ b/packages/blueprints-proxy/src/index.ts @@ -0,0 +1,14 @@ +import { IBlueprintConfig, IConfigMessage } from '@sofie-automation/blueprints-integration' + +export type ResultCallback = (err: any, res: T) => void + +export interface ServerToClientEvents { + noArg: () => void + // basicEmit: (a: number, b: string, c: Buffer) => void + // withAck: (d: string, callback: (e: number) => void) => void +} + +export interface ClientToServerEvents { + // hello: () => void + studio_validateConfig: (functionId: string, identifier: string, config: IBlueprintConfig) => IConfigMessage[] +} diff --git a/packages/blueprints-proxy/tsconfig.build.json b/packages/blueprints-proxy/tsconfig.build.json new file mode 100644 index 0000000000..7acc7845d5 --- /dev/null +++ b/packages/blueprints-proxy/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.lib", + "include": ["src/**/*.ts"], + "exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"], + "compilerOptions": { + "target": "es2019", + "outDir": "./dist", + "baseUrl": "./", + "paths": { + "*": ["./node_modules/*"], + "@sofie-automation/shared-lib": ["./src/index.ts"] + }, + "resolveJsonModule": true, + "types": ["node"] + } +} diff --git a/packages/blueprints-proxy/tsconfig.json b/packages/blueprints-proxy/tsconfig.json new file mode 100644 index 0000000000..39cf9672dc --- /dev/null +++ b/packages/blueprints-proxy/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.build.json", + "exclude": ["node_modules/**"], + "compilerOptions": { + "types": ["jest", "node"] + } +} diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 597344cb41..7313fc714e 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -42,6 +42,7 @@ "dependencies": { "@slack/webhook": "^6.1.0", "@sofie-automation/blueprints-integration": "1.51.0-in-development", + "@sofie-automation/blueprints-proxy": "1.51.0-in-development", "@sofie-automation/corelib": "1.51.0-in-development", "@sofie-automation/shared-lib": "1.51.0-in-development", "amqplib": "^0.10.3", @@ -51,6 +52,7 @@ "mongodb": "^5.5.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", + "socket.io-client": "^4.7.1", "superfly-timeline": "^8.3.1", "threadedclass": "^1.2.1", "tslib": "^2.6.0", @@ -67,5 +69,8 @@ "yarn lint:raw" ] }, - "packageManager": "yarn@3.5.0" + "packageManager": "yarn@3.5.0", + "devDependencies": { + "@types/socket.io-client": "^3.0.0" + } } diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts new file mode 100644 index 0000000000..8b1efa5db1 --- /dev/null +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -0,0 +1,154 @@ +import { + BlueprintManifestType, + BlueprintResultStudioBaseline, + ExtendedIngestRundown, + IBlueprintConfig, + IBlueprintShowStyleBase, + ICommonContext, + IConfigMessage, + IShowStyleConfigPreset, + IStudioBaselineContext, + IStudioUserContext, + JSONBlob, + JSONBlobStringify, + JSONSchema, + MigrationStepStudio, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { getRandomString } from '@sofie-automation/corelib/dist/lib' +import { logger } from '../logging' +import * as SocketIOClient from 'socket.io-client' +import { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' +import { ReadonlyDeep } from 'type-fest' +import { CommonContext } from './context' + +type ParamsIfReturnIsValid any> = ReturnType extends never ? never : Parameters + +type MyClient = SocketIOClient.Socket + +export class ProxiedStudioBlueprint implements StudioBlueprintManifest { + readonly blueprintType = BlueprintManifestType.STUDIO + + readonly #client: MyClient = SocketIOClient.io({ + host: '127.0.0.1', + port: 2345, + reconnection: true, + timeout: 5000, + autoConnect: true, + // transports: ['websocket'], + }) as MyClient + + /** Unique id of the blueprint. This is used by core to check if blueprints are the same blueprint, but differing versions */ + blueprintId?: string + /** Version of the blueprint */ + blueprintVersion = '0.0.0' + /** Version of the blueprint-integration that the blueprint depend on */ + integrationVersion = '0.0.0' + /** Version of the TSR-types that the blueprint depend on */ + TSRVersion = '0.0.0' + + /** A list of config items this blueprint expects to be available on the ShowStyle */ + studioConfigSchema: JSONBlob = JSONBlobStringify({}) + /** A list of Migration steps related to a ShowStyle */ + studioMigrations: MigrationStepStudio[] = [] + + /** The config presets exposed by this blueprint */ + configPresets: Record> = {} + + /** Translations connected to the studio (as stringified JSON) */ + translations?: string + + constructor() { + logger.info('Creating ShowStyle blueprint proxy') + + this.#client.connect() + + this.#client.on('connect', () => { + console.log('conencted') + // TODO - load constants from blueprints + }) + + this.#client.on('connect_error', (err) => { + console.log('conencted failed', err) + // TODO - load constants from blueprints + }) + + this.#client.on('disconnect', () => { + console.log('disconnect') + // TODO - abort any in-progress? + }) + } + + async #runProxied( + name: T, + ...args: ParamsIfReturnIsValid + ): Promise> { + if (!this.#client.connected) throw new Error('Blueprints are unavailable') + + return new Promise>((resolve, reject) => { + const handleDisconnect = () => { + reject('Client disconnected') + } + this.#client.once('disconnect', handleDisconnect) + + const innerCb: ResultCallback> = ( + err: any, + res: ReturnType + ): void => { + this.#client.off('disconnect', handleDisconnect) + + if (err) reject(err) + else resolve(res) + } + this.#client.emit(name as any, ...args, innerCb) + }) + } + + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ + getBaseline(_context: IStudioBaselineContext): BlueprintResultStudioBaseline { + throw new Error('not implemented') + } + + /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ + getShowStyleId( + _context: IStudioUserContext, + _showStyles: ReadonlyDeep>, + _ingestRundown: ExtendedIngestRundown + ): string | null { + return null + } + + // /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ + // getRundownPlaylistInfo?: ( + // context: IStudioUserContext, + // rundowns: IBlueprintRundownDB[], + // playlistExternalId: string + // ) => BlueprintResultRundownPlaylist | null + + async validateConfig(context0: ICommonContext, config: IBlueprintConfig): Promise> { + const context = context0 as CommonContext + + const id = getRandomString() // TODO - use this properly + + // TODO - handle this method being optional + + return this.#runProxied('studio_validateConfig', id, context._contextIdentifier, config) + } + + // /** + // * Apply the config by generating the data to be saved into the db. + // * This should be written to give a predictable and stable result, it can be called with the same config multiple times + // */ + // applyConfig?: ( + // context: ICommonContext, + // config: TRawConfig, + // coreConfig: BlueprintConfigCoreConfig + // ) => BlueprintResultApplyStudioConfig + + // /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ + // preprocessConfig?: ( + // context: ICommonContext, + // config: TRawConfig, + // coreConfig: BlueprintConfigCoreConfig + // ) => TProcessedConfig +} diff --git a/packages/job-worker/src/blueprints/cache.ts b/packages/job-worker/src/blueprints/cache.ts index 9762497c18..6dafb174d0 100644 --- a/packages/job-worker/src/blueprints/cache.ts +++ b/packages/job-worker/src/blueprints/cache.ts @@ -50,6 +50,13 @@ export async function parseBlueprintDocument( ): Promise | undefined> { if (!blueprint) return undefined + // // nocommit - do this based on a config + // if (blueprint.blueprintType === BlueprintManifestType.SHOWSTYLE) { + // throw new Error(`not implemented`) + // } else if (blueprint.blueprintType === BlueprintManifestType.STUDIO) { + // throw new Error(`not implemented`) + // } + if (blueprint.code) { let manifest: SomeBlueprintManifest try { diff --git a/packages/job-worker/src/blueprints/context/CommonContext.ts b/packages/job-worker/src/blueprints/context/CommonContext.ts index b2d3382bb0..cd2a49ad5c 100644 --- a/packages/job-worker/src/blueprints/context/CommonContext.ts +++ b/packages/job-worker/src/blueprints/context/CommonContext.ts @@ -15,7 +15,7 @@ export interface UserContextInfo extends ContextInfo { /** Common */ export class CommonContext implements ICommonContext { - private readonly _contextIdentifier: string + public readonly _contextIdentifier: string private readonly _contextName: string private hashI = 0 diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 2d73192d26..ee1cd7c690 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -113,7 +113,7 @@ export async function handleBlueprintValidateConfigForStudio( const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj // TODO - why is this clone necessary? - const messages = clone(blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig)) + const messages = clone(await blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig)) return { messages: messages.map((msg) => ({ diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index f816a8f407..e7dc2a6c0f 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -1,5 +1,5 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { parseBlueprintDocument, WrappedShowStyleBlueprint, WrappedStudioBlueprint } from '../blueprints/cache' +import { WrappedShowStyleBlueprint, WrappedStudioBlueprint } from '../blueprints/cache' import { ReadonlyDeep } from 'type-fest' import { IDirectCollections } from '../db' import { @@ -17,6 +17,7 @@ import { logger } from '../logging' import deepmerge = require('deepmerge') import { ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs' import { StudioCacheContextImpl } from './context' +import { ProxiedStudioBlueprint } from '../blueprints/ProxiedStudioBlueprint' /** * A Wrapper to maintain a cache and provide a context using the cache when appropriate @@ -301,10 +302,11 @@ async function loadStudioBlueprintOrPlaceholder( } const blueprintDoc = await collections.Blueprints.findOne(studio.blueprintId) - const blueprintManifest = await parseBlueprintDocument(blueprintDoc) - if (!blueprintManifest) { - throw new Error(`Blueprint "${studio.blueprintId}" not found! (referenced by Studio "${studio._id}")`) - } + const blueprintManifest = new ProxiedStudioBlueprint() + // const blueprintManifest = await parseBlueprintDocument(blueprintDoc) + // if (!blueprintManifest) { + // throw new Error(`Blueprint "${studio.blueprintId}" not found! (referenced by Studio "${studio._id}")`) + // } if (blueprintManifest.blueprintType !== BlueprintManifestType.STUDIO) { throw new Error( diff --git a/packages/package.json b/packages/package.json index c28a69ef40..438a66ab6f 100644 --- a/packages/package.json +++ b/packages/package.json @@ -4,6 +4,7 @@ "packages": [ "*-integration", "*-gateway", + "blueprints-proxy", "corelib", "openapi", "shared-lib", diff --git a/packages/yarn.lock b/packages/yarn.lock index 18a712d731..59066a964b 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4825,6 +4825,13 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.0 + resolution: "@socket.io/component-emitter@npm:3.1.0" + checksum: db069d95425b419de1514dffe945cc439795f6a8ef5b9465715acf5b8b50798e2c91b8719cbf5434b3fe7de179d6cdcd503c277b7871cb3dd03febb69bdd50fa + languageName: node + linkType: hard + "@sofie-automation/blueprints-integration@1.51.0-in-development, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" @@ -4835,6 +4842,16 @@ __metadata: languageName: unknown linkType: soft +"@sofie-automation/blueprints-proxy@1.51.0-in-development, @sofie-automation/blueprints-proxy@workspace:blueprints-proxy": + version: 0.0.0-use.local + resolution: "@sofie-automation/blueprints-proxy@workspace:blueprints-proxy" + dependencies: + "@sofie-automation/blueprints-integration": 1.51.0-in-development + tslib: ^2.6.0 + type-fest: ^3.10.0 + languageName: unknown + linkType: soft + "@sofie-automation/code-standard-preset@npm:~2.4.7": version: 2.4.7 resolution: "@sofie-automation/code-standard-preset@npm:2.4.7" @@ -4903,8 +4920,10 @@ __metadata: dependencies: "@slack/webhook": ^6.1.0 "@sofie-automation/blueprints-integration": 1.51.0-in-development + "@sofie-automation/blueprints-proxy": 1.51.0-in-development "@sofie-automation/corelib": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development + "@types/socket.io-client": ^3.0.0 amqplib: ^0.10.3 deepmerge: ^4.3.1 elastic-apm-node: ^3.47.0 @@ -4912,6 +4931,7 @@ __metadata: mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 + socket.io-client: ^4.7.1 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 tslib: ^2.6.0 @@ -6159,6 +6179,15 @@ __metadata: languageName: node linkType: hard +"@types/socket.io-client@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/socket.io-client@npm:3.0.0" + dependencies: + socket.io-client: "*" + checksum: 6eef7529afc1d732fb4091864aa6396a2f676ad25eda0035dd1f2c228cbcaf8520b15236ee0f2c714e061307eeab2b6c8edad637bc83a317c326ae70729d88b0 + languageName: node + linkType: hard + "@types/sockjs@npm:^0.3.33": version: 0.3.33 resolution: "@types/sockjs@npm:0.3.33" @@ -9650,7 +9679,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -10396,6 +10425,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"engine.io-client@npm:~6.5.1": + version: 6.5.1 + resolution: "engine.io-client@npm:6.5.1" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + engine.io-parser: ~5.1.0 + ws: ~8.11.0 + xmlhttprequest-ssl: ~2.0.0 + checksum: 411a4f1d2ef8c624fa2d6499ea31bc1da9609e0aaa07aee6e0b713084ab534e1db02b76cf9e80a625bf8d4432ae6088438a40a3748e4746179753a639c5054dc + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.1.0": + version: 5.1.0 + resolution: "engine.io-parser@npm:5.1.0" + checksum: a15fc0ba5d5fc5fb2c3029de1826538970463d0fa5c04d8dc2c72aabde92f1c923a9de409962490c3204da7245704286f9fb0ed4e5d71b55a6b035945f64c281 + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.10.0": version: 5.12.0 resolution: "enhanced-resolve@npm:5.12.0" @@ -20900,6 +20949,28 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"socket.io-client@npm:*, socket.io-client@npm:^4.7.1": + version: 4.7.1 + resolution: "socket.io-client@npm:4.7.1" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.2 + engine.io-client: ~6.5.1 + socket.io-parser: ~4.2.4 + checksum: 5e606ebe01eab4a034ef982b2fc9936a6d98ce9fa7940dd7dcd93f1473a8c273ee69d045c087ac534f0d232285e81c16644de4f28d1731ee864402a9ee3059ee + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -24074,6 +24145,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"ws@npm:~8.11.0": + version: 8.11.0 + resolution: "ws@npm:8.11.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + languageName: node + linkType: hard + "xdg-basedir@npm:^4.0.0": version: 4.0.0 resolution: "xdg-basedir@npm:4.0.0" @@ -24130,6 +24216,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e + languageName: node + linkType: hard + "xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" From 02f63ce61a3ca3f2d14e342369aed377afbb9d0b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 7 Jul 2023 16:48:17 +0100 Subject: [PATCH 02/11] wip: studio.validateConfig works --- meteor/yarn.lock | 85 ++++++++++++++++- packages/blueprints-proxy/package.json | 1 + .../blueprints-proxy/src/context/common.ts | 57 +++++++++++ packages/blueprints-proxy/src/helper.ts | 49 ++++++++++ packages/blueprints-proxy/src/host.ts | 62 ++++++++++++ packages/blueprints-proxy/src/index.ts | 8 +- .../src/routers/studio/config.ts | 17 ++++ packages/blueprints-proxy/src/routers/util.ts | 4 + .../src/blueprints/ProxiedStudioBlueprint.ts | 20 ++-- packages/yarn.lock | 94 ++++++++++++++++++- 10 files changed, 385 insertions(+), 12 deletions(-) create mode 100644 packages/blueprints-proxy/src/context/common.ts create mode 100644 packages/blueprints-proxy/src/helper.ts create mode 100644 packages/blueprints-proxy/src/host.ts create mode 100644 packages/blueprints-proxy/src/routers/studio/config.ts create mode 100644 packages/blueprints-proxy/src/routers/util.ts diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b952415c1f..15965e980b 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1584,6 +1584,13 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.0 + resolution: "@socket.io/component-emitter@npm:3.1.0" + checksum: db069d95425b419de1514dffe945cc439795f6a8ef5b9465715acf5b8b50798e2c91b8719cbf5434b3fe7de179d6cdcd503c277b7871cb3dd03febb69bdd50fa + languageName: node + linkType: hard + "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A.": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." @@ -1594,6 +1601,16 @@ __metadata: languageName: node linkType: soft +"@sofie-automation/blueprints-proxy@portal:../packages/blueprints-proxy::locator=automation-core%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@sofie-automation/blueprints-proxy@portal:../packages/blueprints-proxy::locator=automation-core%40workspace%3A." + dependencies: + "@sofie-automation/blueprints-integration": 1.51.0-in-development + tslib: ^2.6.0 + type-fest: ^3.10.0 + languageName: node + linkType: soft + "@sofie-automation/code-standard-preset@npm:~2.4.7": version: 2.4.7 resolution: "@sofie-automation/code-standard-preset@npm:2.4.7" @@ -1662,6 +1679,7 @@ __metadata: dependencies: "@slack/webhook": ^6.1.0 "@sofie-automation/blueprints-integration": 1.51.0-in-development + "@sofie-automation/blueprints-proxy": 1.51.0-in-development "@sofie-automation/corelib": 1.51.0-in-development "@sofie-automation/shared-lib": 1.51.0-in-development amqplib: ^0.10.3 @@ -1671,6 +1689,7 @@ __metadata: mongodb: ^5.5.0 p-lazy: ^3.1.0 p-timeout: ^4.1.0 + socket.io-client: ^4.7.1 superfly-timeline: ^8.3.1 threadedclass: ^1.2.1 tslib: ^2.6.0 @@ -4589,7 +4608,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -5108,6 +5127,26 @@ __metadata: languageName: node linkType: hard +"engine.io-client@npm:~6.5.1": + version: 6.5.1 + resolution: "engine.io-client@npm:6.5.1" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + engine.io-parser: ~5.1.0 + ws: ~8.11.0 + xmlhttprequest-ssl: ~2.0.0 + checksum: 411a4f1d2ef8c624fa2d6499ea31bc1da9609e0aaa07aee6e0b713084ab534e1db02b76cf9e80a625bf8d4432ae6088438a40a3748e4746179753a639c5054dc + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.1.0": + version: 5.1.0 + resolution: "engine.io-parser@npm:5.1.0" + checksum: a15fc0ba5d5fc5fb2c3029de1826538970463d0fa5c04d8dc2c72aabde92f1c923a9de409962490c3204da7245704286f9fb0ed4e5d71b55a6b035945f64c281 + languageName: node + linkType: hard + "ensure-type@npm:^1.5.0": version: 1.5.1 resolution: "ensure-type@npm:1.5.1" @@ -11756,6 +11795,28 @@ __metadata: languageName: node linkType: hard +"socket.io-client@npm:^4.7.1": + version: 4.7.1 + resolution: "socket.io-client@npm:4.7.1" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.2 + engine.io-client: ~6.5.1 + socket.io-parser: ~4.2.4 + checksum: 5e606ebe01eab4a034ef982b2fc9936a6d98ce9fa7940dd7dcd93f1473a8c273ee69d045c087ac534f0d232285e81c16644de4f28d1731ee864402a9ee3059ee + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -13453,6 +13514,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:~8.11.0": + version: 8.11.0 + resolution: "ws@npm:8.11.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + languageName: node + linkType: hard + "xml-js@npm:^1.6.11": version: 1.6.11 resolution: "xml-js@npm:1.6.11" @@ -13502,6 +13578,13 @@ __metadata: languageName: node linkType: hard +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e + languageName: node + linkType: hard + "xtend@npm:^4.0.2, xtend@npm:~4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" diff --git a/packages/blueprints-proxy/package.json b/packages/blueprints-proxy/package.json index 7c86e635d1..8a0fb88b0b 100644 --- a/packages/blueprints-proxy/package.json +++ b/packages/blueprints-proxy/package.json @@ -39,6 +39,7 @@ ], "dependencies": { "@sofie-automation/blueprints-integration": "1.51.0-in-development", + "socket.io": "^4.7.1", "tslib": "^2.6.0", "type-fest": "^3.10.0" }, diff --git a/packages/blueprints-proxy/src/context/common.ts b/packages/blueprints-proxy/src/context/common.ts new file mode 100644 index 0000000000..a2ed6d418a --- /dev/null +++ b/packages/blueprints-proxy/src/context/common.ts @@ -0,0 +1,57 @@ +import { ICommonContext, NoteSeverity } from '@sofie-automation/blueprints-integration' +import * as crypto from 'crypto' + +function getHash(str: string): string { + const hash = crypto.createHash('sha1') + return hash.update(str).digest('base64').replace(/[+/=]/g, '_') // remove +/= from strings, because they cause troubles +} + +export class CommonContext implements ICommonContext { + private readonly _contextName: string + + private hashI = 0 + private hashed: { [hash: string]: string } = {} + + constructor(identifier: string) { + this._contextName = identifier + } + getHashId(str: string, isNotUnique?: boolean): string { + if (!str) str = 'hash' + this.hashI++ + + if (isNotUnique) { + str = str + '_' + this.hashI++ + } + + const id = getHash(this._contextName + '_' + str.toString()) // TODO - is this unique enough? + this.hashed[id] = str + return id + } + unhashId(hash: string): string { + return this.hashed[hash] || hash + } + + logDebug(message: string): void { + console.debug(`"${this._contextName}": "${message}"`) + } + logInfo(message: string): void { + console.info(`"${this._contextName}": "${message}"`) + } + logWarning(message: string): void { + console.warn(`"${this._contextName}": "${message}"`) + } + logError(message: string): void { + console.error(`"${this._contextName}": "${message}"`) + } + protected logNote(message: string, type: NoteSeverity): void { + if (type === NoteSeverity.ERROR) { + this.logError(message) + } else if (type === NoteSeverity.WARNING) { + this.logWarning(message) + } else if (type === NoteSeverity.INFO) { + this.logInfo(message) + } else { + // assertNever(type) + this.logDebug(message) + } + } +} diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts new file mode 100644 index 0000000000..cea36a7a12 --- /dev/null +++ b/packages/blueprints-proxy/src/helper.ts @@ -0,0 +1,49 @@ +/** + * Signature for the handler functions + */ +type HandlerFunction any> = ( + functionId: string, + ...args: Parameters +) => Promise> + +type HandlerFunctionOrNever = T extends (...args: any) => any ? HandlerFunction : never + +/** Map of handler functions */ +export type EventHandlers = { + [K in keyof T]: HandlerFunctionOrNever +} + +export type ResultCallback = (err: any, res: T) => void + +/** Subscribe to all the events defined in the handlers, and wrap with safety and logging */ +export function listenToEvents(socket: any, handlers: EventHandlers): void { + // const logger = createChildLogger(`module/${connectionId}`); + + for (const [event, handler] of Object.entries(handlers)) { + socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback) => { + if (!functionId || typeof functionId !== 'string') { + console.warn(`Received malformed functionId "${event}"`) + return // Ignore messages without correct structure + } + if (!msg || typeof msg !== 'object') { + console.warn(`Received malformed message object "${event}"`) + return // Ignore messages without correct structure + } + if (cb && typeof cb !== 'function') { + console.warn(`Received malformed callback "${event}"`) + return // Ignore messages without correct structure + } + + try { + // Run it + const handler2 = handler as HandlerFunction<(msg: any) => any> + const result = await handler2(functionId, msg) + + if (cb) cb(null, result) + } catch (e: any) { + console.error(`Command failed: ${e}`, e.stack) + if (cb) cb(e?.toString() ?? JSON.stringify(e), undefined) + } + }) + } +} diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts new file mode 100644 index 0000000000..ad17c851f7 --- /dev/null +++ b/packages/blueprints-proxy/src/host.ts @@ -0,0 +1,62 @@ +import { ShowStyleBlueprintManifest, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +// import { proxyStudioBlueprint } from './blueprint/studio' +// import { klona } from 'klona/full' +import { createServer } from 'http' +import { ClientToServerEvents, ServerToClientEvents } from './index' +import { Server } from 'socket.io' +import { listenToEvents } from './helper' +import { studio_validateConfig } from './routers/studio/config' + +export function runForBlueprints( + studioBlueprint: StudioBlueprintManifest, + _showStyleBlueprint: ShowStyleBlueprintManifest +): void { + // // Clone blueprint and replace any methods with their proxy versions + // const proxiedStudioBlueprint = klona(studioBlueprint) + // for (const [key, value] of Object.entries(proxiedStudioBlueprint)) { + // if (typeof value === 'function') { + // // @ts-expect-error key fails + // if (!proxyStudioBlueprint[key]) { + // throw new Error(`Missing key in studio proxy: ${key}`) + // } + + // // @ts-expect-error key fails + // proxiedStudioBlueprint[key] = proxyStudioBlueprint[key] + // } + // } + + const httpServer = createServer() + const io = new Server(httpServer, { + cors: { + // Allow everything + origin: (o, cb) => cb(null, o), + credentials: true, + }, + // options + }) + + io.on('connection', (socket) => { + // ... + console.log(`connection from ${socket.id}`) + + // subscribe to socket events from host + listenToEvents(socket, { + // init: this._handleInit.bind(this), + // destroy: this._handleDestroy.bind(this), + // updateConfig: this._handleConfigUpdate.bind(this), + // executeAction: this._handleExecuteAction.bind(this), + // updateFeedbacks: this._handleUpdateFeedbacks.bind(this), + // updateActions: this._handleUpdateActions.bind(this), + // getConfigFields: this._handleGetConfigFields.bind(this), + // handleHttpRequest: this._handleHttpRequest.bind(this), + // learnAction: this._handleLearnAction.bind(this), + // learnFeedback: this._handleLearnFeedback.bind(this), + // startStopRecordActions: this._handleStartStopRecordActions.bind(this), + studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args), + }) + }) + + httpServer.listen(2345, () => { + console.log('Started server') + }) +} diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index 70baae8cd4..05184b5dc9 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -10,5 +10,11 @@ export interface ServerToClientEvents { export interface ClientToServerEvents { // hello: () => void - studio_validateConfig: (functionId: string, identifier: string, config: IBlueprintConfig) => IConfigMessage[] + studio_validateConfig: (msg: StudioValidateConfigArgs) => StudioValidateConfigResult } + +export interface StudioValidateConfigArgs { + identifier: string + config: IBlueprintConfig +} +export type StudioValidateConfigResult = IConfigMessage[] diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts new file mode 100644 index 0000000000..2fd9bd4079 --- /dev/null +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -0,0 +1,17 @@ +import type { IConfigMessage, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { CommonContext } from '../../context/common' +import type { StudioValidateConfigArgs } from '../../index' +import { MySocket } from '../util' + +export async function studio_validateConfig( + studioBlueprint: StudioBlueprintManifest, + _socket: MySocket, + _id: string, + msg: StudioValidateConfigArgs +): Promise { + if (!studioBlueprint.validateConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. + + const context = new CommonContext(`validateConfig ${msg.identifier}`) + + return studioBlueprint.validateConfig(context, msg.config) +} diff --git a/packages/blueprints-proxy/src/routers/util.ts b/packages/blueprints-proxy/src/routers/util.ts new file mode 100644 index 0000000000..602e612858 --- /dev/null +++ b/packages/blueprints-proxy/src/routers/util.ts @@ -0,0 +1,4 @@ +import { Socket } from 'socket.io' +import { ClientToServerEvents, ServerToClientEvents } from '..' + +export type MySocket = Socket diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index 8b1efa5db1..a8979a9cd6 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -18,7 +18,7 @@ import { import { getRandomString } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' import * as SocketIOClient from 'socket.io-client' -import { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' +import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' import { ReadonlyDeep } from 'type-fest' import { CommonContext } from './context' @@ -29,11 +29,9 @@ type MyClient = SocketIOClient.Socket { - console.log('conencted failed', err) + console.log('conencted failed', err, err?.message, err?.toString()) // TODO - load constants from blueprints }) @@ -81,10 +79,13 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { async #runProxied( name: T, + functionId: string, ...args: ParamsIfReturnIsValid ): Promise> { if (!this.#client.connected) throw new Error('Blueprints are unavailable') + // TODO - timeouts? + return new Promise>((resolve, reject) => { const handleDisconnect = () => { reject('Client disconnected') @@ -100,7 +101,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { if (err) reject(err) else resolve(res) } - this.#client.emit(name as any, ...args, innerCb) + this.#client.emit(name as any, functionId, ...args, innerCb) }) } @@ -132,7 +133,10 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - handle this method being optional - return this.#runProxied('studio_validateConfig', id, context._contextIdentifier, config) + return this.#runProxied('studio_validateConfig', id, { + identifier: context._contextIdentifier, + config, + }) } // /** diff --git a/packages/yarn.lock b/packages/yarn.lock index 59066a964b..418d80570b 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4847,6 +4847,7 @@ __metadata: resolution: "@sofie-automation/blueprints-proxy@workspace:blueprints-proxy" dependencies: "@sofie-automation/blueprints-integration": 1.51.0-in-development + socket.io: ^4.7.1 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: unknown @@ -5709,6 +5710,22 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/cookie@npm:0.4.1" + checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.12": + version: 2.8.13 + resolution: "@types/cors@npm:2.8.13" + dependencies: + "@types/node": "*" + checksum: 7ef197ea19d2e5bf1313b8416baa6f3fd6dd887fd70191da1f804f557395357dafd8bc8bed0ac60686923406489262a7c8a525b55748f7b2b8afa686700de907 + languageName: node + linkType: hard + "@types/debug@npm:^4.1.8": version: 4.1.8 resolution: "@types/debug@npm:4.1.8" @@ -5985,6 +6002,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=10.0.0": + version: 20.4.0 + resolution: "@types/node@npm:20.4.0" + checksum: 8ad632ee131611651fc5f4ac3a47427640e2492ab314fe1c4d0c3b97af71784ef48c53221d5f9922aab4724375dcb4f33137b3107ba2c356d9366216a31678aa + languageName: node + linkType: hard + "@types/node@npm:^14.18.53": version: 14.18.53 resolution: "@types/node@npm:14.18.53" @@ -7686,6 +7710,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"base64id@npm:2.0.0, base64id@npm:~2.0.0": + version: 2.0.0 + resolution: "base64id@npm:2.0.0" + checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 + languageName: node + linkType: hard + "basic-auth@npm:^2.0.1": version: 2.0.1 resolution: "basic-auth@npm:2.0.1" @@ -9194,6 +9225,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"cookie@npm:~0.4.1": + version: 0.4.2 + resolution: "cookie@npm:0.4.2" + checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b + languageName: node + linkType: hard + "copy-text-to-clipboard@npm:^3.0.1": version: 3.1.0 resolution: "copy-text-to-clipboard@npm:3.1.0" @@ -9272,6 +9310,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"cors@npm:~2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: ^4 + vary: ^1 + checksum: ced838404ccd184f61ab4fdc5847035b681c90db7ac17e428f3d81d69e2989d2b680cc254da0e2554f5ed4f8a341820a1ce3d1c16b499f6e2f47a1b9b07b5006 + languageName: node + linkType: hard + "cosmiconfig@npm:7.0.0": version: 7.0.0 resolution: "cosmiconfig@npm:7.0.0" @@ -10445,6 +10493,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"engine.io@npm:~6.5.0": + version: 6.5.1 + resolution: "engine.io@npm:6.5.1" + dependencies: + "@types/cookie": ^0.4.1 + "@types/cors": ^2.8.12 + "@types/node": ">=10.0.0" + accepts: ~1.3.4 + base64id: 2.0.0 + cookie: ~0.4.1 + cors: ~2.8.5 + debug: ~4.3.1 + engine.io-parser: ~5.1.0 + ws: ~8.11.0 + checksum: e902bbb3a484236edd6f0be89c14eb694cd905e727f88f3082a8b33ba23af9a71ca51e109b213962ccf836b02ba5bb9eea6f680a44d5008eb5b6aa2028d3bb7f + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.10.0": version: 5.12.0 resolution: "enhanced-resolve@npm:5.12.0" @@ -17254,7 +17320,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -20949,6 +21015,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"socket.io-adapter@npm:~2.5.2": + version: 2.5.2 + resolution: "socket.io-adapter@npm:2.5.2" + dependencies: + ws: ~8.11.0 + checksum: 481251c3547221e57eb5cb247d0b1a3cde4d152a4c1c9051cc887345a7770e59f3b47f1011cac4499e833f01fcfc301ed13c4ec6e72f7dbb48a476375a6344cd + languageName: node + linkType: hard + "socket.io-client@npm:*, socket.io-client@npm:^4.7.1": version: 4.7.1 resolution: "socket.io-client@npm:4.7.1" @@ -20971,6 +21046,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"socket.io@npm:^4.7.1": + version: 4.7.1 + resolution: "socket.io@npm:4.7.1" + dependencies: + accepts: ~1.3.4 + base64id: ~2.0.0 + cors: ~2.8.5 + debug: ~4.3.2 + engine.io: ~6.5.0 + socket.io-adapter: ~2.5.2 + socket.io-parser: ~4.2.4 + checksum: 81404d06383aa5495b3cb9a1a4fc1435cfa97d8963c89fa54403c3ef20e0884eccedb8799b1c804a40896f903d64543e2303071d5d60dcbf7e062edf7a98d87f + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -23374,7 +23464,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vary@npm:~1.1.2": +"vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b From cab413bcca33139d1810cf82909d97c7f2df3a87 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 7 Jul 2023 17:44:56 +0100 Subject: [PATCH 03/11] wip: another method --- .../blueprints-integration/src/api/studio.ts | 2 +- packages/blueprints-proxy/src/helper.ts | 2 ++ packages/blueprints-proxy/src/host.ts | 3 +- packages/blueprints-proxy/src/index.ts | 17 ++++++++-- .../src/routers/studio/config.ts | 21 ++++++++++-- .../src/blueprints/ProxiedStudioBlueprint.ts | 32 +++++++++++++------ packages/job-worker/src/playout/upgrade.ts | 2 +- 7 files changed, 62 insertions(+), 17 deletions(-) diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index fea9484bd2..2a6d15403b 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -62,7 +62,7 @@ export interface StudioBlueprintManifest BlueprintResultApplyStudioConfig + ) => BlueprintResultApplyStudioConfig | Promise /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ preprocessConfig?: ( diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts index cea36a7a12..f5ba53be48 100644 --- a/packages/blueprints-proxy/src/helper.ts +++ b/packages/blueprints-proxy/src/helper.ts @@ -21,6 +21,8 @@ export function listenToEvents(socket: any, handlers: EventHan for (const [event, handler] of Object.entries(handlers)) { socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback) => { + // TODO - find/reject callback? + if (!functionId || typeof functionId !== 'string') { console.warn(`Received malformed functionId "${event}"`) return // Ignore messages without correct structure diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts index ad17c851f7..5980ef1378 100644 --- a/packages/blueprints-proxy/src/host.ts +++ b/packages/blueprints-proxy/src/host.ts @@ -5,7 +5,7 @@ import { createServer } from 'http' import { ClientToServerEvents, ServerToClientEvents } from './index' import { Server } from 'socket.io' import { listenToEvents } from './helper' -import { studio_validateConfig } from './routers/studio/config' +import { studio_applyConfig, studio_validateConfig } from './routers/studio/config' export function runForBlueprints( studioBlueprint: StudioBlueprintManifest, @@ -53,6 +53,7 @@ export function runForBlueprints( // learnFeedback: this._handleLearnFeedback.bind(this), // startStopRecordActions: this._handleStartStopRecordActions.bind(this), studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args), + studio_applyConfig: async (...args) => studio_applyConfig(studioBlueprint, socket, ...args), }) }) diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index 05184b5dc9..ca61364eb1 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -1,4 +1,9 @@ -import { IBlueprintConfig, IConfigMessage } from '@sofie-automation/blueprints-integration' +import { + BlueprintConfigCoreConfig, + BlueprintResultApplyStudioConfig, + IBlueprintConfig, + IConfigMessage, +} from '@sofie-automation/blueprints-integration' export type ResultCallback = (err: any, res: T) => void @@ -10,11 +15,17 @@ export interface ServerToClientEvents { export interface ClientToServerEvents { // hello: () => void - studio_validateConfig: (msg: StudioValidateConfigArgs) => StudioValidateConfigResult + studio_validateConfig: (msg: StudioValidateConfigArgs) => IConfigMessage[] + studio_applyConfig: (msg: StudioApplyConfigArgs) => BlueprintResultApplyStudioConfig } export interface StudioValidateConfigArgs { identifier: string config: IBlueprintConfig } -export type StudioValidateConfigResult = IConfigMessage[] + +export interface StudioApplyConfigArgs { + identifier: string + config: IBlueprintConfig + coreConfig: BlueprintConfigCoreConfig +} diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts index 2fd9bd4079..cc89fb3bca 100644 --- a/packages/blueprints-proxy/src/routers/studio/config.ts +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -1,6 +1,10 @@ -import type { IConfigMessage, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import type { + BlueprintResultApplyStudioConfig, + IConfigMessage, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' import { CommonContext } from '../../context/common' -import type { StudioValidateConfigArgs } from '../../index' +import type { StudioApplyConfigArgs, StudioValidateConfigArgs } from '../../index' import { MySocket } from '../util' export async function studio_validateConfig( @@ -15,3 +19,16 @@ export async function studio_validateConfig( return studioBlueprint.validateConfig(context, msg.config) } + +export async function studio_applyConfig( + studioBlueprint: StudioBlueprintManifest, + _socket: MySocket, + _id: string, + msg: StudioApplyConfigArgs +): Promise { + if (!studioBlueprint.applyConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. + + const context = new CommonContext(`validateConfig ${msg.identifier}`) + + return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) +} diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index a8979a9cd6..c559705513 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -1,5 +1,7 @@ import { + BlueprintConfigCoreConfig, BlueprintManifestType, + BlueprintResultApplyStudioConfig, BlueprintResultStudioBaseline, ExtendedIngestRundown, IBlueprintConfig, @@ -139,15 +141,27 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { }) } - // /** - // * Apply the config by generating the data to be saved into the db. - // * This should be written to give a predictable and stable result, it can be called with the same config multiple times - // */ - // applyConfig?: ( - // context: ICommonContext, - // config: TRawConfig, - // coreConfig: BlueprintConfigCoreConfig - // ) => BlueprintResultApplyStudioConfig + /** + * Apply the config by generating the data to be saved into the db. + * This should be written to give a predictable and stable result, it can be called with the same config multiple times + */ + async applyConfig( + context0: ICommonContext, + config: IBlueprintConfig, + coreConfig: BlueprintConfigCoreConfig + ): Promise { + const context = context0 as CommonContext + + const id = getRandomString() // TODO - use this properly + + // TODO - handle this method being optional + + return this.#runProxied('studio_applyConfig', id, { + identifier: context._contextIdentifier, + config, + coreConfig, + }) + } // /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ // preprocessConfig?: ( diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index ee1cd7c690..3d06d97597 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -30,7 +30,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data }) const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj - const result = blueprint.blueprint.applyConfig( + const result = await blueprint.blueprint.applyConfig( blueprintContext, clone(rawBlueprintConfig), compileCoreConfigValues(context.studio.settings) From 68082eb4a04b0fb9b969c9ba8456975ad2903c3e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 7 Jul 2023 17:45:12 +0100 Subject: [PATCH 04/11] wip: start on a more complex method --- .../blueprints-integration/src/api/studio.ts | 4 +- .../blueprints-integration/src/context.ts | 4 +- packages/blueprints-proxy/src/helper.ts | 2 + packages/blueprints-proxy/src/host.ts | 2 + packages/blueprints-proxy/src/index.ts | 9 ++- .../src/routers/studio/baseline.ts | 55 +++++++++++++++++++ .../src/routers/studio/config.ts | 2 +- .../src/blueprints/ProxiedStudioBlueprint.ts | 28 +++++++++- .../blueprints/context/SegmentUserContext.ts | 2 +- .../context/ShowStyleUserContext.ts | 2 +- .../context/StudioBaselineContext.ts | 2 +- .../src/blueprints/context/StudioContext.ts | 2 +- .../src/playout/timeline/generate.ts | 2 +- 13 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 packages/blueprints-proxy/src/routers/studio/baseline.ts diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 2a6d15403b..72047a4e67 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -28,7 +28,9 @@ export interface StudioBlueprintManifest BlueprintResultStudioBaseline + getBaseline: ( + context: IStudioBaselineContext + ) => BlueprintResultStudioBaseline | Promise /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ getShowStyleId: ( diff --git a/packages/blueprints-integration/src/context.ts b/packages/blueprints-integration/src/context.ts index 4601bb8bae..b9d5f45d1e 100644 --- a/packages/blueprints-integration/src/context.ts +++ b/packages/blueprints-integration/src/context.ts @@ -94,7 +94,7 @@ export interface IStudioContext extends ICommonContext { getStudioConfigRef(configKey: string): string /** Get the mappings for the studio */ - getStudioMappings: () => Readonly + getStudioMappings: () => Promise> } export interface IPackageInfoContext { @@ -104,7 +104,7 @@ export interface IPackageInfoContext { * The possible packageIds are scoped based on the ownership of the package. * eg, baseline packages can be accessed when generating the baseline objects, piece/adlib packages can be access when regenerating the segment they are from */ - getPackageInfo: (packageId: string) => Readonly + getPackageInfo: (packageId: string) => Promise> hackGetMediaObjectDuration: (mediaId: string) => Promise } diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts index f5ba53be48..1573fbc712 100644 --- a/packages/blueprints-proxy/src/helper.ts +++ b/packages/blueprints-proxy/src/helper.ts @@ -36,6 +36,8 @@ export function listenToEvents(socket: any, handlers: EventHan return // Ignore messages without correct structure } + console.log('running', event, functionId) + try { // Run it const handler2 = handler as HandlerFunction<(msg: any) => any> diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts index 5980ef1378..d0743d837f 100644 --- a/packages/blueprints-proxy/src/host.ts +++ b/packages/blueprints-proxy/src/host.ts @@ -6,6 +6,7 @@ import { ClientToServerEvents, ServerToClientEvents } from './index' import { Server } from 'socket.io' import { listenToEvents } from './helper' import { studio_applyConfig, studio_validateConfig } from './routers/studio/config' +import { studio_getBaseline } from './routers/studio/baseline' export function runForBlueprints( studioBlueprint: StudioBlueprintManifest, @@ -52,6 +53,7 @@ export function runForBlueprints( // learnAction: this._handleLearnAction.bind(this), // learnFeedback: this._handleLearnFeedback.bind(this), // startStopRecordActions: this._handleStartStopRecordActions.bind(this), + studio_getBaseline: async (...args) => studio_getBaseline(studioBlueprint, socket, ...args), studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args), studio_applyConfig: async (...args) => studio_applyConfig(studioBlueprint, socket, ...args), }) diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index ca61364eb1..068c55861d 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -1,6 +1,7 @@ import { BlueprintConfigCoreConfig, BlueprintResultApplyStudioConfig, + BlueprintResultStudioBaseline, IBlueprintConfig, IConfigMessage, } from '@sofie-automation/blueprints-integration' @@ -14,11 +15,17 @@ export interface ServerToClientEvents { } export interface ClientToServerEvents { - // hello: () => void + studio_getBaseline: (msg: StudioGetBaselineArgs) => BlueprintResultStudioBaseline studio_validateConfig: (msg: StudioValidateConfigArgs) => IConfigMessage[] studio_applyConfig: (msg: StudioApplyConfigArgs) => BlueprintResultApplyStudioConfig } +export interface StudioGetBaselineArgs { + identifier: string + studioId: string + studioConfig: IBlueprintConfig +} + export interface StudioValidateConfigArgs { identifier: string config: IBlueprintConfig diff --git a/packages/blueprints-proxy/src/routers/studio/baseline.ts b/packages/blueprints-proxy/src/routers/studio/baseline.ts new file mode 100644 index 0000000000..2b5a001ce9 --- /dev/null +++ b/packages/blueprints-proxy/src/routers/studio/baseline.ts @@ -0,0 +1,55 @@ +import { + BlueprintMappings, + BlueprintResultStudioBaseline, + IStudioBaselineContext, + PackageInfo, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { StudioGetBaselineArgs } from '@sofie-automation/shared-lib' +import { CommonContext } from '../../context/common' +import { MySocket } from '../util' + +class StudioBaselineContext extends CommonContext implements IStudioBaselineContext { + readonly #data: StudioGetBaselineArgs + + public get studioId(): string { + return this.#data.studioId + } + + constructor(msg: StudioGetBaselineArgs) { + super(`getBaseline ${msg.identifier}`) + + this.#data = msg + } + + getStudioConfig(): unknown { + return this.#data.studioConfig + } + getStudioConfigRef(configKey: string): string { + throw new Error('Method not implemented.') + } + async getStudioMappings(): Promise> { + throw new Error('not implemented') + } + async getPackageInfo(packageId: string): Promise { + throw new Error('not implemented') + } + async hackGetMediaObjectDuration(mediaId: string): Promise { + throw new Error('not implemented') + } +} + +export async function studio_getBaseline( + studioBlueprint: StudioBlueprintManifest, + _socket: MySocket, + _id: string, + msg: StudioGetBaselineArgs +): Promise { + const context = new StudioBaselineContext(msg) + + const result = await studioBlueprint.getBaseline(context) + + // TODO - cleanup? + + return result +} diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts index cc89fb3bca..483fcc2548 100644 --- a/packages/blueprints-proxy/src/routers/studio/config.ts +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -28,7 +28,7 @@ export async function studio_applyConfig( ): Promise { if (!studioBlueprint.applyConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`validateConfig ${msg.identifier}`) + const context = new CommonContext(`applyConfig ${msg.identifier}`) return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) } diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index c559705513..77c6bfc0e1 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -22,7 +22,7 @@ import { logger } from '../logging' import * as SocketIOClient from 'socket.io-client' import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' import { ReadonlyDeep } from 'type-fest' -import { CommonContext } from './context' +import { CommonContext, StudioBaselineContext } from './context' type ParamsIfReturnIsValid any> = ReturnType extends never ? never : Parameters @@ -107,9 +107,31 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { }) } + #listenToEventsForMethod(functionId: string, handlers: any): () => void { + // TODO + + return () => { + // TODO -cleanup + } + } + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ - getBaseline(_context: IStudioBaselineContext): BlueprintResultStudioBaseline { - throw new Error('not implemented') + async getBaseline(context0: IStudioBaselineContext): Promise { + const context = context0 as StudioBaselineContext + + const id = getRandomString() // TODO - use this properly + + const stop = this.#listenToEventsForMethod(id, {}) + + try { + return this.#runProxied('studio_getBaseline', id, { + identifier: context._contextIdentifier, + studioId: context.studioId, + studioConfig: context.getStudioConfig() as IBlueprintConfig, + }) + } finally { + stop() + } } /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ diff --git a/packages/job-worker/src/blueprints/context/SegmentUserContext.ts b/packages/job-worker/src/blueprints/context/SegmentUserContext.ts index 9af2a4d83a..422c00d38e 100644 --- a/packages/job-worker/src/blueprints/context/SegmentUserContext.ts +++ b/packages/job-worker/src/blueprints/context/SegmentUserContext.ts @@ -67,7 +67,7 @@ export class SegmentUserContext extends RundownContext implements ISegmentUserCo }) } - getPackageInfo(packageId: string): Readonly> { + async getPackageInfo(packageId: string): Promise>> { return this.watchedPackages.getPackageInfo(packageId) } diff --git a/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts b/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts index 900d568530..efd4206355 100644 --- a/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts +++ b/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts @@ -71,7 +71,7 @@ export class ShowStyleUserContext extends ShowStyleContext implements IShowStyle } } - getPackageInfo(packageId: string): Readonly> { + async getPackageInfo(packageId: string): Promise>> { return this.watchedPackages.getPackageInfo(packageId) } diff --git a/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts b/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts index b2481d3bc7..64b2bdd1a1 100644 --- a/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts @@ -17,7 +17,7 @@ export class StudioBaselineContext extends StudioContext implements IStudioBasel this.jobContext = context } - getPackageInfo(packageId: string): readonly PackageInfo.Any[] { + async getPackageInfo(packageId: string): Promise>> { return this.watchedPackages.getPackageInfo(packageId) } diff --git a/packages/job-worker/src/blueprints/context/StudioContext.ts b/packages/job-worker/src/blueprints/context/StudioContext.ts index 378a60971d..c2c77ddda1 100644 --- a/packages/job-worker/src/blueprints/context/StudioContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioContext.ts @@ -34,7 +34,7 @@ export class StudioContext extends CommonContext implements IStudioContext { getStudioConfigRef(configKey: string): string { return getStudioConfigRef(this.studio._id, configKey) } - getStudioMappings(): Readonly { + async getStudioMappings(): Promise> { if (!this.#processedMappings) { this.#processedMappings = applyAndValidateOverrides(this.studio.mappingsWithOverrides).obj } diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 82270e41bc..fd769cf0ce 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -108,7 +108,7 @@ export async function updateStudioTimeline( const blueprint = studioBlueprint.blueprint try { - studioBaseline = blueprint.getBaseline( + studioBaseline = await blueprint.getBaseline( new StudioBaselineContext( { name: 'studioBaseline', identifier: `studioId=${studio._id}` }, context, From 6f5d8a400c948d215f4a5ab67a3f926da6411761 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 11:09:21 +0100 Subject: [PATCH 05/11] wip: start implementing method back --- meteor/yarn.lock | 96 +++++++++++++++++- packages/blueprints-proxy/src/helper.ts | 8 +- packages/blueprints-proxy/src/index.ts | 17 +++- .../src/routers/studio/baseline.ts | 23 +++-- packages/blueprints-proxy/src/routers/util.ts | 31 ++++++ .../src/blueprints/ProxiedStudioBlueprint.ts | 99 +++++++++++++------ 6 files changed, 227 insertions(+), 47 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 15965e980b..b30aa8d1f8 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1606,6 +1606,7 @@ __metadata: resolution: "@sofie-automation/blueprints-proxy@portal:../packages/blueprints-proxy::locator=automation-core%40workspace%3A." dependencies: "@sofie-automation/blueprints-integration": 1.51.0-in-development + socket.io: ^4.7.1 tslib: ^2.6.0 type-fest: ^3.10.0 languageName: node @@ -1830,6 +1831,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/cookie@npm:0.4.1" + checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18 + languageName: node + linkType: hard + "@types/cookies@npm:*": version: 0.7.7 resolution: "@types/cookies@npm:0.7.7" @@ -1842,6 +1850,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.12": + version: 2.8.13 + resolution: "@types/cors@npm:2.8.13" + dependencies: + "@types/node": "*" + checksum: 7ef197ea19d2e5bf1313b8416baa6f3fd6dd887fd70191da1f804f557395357dafd8bc8bed0ac60686923406489262a7c8a525b55748f7b2b8afa686700de907 + languageName: node + linkType: hard + "@types/deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "@types/deep-extend@npm:0.6.0" @@ -2050,6 +2067,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=10.0.0": + version: 20.4.1 + resolution: "@types/node@npm:20.4.1" + checksum: 22cbcc792f2eb636fe4188778ed0f32658ab872aa7fcb9847b3fa289a42b14b9f5e30c6faec50ef3c7adbc6c2a246926e5858136bb8b10c035a3fcaa6afbeed2 + languageName: node + linkType: hard + "@types/node@npm:^14.18.53": version: 14.18.53 resolution: "@types/node@npm:14.18.53" @@ -2652,7 +2676,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:^1.3.7": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -3385,6 +3409,13 @@ __metadata: languageName: node linkType: hard +"base64id@npm:2.0.0, base64id@npm:~2.0.0": + version: 2.0.0 + resolution: "base64id@npm:2.0.0" + checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 + languageName: node + linkType: hard + "basic-auth@npm:^2.0.1": version: 2.0.1 resolution: "basic-auth@npm:2.0.1" @@ -4414,6 +4445,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:~0.4.1": + version: 0.4.2 + resolution: "cookie@npm:0.4.2" + checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b + languageName: node + linkType: hard + "cookies@npm:~0.8.0": version: 0.8.0 resolution: "cookies@npm:0.8.0" @@ -4445,6 +4483,16 @@ __metadata: languageName: node linkType: hard +"cors@npm:~2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: ^4 + vary: ^1 + checksum: ced838404ccd184f61ab4fdc5847035b681c90db7ac17e428f3d81d69e2989d2b680cc254da0e2554f5ed4f8a341820a1ce3d1c16b499f6e2f47a1b9b07b5006 + languageName: node + linkType: hard + "create-ecdh@npm:^4.0.0": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" @@ -5147,6 +5195,24 @@ __metadata: languageName: node linkType: hard +"engine.io@npm:~6.5.0": + version: 6.5.1 + resolution: "engine.io@npm:6.5.1" + dependencies: + "@types/cookie": ^0.4.1 + "@types/cors": ^2.8.12 + "@types/node": ">=10.0.0" + accepts: ~1.3.4 + base64id: 2.0.0 + cookie: ~0.4.1 + cors: ~2.8.5 + debug: ~4.3.1 + engine.io-parser: ~5.1.0 + ws: ~8.11.0 + checksum: e902bbb3a484236edd6f0be89c14eb694cd905e727f88f3082a8b33ba23af9a71ca51e109b213962ccf836b02ba5bb9eea6f680a44d5008eb5b6aa2028d3bb7f + languageName: node + linkType: hard + "ensure-type@npm:^1.5.0": version: 1.5.1 resolution: "ensure-type@npm:1.5.1" @@ -9624,7 +9690,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -11795,6 +11861,15 @@ __metadata: languageName: node linkType: hard +"socket.io-adapter@npm:~2.5.2": + version: 2.5.2 + resolution: "socket.io-adapter@npm:2.5.2" + dependencies: + ws: ~8.11.0 + checksum: 481251c3547221e57eb5cb247d0b1a3cde4d152a4c1c9051cc887345a7770e59f3b47f1011cac4499e833f01fcfc301ed13c4ec6e72f7dbb48a476375a6344cd + languageName: node + linkType: hard + "socket.io-client@npm:^4.7.1": version: 4.7.1 resolution: "socket.io-client@npm:4.7.1" @@ -11817,6 +11892,21 @@ __metadata: languageName: node linkType: hard +"socket.io@npm:^4.7.1": + version: 4.7.1 + resolution: "socket.io@npm:4.7.1" + dependencies: + accepts: ~1.3.4 + base64id: ~2.0.0 + cors: ~2.8.5 + debug: ~4.3.2 + engine.io: ~6.5.0 + socket.io-adapter: ~2.5.2 + socket.io-parser: ~4.2.4 + checksum: 81404d06383aa5495b3cb9a1a4fc1435cfa97d8963c89fa54403c3ef20e0884eccedb8799b1c804a40896f903d64543e2303071d5d60dcbf7e062edf7a98d87f + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -13147,7 +13237,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1.1.2": +"vary@npm:^1, vary@npm:^1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts index 1573fbc712..05b5474842 100644 --- a/packages/blueprints-proxy/src/helper.ts +++ b/packages/blueprints-proxy/src/helper.ts @@ -15,6 +15,10 @@ export type EventHandlers = { export type ResultCallback = (err: any, res: T) => void +export type ParamsIfReturnIsValid any> = ReturnType extends never + ? never + : Parameters + /** Subscribe to all the events defined in the handlers, and wrap with safety and logging */ export function listenToEvents(socket: any, handlers: EventHandlers): void { // const logger = createChildLogger(`module/${connectionId}`); @@ -23,6 +27,8 @@ export function listenToEvents(socket: any, handlers: EventHan socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback) => { // TODO - find/reject callback? + console.log('running', event, functionId, JSON.stringify(msg)) + if (!functionId || typeof functionId !== 'string') { console.warn(`Received malformed functionId "${event}"`) return // Ignore messages without correct structure @@ -36,8 +42,6 @@ export function listenToEvents(socket: any, handlers: EventHan return // Ignore messages without correct structure } - console.log('running', event, functionId) - try { // Run it const handler2 = handler as HandlerFunction<(msg: any) => any> diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index 068c55861d..64dc1e4bf4 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -1,17 +1,26 @@ -import { +import type { BlueprintConfigCoreConfig, + BlueprintMappings, BlueprintResultApplyStudioConfig, BlueprintResultStudioBaseline, IBlueprintConfig, IConfigMessage, + PackageInfo, } from '@sofie-automation/blueprints-integration' export type ResultCallback = (err: any, res: T) => void export interface ServerToClientEvents { - noArg: () => void - // basicEmit: (a: number, b: string, c: Buffer) => void - // withAck: (d: string, callback: (e: number) => void) => void + packageInfo_getPackageInfo: (msg: PackageInfoGetPackageInfoArgs) => Readonly + packageInfo_hackGetMediaObjectDuration: (msg: PackageInfoHackGetMediaObjectDuration) => number | undefined + studio_getStudioMappings: () => Readonly +} + +export interface PackageInfoGetPackageInfoArgs { + packageId: string +} +export interface PackageInfoHackGetMediaObjectDuration { + mediaId: string } export interface ClientToServerEvents { diff --git a/packages/blueprints-proxy/src/routers/studio/baseline.ts b/packages/blueprints-proxy/src/routers/studio/baseline.ts index 2b5a001ce9..baeefea2e6 100644 --- a/packages/blueprints-proxy/src/routers/studio/baseline.ts +++ b/packages/blueprints-proxy/src/routers/studio/baseline.ts @@ -7,45 +7,50 @@ import { } from '@sofie-automation/blueprints-integration' import { StudioGetBaselineArgs } from '@sofie-automation/shared-lib' import { CommonContext } from '../../context/common' -import { MySocket } from '../util' +import { callHelper, MySocket } from '../util' class StudioBaselineContext extends CommonContext implements IStudioBaselineContext { readonly #data: StudioGetBaselineArgs + readonly #socket: MySocket + readonly #functionId: string public get studioId(): string { return this.#data.studioId } - constructor(msg: StudioGetBaselineArgs) { + constructor(msg: StudioGetBaselineArgs, socket: MySocket, functionId: string) { super(`getBaseline ${msg.identifier}`) this.#data = msg + this.#socket = socket + this.#functionId = functionId } getStudioConfig(): unknown { return this.#data.studioConfig } getStudioConfigRef(configKey: string): string { - throw new Error('Method not implemented.') + // TODO - we should avoid duplicating this logic + return '${studio.' + this.#data.studioId + '.' + configKey + '}' } async getStudioMappings(): Promise> { - throw new Error('not implemented') + return callHelper(this.#socket, this.#functionId, 'studio_getStudioMappings', {}) } async getPackageInfo(packageId: string): Promise { - throw new Error('not implemented') + return callHelper(this.#socket, this.#functionId, 'packageInfo_getPackageInfo', { packageId }) } async hackGetMediaObjectDuration(mediaId: string): Promise { - throw new Error('not implemented') + return callHelper(this.#socket, this.#functionId, 'packageInfo_hackGetMediaObjectDuration', { mediaId }) } } export async function studio_getBaseline( studioBlueprint: StudioBlueprintManifest, - _socket: MySocket, - _id: string, + socket: MySocket, + id: string, msg: StudioGetBaselineArgs ): Promise { - const context = new StudioBaselineContext(msg) + const context = new StudioBaselineContext(msg, socket, id) const result = await studioBlueprint.getBaseline(context) diff --git a/packages/blueprints-proxy/src/routers/util.ts b/packages/blueprints-proxy/src/routers/util.ts index 602e612858..727c5ea453 100644 --- a/packages/blueprints-proxy/src/routers/util.ts +++ b/packages/blueprints-proxy/src/routers/util.ts @@ -1,4 +1,35 @@ import { Socket } from 'socket.io' +import { ParamsIfReturnIsValid, ResultCallback } from '../helper' import { ClientToServerEvents, ServerToClientEvents } from '..' export type MySocket = Socket + +export async function callHelper( + socket: MySocket, + functionId: string, + name: T, + data: ParamsIfReturnIsValid[0] +): Promise> { + if (!socket.connected) throw new Error('Blueprints are unavailable') + + // TODO - ensure #callHandlers is cleaned up + + // TODO - timeouts? + return new Promise>((resolve, reject) => { + const handleDisconnect = () => { + reject('Lost connection') + } + socket.once('disconnect', handleDisconnect) + + const innerCb: ResultCallback> = ( + err: any, + res: ReturnType + ): void => { + socket.off('disconnect', handleDisconnect) + + if (err) reject(err) + else resolve(res) + } + socket.emit(name as any, functionId, data, innerCb) + }) +} diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index 77c6bfc0e1..8221665931 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -23,13 +23,12 @@ import * as SocketIOClient from 'socket.io-client' import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' import { ReadonlyDeep } from 'type-fest' import { CommonContext, StudioBaselineContext } from './context' - -type ParamsIfReturnIsValid any> = ReturnType extends never ? never : Parameters +import { EventHandlers, listenToEvents, ParamsIfReturnIsValid } from '@sofie-automation/blueprints-proxy/dist/helper' type MyClient = SocketIOClient.Socket export class ProxiedStudioBlueprint implements StudioBlueprintManifest { - readonly blueprintType = BlueprintManifestType.STUDIO + readonly blueprintType = BlueprintManifestType.STUDIO // s readonly #client: MyClient = SocketIOClient.io('http://localhost:2345', { reconnection: true, @@ -37,6 +36,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { autoConnect: true, // transports: ['websocket'], }) as MyClient + readonly #callHandlers = new Map>>() /** Unique id of the blueprint. This is used by core to check if blueprints are the same blueprint, but differing versions */ blueprintId?: string @@ -75,19 +75,46 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { this.#client.on('disconnect', () => { console.log('disconnect') + + this.#callHandlers.clear() + // TODO - abort any in-progress? }) + + listenToEvents(this.#client, this.#generateListenerRouter()) + } + + async #handleListen( + name: T, + functionId: string, + ...args: Parameters + ): Promise { + const handlers = this.#callHandlers.get(functionId) + const handler = handlers?.[name] as any + if (!handler) throw new Error(`Method "${name}" is not supported`) + + return handler(functionId, ...args) + } + + #generateListenerRouter(): EventHandlers { + return { + packageInfo_getPackageInfo: async (...args) => this.#handleListen('packageInfo_getPackageInfo', ...args), + packageInfo_hackGetMediaObjectDuration: async (...args) => + this.#handleListen('packageInfo_hackGetMediaObjectDuration', ...args), + studio_getStudioMappings: async (...args) => this.#handleListen('studio_getStudioMappings', ...args), + } } async #runProxied( name: T, functionId: string, - ...args: ParamsIfReturnIsValid + data: ParamsIfReturnIsValid[0] ): Promise> { if (!this.#client.connected) throw new Error('Blueprints are unavailable') - // TODO - timeouts? + // TODO - ensure #callHandlers is cleaned up + // TODO - timeouts? return new Promise>((resolve, reject) => { const handleDisconnect = () => { reject('Client disconnected') @@ -99,39 +126,41 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { res: ReturnType ): void => { this.#client.off('disconnect', handleDisconnect) + this.#callHandlers.delete(functionId) if (err) reject(err) else resolve(res) } - this.#client.emit(name as any, functionId, ...args, innerCb) + this.#client.emit(name as any, functionId, data, innerCb) }) } - #listenToEventsForMethod(functionId: string, handlers: any): () => void { - // TODO - - return () => { - // TODO -cleanup + #listenToEventsForMethod(functionId: string, handlers: EventHandlers>): void { + if (this.#callHandlers.has(functionId)) { + logger.warn(`Methods already registered for call ${functionId}`) } + + this.#callHandlers.set(functionId, handlers) } /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ async getBaseline(context0: IStudioBaselineContext): Promise { const context = context0 as StudioBaselineContext - const id = getRandomString() // TODO - use this properly - - const stop = this.#listenToEventsForMethod(id, {}) + const id = getRandomString() + this.#listenToEventsForMethod(id, { + // TODO - refactor this to be less error prone + packageInfo_getPackageInfo: async (_id, data) => context.getPackageInfo(data.packageId), + packageInfo_hackGetMediaObjectDuration: async (_id, data) => + context.hackGetMediaObjectDuration(data.mediaId), + studio_getStudioMappings: async (_id) => context.getStudioMappings(), + }) - try { - return this.#runProxied('studio_getBaseline', id, { - identifier: context._contextIdentifier, - studioId: context.studioId, - studioConfig: context.getStudioConfig() as IBlueprintConfig, - }) - } finally { - stop() - } + return this.#runProxied('studio_getBaseline', id, { + identifier: context._contextIdentifier, + studioId: context.studioId, + studioConfig: context.getStudioConfig() as IBlueprintConfig, + }) } /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ @@ -185,10 +214,22 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { }) } - // /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ - // preprocessConfig?: ( - // context: ICommonContext, - // config: TRawConfig, - // coreConfig: BlueprintConfigCoreConfig - // ) => TProcessedConfig + /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ + preprocessConfig( + context0: ICommonContext, + config: IBlueprintConfig, + coreConfig: BlueprintConfigCoreConfig + ): IBlueprintConfig { + const context = context0 as CommonContext + + const id = getRandomString() // TODO - use this properly + + // TODO - handle this method being optional + + return this.#runProxied('studio_applyConfig', id, { + identifier: context._contextIdentifier, + config, + coreConfig, + }) + } } From 0a7805a59195d472c98dfdbfc88585b1eac8370b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 11:24:37 +0100 Subject: [PATCH 06/11] wip: asyncify --- .../blueprints-integration/src/api/studio.ts | 2 +- packages/blueprints-proxy/src/host.ts | 3 ++- packages/blueprints-proxy/src/index.ts | 12 ++++++---- .../src/routers/studio/config.ts | 15 ++++++++++++- packages/job-worker/src/__mocks__/context.ts | 2 +- .../src/__tests__/rundownPlaylist.test.ts | 2 +- .../src/blueprints/ProxiedStudioBlueprint.ts | 6 ++--- .../src/blueprints/__tests__/config.test.ts | 8 +++---- .../__tests__/context-events.test.ts | 2 +- .../src/blueprints/__tests__/context.test.ts | 14 ++++++------ packages/job-worker/src/blueprints/config.ts | 6 ++--- .../blueprints/context/GetRundownContext.ts | 4 +++- .../context/RundownActivationContext.ts | 4 +++- .../context/RundownDataChangedEventContext.ts | 4 +++- .../context/RundownTimingEventContext.ts | 4 +++- .../blueprints/context/SegmentUserContext.ts | 4 +++- .../context/ShowStyleUserContext.ts | 4 +++- .../context/StudioBaselineContext.ts | 4 +++- .../SyncIngestUpdateToPartInstanceContext.ts | 4 +++- .../src/blueprints/context/adlibActions.ts | 8 ++++--- packages/job-worker/src/events/handle.ts | 2 ++ .../job-worker/src/ingest/bucket/import.ts | 7 +++--- packages/job-worker/src/ingest/commit.ts | 4 ++-- .../src/ingest/generationRundown.ts | 5 +++-- .../src/ingest/generationSegment.ts | 1 + .../src/ingest/syncChangesToPartInstance.ts | 1 + packages/job-worker/src/jobs/index.ts | 2 +- .../src/playout/abPlayback/index.ts | 6 ++--- .../src/playout/activePlaylistActions.ts | 12 ++++++++-- .../job-worker/src/playout/adlibAction.ts | 2 ++ packages/job-worker/src/playout/take.ts | 22 +++++++++++++------ .../src/playout/timeline/generate.ts | 5 +++-- .../src/playout/timings/partPlayback.ts | 2 +- packages/job-worker/src/rundownPlaylists.ts | 6 ++--- packages/job-worker/src/workers/context.ts | 8 ++++--- 35 files changed, 130 insertions(+), 67 deletions(-) diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 72047a4e67..3893c75619 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -71,7 +71,7 @@ export interface StudioBlueprintManifest TProcessedConfig + ) => TProcessedConfig | Promise } export interface BlueprintResultStudioBaseline { diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts index d0743d837f..ec46469d0c 100644 --- a/packages/blueprints-proxy/src/host.ts +++ b/packages/blueprints-proxy/src/host.ts @@ -5,7 +5,7 @@ import { createServer } from 'http' import { ClientToServerEvents, ServerToClientEvents } from './index' import { Server } from 'socket.io' import { listenToEvents } from './helper' -import { studio_applyConfig, studio_validateConfig } from './routers/studio/config' +import { studio_applyConfig, studio_preprocessConfig, studio_validateConfig } from './routers/studio/config' import { studio_getBaseline } from './routers/studio/baseline' export function runForBlueprints( @@ -56,6 +56,7 @@ export function runForBlueprints( studio_getBaseline: async (...args) => studio_getBaseline(studioBlueprint, socket, ...args), studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args), studio_applyConfig: async (...args) => studio_applyConfig(studioBlueprint, socket, ...args), + studio_preprocessConfig: async (...args) => studio_preprocessConfig(studioBlueprint, socket, ...args), }) }) diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index 64dc1e4bf4..d14734ea9e 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -12,14 +12,14 @@ export type ResultCallback = (err: any, res: T) => void export interface ServerToClientEvents { packageInfo_getPackageInfo: (msg: PackageInfoGetPackageInfoArgs) => Readonly - packageInfo_hackGetMediaObjectDuration: (msg: PackageInfoHackGetMediaObjectDuration) => number | undefined + packageInfo_hackGetMediaObjectDuration: (msg: PackageInfoHackGetMediaObjectDurationArgs) => number | undefined studio_getStudioMappings: () => Readonly } export interface PackageInfoGetPackageInfoArgs { packageId: string } -export interface PackageInfoHackGetMediaObjectDuration { +export interface PackageInfoHackGetMediaObjectDurationArgs { mediaId: string } @@ -27,6 +27,7 @@ export interface ClientToServerEvents { studio_getBaseline: (msg: StudioGetBaselineArgs) => BlueprintResultStudioBaseline studio_validateConfig: (msg: StudioValidateConfigArgs) => IConfigMessage[] studio_applyConfig: (msg: StudioApplyConfigArgs) => BlueprintResultApplyStudioConfig + studio_preprocessConfig: (msg: StudioPreprocessConfigArgs) => unknown } export interface StudioGetBaselineArgs { @@ -34,14 +35,17 @@ export interface StudioGetBaselineArgs { studioId: string studioConfig: IBlueprintConfig } - export interface StudioValidateConfigArgs { identifier: string config: IBlueprintConfig } - export interface StudioApplyConfigArgs { identifier: string config: IBlueprintConfig coreConfig: BlueprintConfigCoreConfig } +export interface StudioPreprocessConfigArgs { + identifier: string + config: IBlueprintConfig + coreConfig: BlueprintConfigCoreConfig +} diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts index 483fcc2548..15cf6b30bf 100644 --- a/packages/blueprints-proxy/src/routers/studio/config.ts +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -4,7 +4,7 @@ import type { StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' import { CommonContext } from '../../context/common' -import type { StudioApplyConfigArgs, StudioValidateConfigArgs } from '../../index' +import type { StudioApplyConfigArgs, StudioPreprocessConfigArgs, StudioValidateConfigArgs } from '../../index' import { MySocket } from '../util' export async function studio_validateConfig( @@ -32,3 +32,16 @@ export async function studio_applyConfig( return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) } + +export async function studio_preprocessConfig( + studioBlueprint: StudioBlueprintManifest, + _socket: MySocket, + _id: string, + msg: StudioPreprocessConfigArgs +): Promise { + if (!studioBlueprint.preprocessConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. + + const context = new CommonContext(`applyConfig ${msg.identifier}`) + + return studioBlueprint.preprocessConfig(context, msg.config, msg.coreConfig) +} diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index a129e4dd52..57d0a3f58e 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -149,7 +149,7 @@ export class MockJobContext implements JobContext { throw new Error('Method not implemented.') } - getStudioBlueprintConfig(): ProcessedStudioConfig { + async getStudioBlueprintConfig(): Promise { return preprocessStudioConfig(this.studio, this.#studioBlueprint) } async getShowStyleBases(): Promise>> { diff --git a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts index d2a256734d..36964105d8 100644 --- a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts +++ b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts @@ -77,7 +77,7 @@ describe('Rundown', () => { const allRundowns = await context.mockCollections.Rundowns.findFetch({ playlistId: playlist0._id, }) - const rundownPlaylist = produceRundownPlaylistInfoFromRundown( + const rundownPlaylist = await produceRundownPlaylistInfoFromRundown( context, undefined, playlist0, diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index 8221665931..ec71fa4e49 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -215,18 +215,18 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { } /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ - preprocessConfig( + async preprocessConfig( context0: ICommonContext, config: IBlueprintConfig, coreConfig: BlueprintConfigCoreConfig - ): IBlueprintConfig { + ): Promise { const context = context0 as CommonContext const id = getRandomString() // TODO - use this properly // TODO - handle this method being optional - return this.#runProxied('studio_applyConfig', id, { + return this.#runProxied('studio_preprocessConfig', id, { identifier: context._contextIdentifier, config, coreConfig, diff --git a/packages/job-worker/src/blueprints/__tests__/config.test.ts b/packages/job-worker/src/blueprints/__tests__/config.test.ts index bb6b6b3115..8811699dde 100644 --- a/packages/job-worker/src/blueprints/__tests__/config.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/config.test.ts @@ -11,7 +11,7 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' describe('Test blueprint config', () => { - test('compileStudioConfig', () => { + test('compileStudioConfig', async () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ ...jobContext.studio, @@ -26,7 +26,7 @@ describe('Test blueprint config', () => { studioConfigSchema: undefined, }) - const res = preprocessStudioConfig(jobContext.studio, jobContext.studioBlueprint.blueprint) + const res = await preprocessStudioConfig(jobContext.studio, jobContext.studioBlueprint.blueprint) expect(res).toEqual({ // SofieHostURL: 'host url', sdfsdf: 'one', @@ -34,7 +34,7 @@ describe('Test blueprint config', () => { }) }) - test('compileStudioConfig with function', () => { + test('compileStudioConfig with function', async () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ ...jobContext.studio, @@ -55,7 +55,7 @@ describe('Test blueprint config', () => { }, }) - const res = preprocessStudioConfig(jobContext.studio, jobContext.studioBlueprint.blueprint) + const res = await preprocessStudioConfig(jobContext.studio, jobContext.studioBlueprint.blueprint) expect(res).toEqual({ core: { hostUrl: 'https://sofie-in-jest:3000', diff --git a/packages/job-worker/src/blueprints/__tests__/context-events.test.ts b/packages/job-worker/src/blueprints/__tests__/context-events.test.ts index ee51108643..95c3e9ef86 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-events.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-events.test.ts @@ -86,7 +86,7 @@ describe('Test blueprint api context', () => { const context = new PartEventContext( 'fake', jobContext.studio, - jobContext.getStudioBlueprintConfig(), + await jobContext.getStudioBlueprintConfig(), showStyle, showStyleConfig, rundown, diff --git a/packages/job-worker/src/blueprints/__tests__/context.test.ts b/packages/job-worker/src/blueprints/__tests__/context.test.ts index d067b4a194..ddf4f55e3b 100644 --- a/packages/job-worker/src/blueprints/__tests__/context.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context.test.ts @@ -70,9 +70,9 @@ describe('Test blueprint api context', () => { }) describe('StudioContext', () => { - test('getStudio', () => { + test('getStudio', async () => { const studio = jobContext.studio - const studioConfig = jobContext.getStudioBlueprintConfig() + const studioConfig = await jobContext.getStudioBlueprintConfig() const context = new StudioContext( { name: 'studio', identifier: unprotectString(jobContext.studioId) }, studio, @@ -83,11 +83,11 @@ describe('Test blueprint api context', () => { expect(context.getStudioConfig()).toBe(studioConfig) expect(context.getStudioMappings()).toEqual(applyAndValidateOverrides(studio.mappingsWithOverrides).obj) }) - test('getStudioConfigRef', () => { + test('getStudioConfigRef', async () => { const context = new StudioContext( { name: 'studio', identifier: unprotectString(jobContext.studioId) }, jobContext.studio, - jobContext.getStudioBlueprintConfig() + await jobContext.getStudioBlueprintConfig() ) expect(context.getStudioConfigRef('conf1')).toEqual(getStudioConfigRef(jobContext.studioId, 'conf1')) @@ -105,7 +105,7 @@ describe('Test blueprint api context', () => { identifier: `fake context`, }, jobContext.studio, - jobContext.getStudioBlueprintConfig(), + await jobContext.getStudioBlueprintConfig(), showStyleCompound, showStyleConfig ) @@ -114,14 +114,14 @@ describe('Test blueprint api context', () => { expect(context.showStyleCompound).toBe(showStyleCompound) }) - test('getShowStyleConfigRef', () => { + test('getShowStyleConfigRef', async () => { const context = new ShowStyleContext( { name: 'N/A', identifier: `fake context`, }, jobContext.studio, - jobContext.getStudioBlueprintConfig(), + await jobContext.getStudioBlueprintConfig(), '1' as any, '2' as any ) diff --git a/packages/job-worker/src/blueprints/config.ts b/packages/job-worker/src/blueprints/config.ts index 1549c67ab4..f66ff1bdf4 100644 --- a/packages/job-worker/src/blueprints/config.ts +++ b/packages/job-worker/src/blueprints/config.ts @@ -110,10 +110,10 @@ export function compileCoreConfigValues(studioSettings: ReadonlyDeep, blueprint: ReadonlyDeep -): ProcessedStudioConfig { +): Promise { let res: any = applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj try { @@ -122,7 +122,7 @@ export function preprocessStudioConfig( name: `preprocessStudioConfig`, identifier: `studioId=${studio._id}`, }) - res = blueprint.preprocessConfig(context, res, compileCoreConfigValues(studio.settings)) + res = await blueprint.preprocessConfig(context, res, compileCoreConfigValues(studio.settings)) } } catch (err) { logger.error(`Error in studioBlueprint.preprocessConfig: ${stringifyError(err)}`) diff --git a/packages/job-worker/src/blueprints/context/GetRundownContext.ts b/packages/job-worker/src/blueprints/context/GetRundownContext.ts index 1bab034433..ede7d8830a 100644 --- a/packages/job-worker/src/blueprints/context/GetRundownContext.ts +++ b/packages/job-worker/src/blueprints/context/GetRundownContext.ts @@ -10,6 +10,7 @@ import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { UserContextInfo } from './CommonContext' import { ShowStyleUserContext } from './ShowStyleUserContext' import { convertRundownPlaylistToBlueprints } from './lib' +import { ProcessedStudioConfig } from '../config' export class GetRundownContext extends ShowStyleUserContext implements IGetRundownContext { private cachedPlaylistsInStudio: Promise[]> | undefined @@ -17,13 +18,14 @@ export class GetRundownContext extends ShowStyleUserContext implements IGetRundo constructor( contextInfo: UserContextInfo, context: JobContext, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, watchedPackages: WatchedPackagesHelper, private getPlaylistsInStudio: () => Promise, private getRundownsInStudio: () => Promise[]>, private getExistingRundown: () => Promise | undefined> ) { - super(contextInfo, context, showStyleCompound, watchedPackages) + super(contextInfo, context, studioConfig, showStyleCompound, watchedPackages) } private async _getPlaylistsInStudio() { if (!this.cachedPlaylistsInStudio) { diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index cbe4cf95fa..b11a23c87c 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -6,6 +6,7 @@ import { executePeripheralDeviceAction, listPlayoutDevices } from '../../periphe import { CacheForPlayout } from '../../playout/cache' import { RundownEventContext } from './RundownEventContext' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { ProcessedStudioConfig } from '../config' export class RundownActivationContext extends RundownEventContext implements IRundownActivationContext { private readonly _cache: CacheForPlayout @@ -14,12 +15,13 @@ export class RundownActivationContext extends RundownEventContext implements IRu constructor( context: JobContext, cache: CacheForPlayout, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep ) { super( context.studio, - context.getStudioBlueprintConfig(), + studioConfig, showStyleCompound, context.getShowStyleBlueprintConfig(showStyleCompound), rundown diff --git a/packages/job-worker/src/blueprints/context/RundownDataChangedEventContext.ts b/packages/job-worker/src/blueprints/context/RundownDataChangedEventContext.ts index 45addeaedd..11b3c4a3a1 100644 --- a/packages/job-worker/src/blueprints/context/RundownDataChangedEventContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownDataChangedEventContext.ts @@ -10,18 +10,20 @@ import { getCurrentTime } from '../../lib' import { JobContext, ProcessedShowStyleCompound } from '../../jobs' import { ContextInfo } from './CommonContext' import { RundownContext } from './RundownContext' +import { ProcessedStudioConfig } from '../config' export class RundownDataChangedEventContext extends RundownContext implements IRundownDataChangedEventContext { constructor( protected readonly context: JobContext, contextInfo: ContextInfo, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep ) { super( contextInfo, context.studio, - context.getStudioBlueprintConfig(), + studioConfig, showStyleCompound, context.getShowStyleBlueprintConfig(showStyleCompound), rundown diff --git a/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts b/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts index 8b25072596..461b5993f6 100644 --- a/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownTimingEventContext.ts @@ -14,6 +14,7 @@ import { MongoQuery } from '../../db' import { convertPartInstanceToBlueprints, convertPieceInstanceToBlueprints, convertSegmentToBlueprints } from './lib' import { ContextInfo } from './CommonContext' import { RundownDataChangedEventContext } from './RundownDataChangedEventContext' +import { ProcessedStudioConfig } from '../config' export class RundownTimingEventContext extends RundownDataChangedEventContext implements IRundownTimingEventContext { readonly previousPart: Readonly> | undefined @@ -29,13 +30,14 @@ export class RundownTimingEventContext extends RundownDataChangedEventContext im constructor( context: JobContext, contextInfo: ContextInfo, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep, previousPartInstance: DBPartInstance | undefined, partInstance: DBPartInstance, nextPartInstance: DBPartInstance | undefined ) { - super(context, contextInfo, showStyleCompound, rundown) + super(context, contextInfo, studioConfig, showStyleCompound, rundown) if (previousPartInstance) this.partInstanceCache.set(previousPartInstance._id, previousPartInstance) if (partInstance) this.partInstanceCache.set(partInstance._id, partInstance) diff --git a/packages/job-worker/src/blueprints/context/SegmentUserContext.ts b/packages/job-worker/src/blueprints/context/SegmentUserContext.ts index 422c00d38e..cc7e7fbbd7 100644 --- a/packages/job-worker/src/blueprints/context/SegmentUserContext.ts +++ b/packages/job-worker/src/blueprints/context/SegmentUserContext.ts @@ -7,6 +7,7 @@ import { ContextInfo } from './CommonContext' import { RundownContext } from './RundownContext' import { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { getMediaObjectDuration } from './lib' +import { ProcessedStudioConfig } from '../config' export interface RawPartNote extends INoteBase { partExternalId: string | undefined @@ -20,6 +21,7 @@ export class SegmentUserContext extends RundownContext implements ISegmentUserCo constructor( contextInfo: ContextInfo, context: JobContext, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep, private readonly watchedPackages: WatchedPackagesHelper @@ -27,7 +29,7 @@ export class SegmentUserContext extends RundownContext implements ISegmentUserCo super( contextInfo, context.studio, - context.getStudioBlueprintConfig(), + studioConfig, showStyleCompound, context.getShowStyleBlueprintConfig(showStyleCompound), rundown diff --git a/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts b/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts index efd4206355..4a44c09686 100644 --- a/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts +++ b/packages/job-worker/src/blueprints/context/ShowStyleUserContext.ts @@ -6,6 +6,7 @@ import { JobContext, ProcessedShowStyleCompound } from '../../jobs' import { UserContextInfo } from './CommonContext' import { ShowStyleContext } from './ShowStyleContext' import { getMediaObjectDuration } from './lib' +import { ProcessedStudioConfig } from '../config' export class ShowStyleUserContext extends ShowStyleContext implements IShowStyleUserContext { public readonly notes: INoteBase[] = [] @@ -16,13 +17,14 @@ export class ShowStyleUserContext extends ShowStyleContext implements IShowStyle constructor( contextInfo: UserContextInfo, context: JobContext, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, private readonly watchedPackages: WatchedPackagesHelper ) { super( contextInfo, context.studio, - context.getStudioBlueprintConfig(), + studioConfig, showStyleCompound, context.getShowStyleBlueprintConfig(showStyleCompound) ) diff --git a/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts b/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts index 64b2bdd1a1..57f60d174d 100644 --- a/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioBaselineContext.ts @@ -4,6 +4,7 @@ import { JobContext } from '../../jobs' import { UserContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' import { getMediaObjectDuration } from './lib' +import { ProcessedStudioConfig } from '../config' export class StudioBaselineContext extends StudioContext implements IStudioBaselineContext { private readonly jobContext: JobContext @@ -11,9 +12,10 @@ export class StudioBaselineContext extends StudioContext implements IStudioBasel constructor( contextInfo: UserContextInfo, context: JobContext, + studioConfig: ProcessedStudioConfig, private readonly watchedPackages: WatchedPackagesHelper ) { - super(contextInfo, context.studio, context.getStudioBlueprintConfig()) + super(contextInfo, context.studio, studioConfig) this.jobContext = context } diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index de0842da5c..f4e6f099ec 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -36,6 +36,7 @@ import { serializePieceTimelineObjectsBlob, } from '@sofie-automation/corelib/dist/dataModel/Piece' import { EXPECTED_INGEST_TO_PLAYOUT_TIME } from '@sofie-automation/shared-lib/dist/core/constants' +import { ProcessedStudioConfig } from '../config' export class SyncIngestUpdateToPartInstanceContext extends RundownUserContext @@ -52,6 +53,7 @@ export class SyncIngestUpdateToPartInstanceContext contextInfo: ContextInfo, private readonly playlistActivationId: RundownPlaylistActivationId, studio: ReadonlyDeep, + studioConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep, partInstance: DBPartInstance, @@ -62,7 +64,7 @@ export class SyncIngestUpdateToPartInstanceContext super( contextInfo, studio, - _context.getStudioBlueprintConfig(), + studioConfig, showStyleCompound, _context.getShowStyleBlueprintConfig(showStyleCompound), rundown diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index fd17e3c10c..7077ada1ab 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -68,7 +68,7 @@ import { isTooCloseToAutonext } from '../../playout/lib' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { moveNextPart } from '../../playout/moveNextPart' import _ = require('underscore') -import { ProcessedShowStyleConfig } from '../config' +import { ProcessedShowStyleConfig, ProcessedStudioConfig } from '../config' import { DatastorePersistenceMode } from '@sofie-automation/shared-lib/dist/core/model/TimelineDatastore' import { getDatastoreId } from '../../playout/datastore' import { executePeripheralDeviceAction, listPlayoutDevices } from '../../peripheralDevice' @@ -87,10 +87,11 @@ export class DatastoreActionExecutionContext constructor( contextInfo: UserContextInfo, context: JobContext, + studioConfig: ProcessedStudioConfig, showStyle: ReadonlyDeep, watchedPackages: WatchedPackagesHelper ) { - super(contextInfo, context, showStyle, watchedPackages) + super(contextInfo, context, studioConfig, showStyle, watchedPackages) this._context = context } @@ -142,12 +143,13 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct contextInfo: UserContextInfo, context: JobContext, cache: CacheForPlayout, + studioConfig: ProcessedStudioConfig, showStyle: ReadonlyDeep, _showStyleBlueprintConfig: ProcessedShowStyleConfig, rundown: DBRundown, watchedPackages: WatchedPackagesHelper ) { - super(contextInfo, context, showStyle, watchedPackages) + super(contextInfo, context, studioConfig, showStyle, watchedPackages) this._context = context this._cache = cache this.rundown = rundown diff --git a/packages/job-worker/src/events/handle.ts b/packages/job-worker/src/events/handle.ts index 6003b6b0c5..edcdcdec29 100644 --- a/packages/job-worker/src/events/handle.ts +++ b/packages/job-worker/src/events/handle.ts @@ -100,6 +100,7 @@ export async function handlePartInstanceTimings(context: JobContext, data: PartI name: rundown.name, identifier: `rundownId=${rundown._id},timestamp=${timestamp}`, }, + await context.getStudioBlueprintConfig(), showStyle, rundown, previousPartInstance, @@ -213,6 +214,7 @@ export async function handleRundownDataHasChanged(context: JobContext, data: Run name: rundown.name, identifier: `rundownId=${rundown._id},timestamp=${timestamp}`, }, + await context.getStudioBlueprintConfig(), showStyle, rundown ) diff --git a/packages/job-worker/src/ingest/bucket/import.ts b/packages/job-worker/src/ingest/bucket/import.ts index f7246eb53c..fdee2ab8ca 100644 --- a/packages/job-worker/src/ingest/bucket/import.ts +++ b/packages/job-worker/src/ingest/bucket/import.ts @@ -74,7 +74,7 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt if (!showStyleCompound) throw new Error(`Unable to create a ShowStyleCompound for ${showStyleBase._id}, ${showStyleVariant._id} `) - const rawAdlib = generateBucketAdlibForVariant(context, blueprint, showStyleCompound, data.payload) + const rawAdlib = await generateBucketAdlibForVariant(context, blueprint, showStyleCompound, data.payload) if (rawAdlib) { const importVersions: RundownImportVersions = { @@ -172,12 +172,12 @@ export async function handleBucketItemImport(context: JobContext, data: BucketIt await Promise.all(ps) } -function generateBucketAdlibForVariant( +async function generateBucketAdlibForVariant( context: JobContext, blueprint: ReadonlyDeep, showStyleCompound: ReadonlyDeep, payload: IngestAdlib -): IBlueprintAdLibPiece | IBlueprintActionManifest | null { +): Promise { const watchedPackages = WatchedPackagesHelper.empty(context) const contextForVariant = new ShowStyleUserContext( @@ -187,6 +187,7 @@ function generateBucketAdlibForVariant( tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT }, context, + await context.getStudioBlueprintConfig(), showStyleCompound, watchedPackages ) diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index ce596ed896..509f3747f2 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -168,7 +168,7 @@ export async function CommitIngestOperation( // Skip the update, if there are no rundowns left // Generate the new playlist, and ranks for the rundowns - const newPlaylist = produceRundownPlaylistInfoFromRundown( + const newPlaylist = await produceRundownPlaylistInfoFromRundown( context, context.studioBlueprint, oldPlaylist, @@ -439,7 +439,7 @@ export async function regeneratePlaylistAndRundownOrder( if (allRundowns.length > 0) { // Skip the update, if there are no rundowns left // Generate the new playlist, and ranks for the rundowns - const newPlaylist = produceRundownPlaylistInfoFromRundown( + const newPlaylist = await produceRundownPlaylistInfoFromRundown( context, context.studioBlueprint, oldPlaylist, diff --git a/packages/job-worker/src/ingest/generationRundown.ts b/packages/job-worker/src/ingest/generationRundown.ts index 31788d2279..b4ebd2a1ea 100644 --- a/packages/job-worker/src/ingest/generationRundown.ts +++ b/packages/job-worker/src/ingest/generationRundown.ts @@ -78,7 +78,7 @@ export async function updateRundownFromIngestData( tempSendUserNotesIntoBlackHole: true, }, context.studio, - context.getStudioBlueprintConfig() + await context.getStudioBlueprintConfig() ) // TODO-CONTEXT save any user notes from selectShowStyleContext const showStyle = await selectShowStyleVariant(context, selectShowStyleContext, extendedIngestRundown) @@ -175,7 +175,7 @@ export async function updateRundownMetadataFromIngestData( tempSendUserNotesIntoBlackHole: true, }, context.studio, - context.getStudioBlueprintConfig() + await context.getStudioBlueprintConfig() ) // TODO-CONTEXT save any user notes from selectShowStyleContext @@ -282,6 +282,7 @@ export async function getRundownFromIngestData( identifier: `showStyleBaseId=${showStyle.base._id},showStyleVariantId=${showStyle.variant._id}`, }, context, + await context.getStudioBlueprintConfig(), showStyle.compound, rundownBaselinePackages, async () => { diff --git a/packages/job-worker/src/ingest/generationSegment.ts b/packages/job-worker/src/ingest/generationSegment.ts index de56351d95..465fc152c9 100644 --- a/packages/job-worker/src/ingest/generationSegment.ts +++ b/packages/job-worker/src/ingest/generationSegment.ts @@ -107,6 +107,7 @@ export async function calculateSegmentsFromIngestData( identifier: `rundownId=${rundown._id}`, }, context, + await context.getStudioBlueprintConfig(), showStyle, rundown, watchedPackages diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 21584c5b9e..2dbad9be73 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -160,6 +160,7 @@ export async function syncChangesToPartInstances( }, cache.Playlist.doc.activationId, context.studio, + await context.getStudioBlueprintConfig(), showStyle, rundown, existingPartInstance, diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index 65fb5a481c..469088a718 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -95,7 +95,7 @@ export interface StudioCacheContext { * Processed Blueprint config for the studio the job belongs to * @returns Processed configuration blob */ - getStudioBlueprintConfig(): ProcessedStudioConfig + getStudioBlueprintConfig(): Promise /** * Get the ShowStyleBases that are allowed in the Studio diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index b27d0bbb03..1b501d41b9 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -25,7 +25,7 @@ import { ABPlayerDefinition } from '@sofie-automation/blueprints-integration' * @param timelineObjects The current timeline * @returns New AB assignments to be persisted on the playlist for the next call */ -export function applyAbPlaybackForTimeline( +export async function applyAbPlaybackForTimeline( context: JobContext, abSessionHelper: AbSessionHelper, blueprint: ReadonlyDeep, @@ -33,7 +33,7 @@ export function applyAbPlaybackForTimeline( playlist: ReadonlyDeep, resolvedPieces: ResolvedPieceInstance[], timelineObjects: OnGenerateTimelineObjExt[] -): Record { +): Promise> { if (!blueprint.blueprint.getAbResolverConfiguration) return {} const blueprintContext = new ShowStyleContext( @@ -42,7 +42,7 @@ export function applyAbPlaybackForTimeline( identifier: `playlistId=${playlist._id},previousPartInstance=${playlist.previousPartInfo?.partInstanceId},currentPartInstance=${playlist.currentPartInfo?.partInstanceId},nextPartInstance=${playlist.nextPartInfo?.partInstanceId}`, }, context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle) ) diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index a00864df00..34052bb1e0 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -119,10 +119,11 @@ export async function activateRundownPlaylist( if (!rundown) return // if the proper rundown hasn't been found, there's little point doing anything else const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) const blueprint = await context.getShowStyleBlueprint(showStyle._id) + const studioConfig = await context.getStudioBlueprintConfig() try { if (blueprint.blueprint.onRundownActivate) { - const blueprintContext = new RundownActivationContext(context, cache, showStyle, rundown) + const blueprintContext = new RundownActivationContext(context, cache, studioConfig, showStyle, rundown) await blueprint.blueprint.onRundownActivate(blueprintContext, wasActive) } @@ -142,10 +143,17 @@ export async function deactivateRundownPlaylist(context: JobContext, cache: Cach if (rundown) { const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) const blueprint = await context.getShowStyleBlueprint(showStyle._id) + const studioConfig = await context.getStudioBlueprintConfig() try { if (blueprint.blueprint.onRundownDeActivate) { - const blueprintContext = new RundownActivationContext(context, cache, showStyle, rundown) + const blueprintContext = new RundownActivationContext( + context, + cache, + studioConfig, + showStyle, + rundown + ) await blueprint.blueprint.onRundownDeActivate(blueprintContext) } } catch (err) { diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index b6743b0c31..3d243df608 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -136,6 +136,7 @@ export async function executeActionInner( }, context, cache, + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), rundown, @@ -224,6 +225,7 @@ async function executeDataStoreAction( tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT store these notes }, context, + await context.getStudioBlueprintConfig(), showStyle, watchedPackages ) diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 0147485405..f75613920e 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -190,7 +190,7 @@ export async function performTakeToNextedPart(context: JobContext, cache: CacheF new PartEventContext( 'onPreTake', context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), takeRundown, @@ -203,7 +203,15 @@ export async function performTakeToNextedPart(context: JobContext, cache: CacheF if (span) span.end() } - updatePartInstanceOnTake(context, cache, showStyle, blueprint, takeRundown, takePartInstance, currentPartInstance) + await updatePartInstanceOnTake( + context, + cache, + showStyle, + blueprint, + takeRundown, + takePartInstance, + currentPartInstance + ) cache.Playlist.update((p) => { p.previousPartInfo = p.currentPartInfo @@ -359,7 +367,7 @@ async function afterTakeUpdateTimingsAndEvents( new PartEventContext( 'onRundownFirstTake', context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), takeRundown, @@ -380,7 +388,7 @@ async function afterTakeUpdateTimingsAndEvents( new PartEventContext( 'onPostTake', context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), takeRundown, @@ -395,7 +403,7 @@ async function afterTakeUpdateTimingsAndEvents( } } -export function updatePartInstanceOnTake( +export async function updatePartInstanceOnTake( context: JobContext, cache: CacheForPlayout, showStyle: ReadonlyDeep, @@ -403,7 +411,7 @@ export function updatePartInstanceOnTake( takeRundown: DBRundown, takePartInstance: DBPartInstance, currentPartInstance: DBPartInstance | undefined -): void { +): Promise { const playlist = cache.Playlist.doc // TODO - the state could change after this sampling point. This should be handled properly @@ -423,7 +431,7 @@ export function updatePartInstanceOnTake( },execution=${getRandomId()}`, }, context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), takeRundown diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index fd769cf0ce..da8eb3d3f6 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -112,6 +112,7 @@ export async function updateStudioTimeline( new StudioBaselineContext( { name: 'studioBaseline', identifier: `studioId=${studio._id}` }, context, + await context.getStudioBlueprintConfig(), watchedPackages ) ) @@ -372,7 +373,7 @@ async function getTimelineRundown( const resolvedPieces = getResolvedPiecesFromFullTimeline(context, cache, timelineObjs) const blueprintContext = new OnTimelineGenerateContext( context.studio, - context.getStudioBlueprintConfig(), + await context.getStudioBlueprintConfig(), showStyle, context.getShowStyleBlueprintConfig(showStyle), cache.Playlist.doc, @@ -384,7 +385,7 @@ async function getTimelineRundown( ) try { const abHelper = blueprintContext.abSessionsHelper // Future: this should be removed from OnTimelineGenerateContext once the methods are removed from the api - const newAbSessionsResult = applyAbPlaybackForTimeline( + const newAbSessionsResult = await applyAbPlaybackForTimeline( context, abHelper, blueprint, diff --git a/packages/job-worker/src/playout/timings/partPlayback.ts b/packages/job-worker/src/playout/timings/partPlayback.ts index 66186e2b7a..44e564d024 100644 --- a/packages/job-worker/src/playout/timings/partPlayback.ts +++ b/packages/job-worker/src/playout/timings/partPlayback.ts @@ -81,7 +81,7 @@ export async function onPartPlaybackStarted( showStyleRundown.showStyleBaseId ) const blueprint = await context.getShowStyleBlueprint(showStyle._id) - updatePartInstanceOnTake( + await updatePartInstanceOnTake( context, cache, showStyle, diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 4567c0fcdc..2bfbf31c82 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -164,14 +164,14 @@ export async function removeRundownFromDb(context: JobContext, lock: RundownLock ]) } -export function produceRundownPlaylistInfoFromRundown( +export async function produceRundownPlaylistInfoFromRundown( context: JobContext, studioBlueprint: ReadonlyDeep | undefined, existingPlaylist: ReadonlyDeep | undefined, playlistId: RundownPlaylistId, playlistExternalId: string, rundowns: ReadonlyDeep> -): DBRundownPlaylist { +): Promise { let playlistInfo: BlueprintResultRundownPlaylist | null = null try { if (studioBlueprint?.blueprint?.getRundownPlaylistInfo) { @@ -185,7 +185,7 @@ export function produceRundownPlaylistInfoFromRundown( tempSendUserNotesIntoBlackHole: true, }, context.studio, - context.getStudioBlueprintConfig() + await context.getStudioBlueprintConfig() ), rundowns.map(convertRundownToBlueprints), playlistExternalId diff --git a/packages/job-worker/src/workers/context.ts b/packages/job-worker/src/workers/context.ts index d75c8bd3f9..e8db80a445 100644 --- a/packages/job-worker/src/workers/context.ts +++ b/packages/job-worker/src/workers/context.ts @@ -64,11 +64,13 @@ export class StudioCacheContextImpl implements StudioCacheContext { return this.cacheData.studioBlueprint } - getStudioBlueprintConfig(): ProcessedStudioConfig { + async getStudioBlueprintConfig(): Promise { if (!this.cacheData.studioBlueprintConfig) { - this.cacheData.studioBlueprintConfig = deepFreeze( - clone(preprocessStudioConfig(this.cacheData.studio, this.cacheData.studioBlueprint.blueprint) ?? null) + const receivedConfig = await preprocessStudioConfig( + this.cacheData.studio, + this.cacheData.studioBlueprint.blueprint ) + this.cacheData.studioBlueprintConfig = deepFreeze(clone(receivedConfig ?? null)) } return this.cacheData.studioBlueprintConfig From 5adc21582c872a73ae105d99e947c42d70ad5499 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 12:33:24 +0100 Subject: [PATCH 07/11] fix: lint --- meteor/lib/api/methods.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index 6160ff7d45..b3845ee192 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -66,26 +66,6 @@ export const MeteorCall: IMeteorCall = { system: makeMethods(SystemAPIMethods), } -/** - * Convenience method to convert a Meteor.apply() into a Promise - * @param callName {string} Method name - * @param args {Array} An array of arguments for the method call - * @param options (Optional) An object with options for the call. See Meteor documentation. - * @returns {Promise} A promise containing the result of the called method. - */ -async function MeteorPromiseApply( - callName: Parameters[0], - args: Parameters[1], - options?: Parameters[2] -): Promise { - return new Promise((resolve, reject) => { - Meteor.apply(callName, args, options, (err, res) => { - if (err) reject(err) - else resolve(res) - }) - }) -} - function makeMethods( methods: Enum, /** (Optional) An array of methodnames. Calls to these methods won't be retried in the case of a loss-of-connection for the client. */ From 1e21c14d8236d16beb2019ec873075f45d4e8c21 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 12:43:40 +0100 Subject: [PATCH 08/11] wip: implement remaining studio blueprint api --- .../blueprints-integration/src/api/studio.ts | 4 +- .../blueprints-proxy/src/context/common.ts | 25 +++- .../src/context/studioContext.ts | 29 +++++ .../src/context/studioUserContext.ts | 29 +++++ packages/blueprints-proxy/src/helper.ts | 22 +++- packages/blueprints-proxy/src/host.ts | 18 +-- packages/blueprints-proxy/src/index.ts | 29 ++++- .../src/routers/studio/baseline.ts | 39 ++----- .../src/routers/studio/config.ts | 18 +-- .../src/routers/studio/rundown.ts | 28 +++++ packages/blueprints-proxy/src/routers/util.ts | 13 ++- .../src/blueprints/ProxiedStudioBlueprint.ts | 109 ++++++++++++++---- .../src/ingest/selectShowStyleVariant.ts | 2 +- packages/job-worker/src/rundownPlaylists.ts | 2 +- packages/tsconfig.json | 3 +- 15 files changed, 282 insertions(+), 88 deletions(-) create mode 100644 packages/blueprints-proxy/src/context/studioContext.ts create mode 100644 packages/blueprints-proxy/src/context/studioUserContext.ts create mode 100644 packages/blueprints-proxy/src/routers/studio/rundown.ts diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 3893c75619..9f1fcedf7a 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -37,14 +37,14 @@ export interface StudioBlueprintManifest>, ingestRundown: ExtendedIngestRundown - ) => string | null + ) => string | null | Promise /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ getRundownPlaylistInfo?: ( context: IStudioUserContext, rundowns: IBlueprintRundownDB[], playlistExternalId: string - ) => BlueprintResultRundownPlaylist | null + ) => BlueprintResultRundownPlaylist | null | Promise /** * Validate the config passed to this blueprint diff --git a/packages/blueprints-proxy/src/context/common.ts b/packages/blueprints-proxy/src/context/common.ts index a2ed6d418a..c3edfe8a3f 100644 --- a/packages/blueprints-proxy/src/context/common.ts +++ b/packages/blueprints-proxy/src/context/common.ts @@ -1,5 +1,8 @@ import { ICommonContext, NoteSeverity } from '@sofie-automation/blueprints-integration' import * as crypto from 'crypto' +import { ParamsIfReturnIsNever, ParamsIfReturnIsValid } from '../helper' +import { callHelper, emitHelper, MySocket } from '../routers/util' +import { ServerToClientEvents } from '..' function getHash(str: string): string { const hash = crypto.createHash('sha1') @@ -9,12 +12,32 @@ function getHash(str: string): string { export class CommonContext implements ICommonContext { private readonly _contextName: string + readonly #socket: MySocket + readonly #functionId: string + private hashI = 0 private hashed: { [hash: string]: string } = {} - constructor(identifier: string) { + constructor(identifier: string, socket: MySocket, functionId: string) { this._contextName = identifier + + this.#socket = socket + this.#functionId = functionId + } + + protected emitMessage( + name: T, + data: ParamsIfReturnIsNever[0] + ): void { + return emitHelper(this.#socket, this.#functionId, name, data) + } + protected emitCall( + name: T, + data: ParamsIfReturnIsValid[0] + ): Promise> { + return callHelper(this.#socket, this.#functionId, name, data) } + getHashId(str: string, isNotUnique?: boolean): string { if (!str) str = 'hash' + this.hashI++ diff --git a/packages/blueprints-proxy/src/context/studioContext.ts b/packages/blueprints-proxy/src/context/studioContext.ts new file mode 100644 index 0000000000..f9ad1b343b --- /dev/null +++ b/packages/blueprints-proxy/src/context/studioContext.ts @@ -0,0 +1,29 @@ +import { BlueprintMappings, IStudioContext } from '@sofie-automation/blueprints-integration' +import { MySocket } from '../routers/util' +import { StudioContextArgs } from '..' +import { CommonContext } from './common' + +export class StudioContext extends CommonContext implements IStudioContext { + readonly #data: StudioContextArgs + + public get studioId(): string { + return this.#data.studioId + } + + constructor(functionName: string, socket: MySocket, functionId: string, msg: StudioContextArgs) { + super(`${functionName} ${msg.identifier}`, socket, functionId) + + this.#data = msg + } + + getStudioConfig(): unknown { + return this.#data.studioConfig + } + getStudioConfigRef(configKey: string): string { + // TODO - we should avoid duplicating this logic + return '${studio.' + this.#data.studioId + '.' + configKey + '}' + } + async getStudioMappings(): Promise> { + return this.emitCall('studio_getStudioMappings', {}) + } +} diff --git a/packages/blueprints-proxy/src/context/studioUserContext.ts b/packages/blueprints-proxy/src/context/studioUserContext.ts new file mode 100644 index 0000000000..19c981a8f8 --- /dev/null +++ b/packages/blueprints-proxy/src/context/studioUserContext.ts @@ -0,0 +1,29 @@ +import { IStudioUserContext } from '@sofie-automation/blueprints-integration' +import { MySocket } from '../routers/util' +import { StudioContextArgs } from '..' +import { StudioContext } from './studioContext' + +export class StudioUserContext extends StudioContext implements IStudioUserContext { + constructor(functionName: string, socket: MySocket, functionId: string, msg: StudioContextArgs) { + super(functionName, socket, functionId, msg) + } + + notifyUserError(message: string, params?: { [key: string]: any } | undefined): void { + this.emitMessage('common_notifyUserError', { + message, + params, + }) + } + notifyUserWarning(message: string, params?: { [key: string]: any } | undefined): void { + this.emitMessage('common_notifyUserWarning', { + message, + params, + }) + } + notifyUserInfo(message: string, params?: { [key: string]: any } | undefined): void { + this.emitMessage('common_notifyUserInfo', { + message, + params, + }) + } +} diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts index 05b5474842..f33dfe8dc2 100644 --- a/packages/blueprints-proxy/src/helper.ts +++ b/packages/blueprints-proxy/src/helper.ts @@ -1,10 +1,11 @@ /** * Signature for the handler functions */ +type HandlerReturnType any> = ReturnType extends never ? void : Promise> type HandlerFunction any> = ( functionId: string, ...args: Parameters -) => Promise> +) => HandlerReturnType type HandlerFunctionOrNever = T extends (...args: any) => any ? HandlerFunction : never @@ -19,26 +20,38 @@ export type ParamsIfReturnIsValid any> = ReturnTyp ? never : Parameters +export type ParamsIfReturnIsNever any> = ReturnType extends never + ? Parameters + : never + /** Subscribe to all the events defined in the handlers, and wrap with safety and logging */ export function listenToEvents(socket: any, handlers: EventHandlers): void { // const logger = createChildLogger(`module/${connectionId}`); for (const [event, handler] of Object.entries(handlers)) { socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback) => { + const doError = (msg: string) => { + console.warn(msg) + if (cb && typeof cb === 'function') { + cb(msg, null) + } else { + socket.close() + } + } // TODO - find/reject callback? console.log('running', event, functionId, JSON.stringify(msg)) if (!functionId || typeof functionId !== 'string') { - console.warn(`Received malformed functionId "${event}"`) + doError(`Received malformed functionId "${event}"`) return // Ignore messages without correct structure } if (!msg || typeof msg !== 'object') { - console.warn(`Received malformed message object "${event}"`) + doError(`Received malformed message object "${event}"`) return // Ignore messages without correct structure } if (cb && typeof cb !== 'function') { - console.warn(`Received malformed callback "${event}"`) + doError(`Received malformed callback "${event}"`) return // Ignore messages without correct structure } @@ -51,6 +64,7 @@ export function listenToEvents(socket: any, handlers: EventHan } catch (e: any) { console.error(`Command failed: ${e}`, e.stack) if (cb) cb(e?.toString() ?? JSON.stringify(e), undefined) + else socket.close() } }) } diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts index ec46469d0c..633fdad18c 100644 --- a/packages/blueprints-proxy/src/host.ts +++ b/packages/blueprints-proxy/src/host.ts @@ -1,12 +1,11 @@ import { ShowStyleBlueprintManifest, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' -// import { proxyStudioBlueprint } from './blueprint/studio' -// import { klona } from 'klona/full' import { createServer } from 'http' import { ClientToServerEvents, ServerToClientEvents } from './index' import { Server } from 'socket.io' import { listenToEvents } from './helper' import { studio_applyConfig, studio_preprocessConfig, studio_validateConfig } from './routers/studio/config' import { studio_getBaseline } from './routers/studio/baseline' +import { studio_getRundownPlaylistInfo, studio_getShowStyleId } from './routers/studio/rundown' export function runForBlueprints( studioBlueprint: StudioBlueprintManifest, @@ -33,7 +32,6 @@ export function runForBlueprints( origin: (o, cb) => cb(null, o), credentials: true, }, - // options }) io.on('connection', (socket) => { @@ -42,18 +40,10 @@ export function runForBlueprints( // subscribe to socket events from host listenToEvents(socket, { - // init: this._handleInit.bind(this), - // destroy: this._handleDestroy.bind(this), - // updateConfig: this._handleConfigUpdate.bind(this), - // executeAction: this._handleExecuteAction.bind(this), - // updateFeedbacks: this._handleUpdateFeedbacks.bind(this), - // updateActions: this._handleUpdateActions.bind(this), - // getConfigFields: this._handleGetConfigFields.bind(this), - // handleHttpRequest: this._handleHttpRequest.bind(this), - // learnAction: this._handleLearnAction.bind(this), - // learnFeedback: this._handleLearnFeedback.bind(this), - // startStopRecordActions: this._handleStartStopRecordActions.bind(this), studio_getBaseline: async (...args) => studio_getBaseline(studioBlueprint, socket, ...args), + studio_getShowStyleId: async (...args) => studio_getShowStyleId(studioBlueprint, socket, ...args), + studio_getRundownPlaylistInfo: async (...args) => + studio_getRundownPlaylistInfo(studioBlueprint, socket, ...args), studio_validateConfig: async (...args) => studio_validateConfig(studioBlueprint, socket, ...args), studio_applyConfig: async (...args) => studio_applyConfig(studioBlueprint, socket, ...args), studio_preprocessConfig: async (...args) => studio_preprocessConfig(studioBlueprint, socket, ...args), diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index d14734ea9e..ef5a56d09f 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -2,20 +2,34 @@ import type { BlueprintConfigCoreConfig, BlueprintMappings, BlueprintResultApplyStudioConfig, + BlueprintResultRundownPlaylist, BlueprintResultStudioBaseline, + ExtendedIngestRundown, IBlueprintConfig, + IBlueprintRundownDB, + IBlueprintShowStyleBase, IConfigMessage, PackageInfo, } from '@sofie-automation/blueprints-integration' +import { ReadonlyDeep } from 'type-fest' export type ResultCallback = (err: any, res: T) => void +export type EmptyArgs = Record + export interface ServerToClientEvents { + common_notifyUserError: (msg: NotifyUserArgs) => never + common_notifyUserWarning: (msg: NotifyUserArgs) => never + common_notifyUserInfo: (msg: NotifyUserArgs) => never packageInfo_getPackageInfo: (msg: PackageInfoGetPackageInfoArgs) => Readonly packageInfo_hackGetMediaObjectDuration: (msg: PackageInfoHackGetMediaObjectDurationArgs) => number | undefined - studio_getStudioMappings: () => Readonly + studio_getStudioMappings: (msg: EmptyArgs) => Readonly } +export interface NotifyUserArgs { + message: string + params: { [key: string]: any } | undefined +} export interface PackageInfoGetPackageInfoArgs { packageId: string } @@ -25,16 +39,27 @@ export interface PackageInfoHackGetMediaObjectDurationArgs { export interface ClientToServerEvents { studio_getBaseline: (msg: StudioGetBaselineArgs) => BlueprintResultStudioBaseline + studio_getShowStyleId: (msg: StudioGetShowStyleIdArgs) => string | null + studio_getRundownPlaylistInfo: (msg: StudioGetRundownPlaylistInfo) => BlueprintResultRundownPlaylist | null studio_validateConfig: (msg: StudioValidateConfigArgs) => IConfigMessage[] studio_applyConfig: (msg: StudioApplyConfigArgs) => BlueprintResultApplyStudioConfig studio_preprocessConfig: (msg: StudioPreprocessConfigArgs) => unknown } -export interface StudioGetBaselineArgs { +export interface StudioContextArgs { identifier: string studioId: string studioConfig: IBlueprintConfig } +export type StudioGetBaselineArgs = StudioContextArgs +export interface StudioGetShowStyleIdArgs extends StudioContextArgs { + showStyles: ReadonlyDeep> + ingestRundown: ExtendedIngestRundown +} +export interface StudioGetRundownPlaylistInfo extends StudioContextArgs { + rundowns: IBlueprintRundownDB[] + playlistExternalId: string +} export interface StudioValidateConfigArgs { identifier: string config: IBlueprintConfig diff --git a/packages/blueprints-proxy/src/routers/studio/baseline.ts b/packages/blueprints-proxy/src/routers/studio/baseline.ts index baeefea2e6..28394f92f8 100644 --- a/packages/blueprints-proxy/src/routers/studio/baseline.ts +++ b/packages/blueprints-proxy/src/routers/studio/baseline.ts @@ -6,41 +6,22 @@ import { StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' import { StudioGetBaselineArgs } from '@sofie-automation/shared-lib' -import { CommonContext } from '../../context/common' -import { callHelper, MySocket } from '../util' - -class StudioBaselineContext extends CommonContext implements IStudioBaselineContext { - readonly #data: StudioGetBaselineArgs - readonly #socket: MySocket - readonly #functionId: string - - public get studioId(): string { - return this.#data.studioId - } +import { StudioContext } from '../../context/studioContext' +import { MySocket } from '../util' +class StudioBaselineContext extends StudioContext implements IStudioBaselineContext { constructor(msg: StudioGetBaselineArgs, socket: MySocket, functionId: string) { - super(`getBaseline ${msg.identifier}`) - - this.#data = msg - this.#socket = socket - this.#functionId = functionId + super('getBaseline', socket, functionId, msg) } - getStudioConfig(): unknown { - return this.#data.studioConfig - } - getStudioConfigRef(configKey: string): string { - // TODO - we should avoid duplicating this logic - return '${studio.' + this.#data.studioId + '.' + configKey + '}' - } async getStudioMappings(): Promise> { - return callHelper(this.#socket, this.#functionId, 'studio_getStudioMappings', {}) + return this.emitCall('studio_getStudioMappings', {}) } async getPackageInfo(packageId: string): Promise { - return callHelper(this.#socket, this.#functionId, 'packageInfo_getPackageInfo', { packageId }) + return this.emitCall('packageInfo_getPackageInfo', { packageId }) } async hackGetMediaObjectDuration(mediaId: string): Promise { - return callHelper(this.#socket, this.#functionId, 'packageInfo_hackGetMediaObjectDuration', { mediaId }) + return this.emitCall('packageInfo_hackGetMediaObjectDuration', { mediaId }) } } @@ -52,9 +33,5 @@ export async function studio_getBaseline( ): Promise { const context = new StudioBaselineContext(msg, socket, id) - const result = await studioBlueprint.getBaseline(context) - - // TODO - cleanup? - - return result + return studioBlueprint.getBaseline(context) } diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts index 15cf6b30bf..aafa79bc5f 100644 --- a/packages/blueprints-proxy/src/routers/studio/config.ts +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -9,39 +9,39 @@ import { MySocket } from '../util' export async function studio_validateConfig( studioBlueprint: StudioBlueprintManifest, - _socket: MySocket, - _id: string, + socket: MySocket, + id: string, msg: StudioValidateConfigArgs ): Promise { if (!studioBlueprint.validateConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`validateConfig ${msg.identifier}`) + const context = new CommonContext(`validateConfig ${msg.identifier}`, socket, id) return studioBlueprint.validateConfig(context, msg.config) } export async function studio_applyConfig( studioBlueprint: StudioBlueprintManifest, - _socket: MySocket, - _id: string, + socket: MySocket, + id: string, msg: StudioApplyConfigArgs ): Promise { if (!studioBlueprint.applyConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`applyConfig ${msg.identifier}`) + const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, id) return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) } export async function studio_preprocessConfig( studioBlueprint: StudioBlueprintManifest, - _socket: MySocket, - _id: string, + socket: MySocket, + id: string, msg: StudioPreprocessConfigArgs ): Promise { if (!studioBlueprint.preprocessConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`applyConfig ${msg.identifier}`) + const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, id) return studioBlueprint.preprocessConfig(context, msg.config, msg.coreConfig) } diff --git a/packages/blueprints-proxy/src/routers/studio/rundown.ts b/packages/blueprints-proxy/src/routers/studio/rundown.ts new file mode 100644 index 0000000000..96ca2df15d --- /dev/null +++ b/packages/blueprints-proxy/src/routers/studio/rundown.ts @@ -0,0 +1,28 @@ +import { BlueprintResultRundownPlaylist, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { StudioGetRundownPlaylistInfo, StudioGetShowStyleIdArgs } from '@sofie-automation/shared-lib' +import { StudioUserContext } from '../../context/studioUserContext' +import { MySocket } from '../util' + +export async function studio_getShowStyleId( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + id: string, + msg: StudioGetShowStyleIdArgs +): Promise { + const context = new StudioUserContext('getShowStyleId', socket, id, msg) + + return studioBlueprint.getShowStyleId(context, msg.showStyles, msg.ingestRundown) +} + +export async function studio_getRundownPlaylistInfo( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + id: string, + msg: StudioGetRundownPlaylistInfo +): Promise { + if (!studioBlueprint.getRundownPlaylistInfo) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. + + const context = new StudioUserContext('getRundownPlaylistInfo', socket, id, msg) + + return studioBlueprint.getRundownPlaylistInfo(context, msg.rundowns, msg.playlistExternalId) +} diff --git a/packages/blueprints-proxy/src/routers/util.ts b/packages/blueprints-proxy/src/routers/util.ts index 727c5ea453..7f69eb8d0c 100644 --- a/packages/blueprints-proxy/src/routers/util.ts +++ b/packages/blueprints-proxy/src/routers/util.ts @@ -1,5 +1,5 @@ import { Socket } from 'socket.io' -import { ParamsIfReturnIsValid, ResultCallback } from '../helper' +import { ParamsIfReturnIsNever, ParamsIfReturnIsValid, ResultCallback } from '../helper' import { ClientToServerEvents, ServerToClientEvents } from '..' export type MySocket = Socket @@ -33,3 +33,14 @@ export async function callHelper( socket.emit(name as any, functionId, data, innerCb) }) } + +export function emitHelper( + socket: MySocket, + functionId: string, + name: T, + data: ParamsIfReturnIsNever[0] +): void { + if (!socket.connected) throw new Error('Blueprints are unavailable') + + socket.emit(name as any, functionId, data) +} diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index ec71fa4e49..a10421d511 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -2,15 +2,20 @@ import { BlueprintConfigCoreConfig, BlueprintManifestType, BlueprintResultApplyStudioConfig, + BlueprintResultRundownPlaylist, BlueprintResultStudioBaseline, ExtendedIngestRundown, IBlueprintConfig, + IBlueprintRundownDB, IBlueprintShowStyleBase, ICommonContext, IConfigMessage, + IPackageInfoContext, IShowStyleConfigPreset, IStudioBaselineContext, + IStudioContext, IStudioUserContext, + IUserNotesContext, JSONBlob, JSONBlobStringify, JSONSchema, @@ -22,7 +27,7 @@ import { logger } from '../logging' import * as SocketIOClient from 'socket.io-client' import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' import { ReadonlyDeep } from 'type-fest' -import { CommonContext, StudioBaselineContext } from './context' +import { CommonContext, StudioBaselineContext, StudioUserContext } from './context' import { EventHandlers, listenToEvents, ParamsIfReturnIsValid } from '@sofie-automation/blueprints-proxy/dist/helper' type MyClient = SocketIOClient.Socket @@ -98,6 +103,10 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { #generateListenerRouter(): EventHandlers { return { + common_notifyUserError: async (...args) => this.#handleListen('common_notifyUserError', ...args), + common_notifyUserWarning: async (...args) => this.#handleListen('common_notifyUserWarning', ...args), + common_notifyUserInfo: async (...args) => this.#handleListen('common_notifyUserInfo', ...args), + packageInfo_getPackageInfo: async (...args) => this.#handleListen('packageInfo_getPackageInfo', ...args), packageInfo_hackGetMediaObjectDuration: async (...args) => this.#handleListen('packageInfo_hackGetMediaObjectDuration', ...args), @@ -143,17 +152,43 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { this.#callHandlers.set(functionId, handlers) } + #packageInfoContextMethods(context: IPackageInfoContext): EventHandlers> { + return { + packageInfo_getPackageInfo: async (_id, data) => context.getPackageInfo(data.packageId), + packageInfo_hackGetMediaObjectDuration: async (_id, data) => + context.hackGetMediaObjectDuration(data.mediaId), + } + } + + #studioContextMethods(context: IStudioContext): EventHandlers> { + return { + studio_getStudioMappings: async (_id) => context.getStudioMappings(), + } + } + + #userNotesContextMethods(context: IUserNotesContext): EventHandlers> { + return { + common_notifyUserError: (_id, msg) => context.notifyUserError(msg.message, msg.params), + common_notifyUserWarning: (_id, msg) => context.notifyUserWarning(msg.message, msg.params), + common_notifyUserInfo: (_id, msg) => context.notifyUserInfo(msg.message, msg.params), + } + } + + #studioUserContextMethods(context: IStudioUserContext): EventHandlers> { + return { + ...this.#studioContextMethods(context), + ...this.#userNotesContextMethods(context), + } + } + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ async getBaseline(context0: IStudioBaselineContext): Promise { const context = context0 as StudioBaselineContext const id = getRandomString() this.#listenToEventsForMethod(id, { - // TODO - refactor this to be less error prone - packageInfo_getPackageInfo: async (_id, data) => context.getPackageInfo(data.packageId), - packageInfo_hackGetMediaObjectDuration: async (_id, data) => - context.hackGetMediaObjectDuration(data.mediaId), - studio_getStudioMappings: async (_id) => context.getStudioMappings(), + ...this.#studioContextMethods(context), + ...this.#packageInfoContextMethods(context), }) return this.#runProxied('studio_getBaseline', id, { @@ -164,25 +199,57 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { } /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ - getShowStyleId( - _context: IStudioUserContext, - _showStyles: ReadonlyDeep>, - _ingestRundown: ExtendedIngestRundown - ): string | null { - return null + async getShowStyleId( + context0: IStudioUserContext, + showStyles: ReadonlyDeep>, + ingestRundown: ExtendedIngestRundown + ): Promise { + const context = context0 as StudioUserContext + + const id = getRandomString() + this.#listenToEventsForMethod(id, { + ...this.#studioUserContextMethods(context), + }) + + return this.#runProxied('studio_getShowStyleId', id, { + identifier: context._contextIdentifier, + studioId: context.studioId, + studioConfig: context.getStudioConfig() as IBlueprintConfig, + + showStyles, + ingestRundown, + }) } - // /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ - // getRundownPlaylistInfo?: ( - // context: IStudioUserContext, - // rundowns: IBlueprintRundownDB[], - // playlistExternalId: string - // ) => BlueprintResultRundownPlaylist | null + /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ + async getRundownPlaylistInfo( + context0: IStudioUserContext, + rundowns: IBlueprintRundownDB[], + playlistExternalId: string + ): Promise { + const context = context0 as StudioUserContext + + // TODO - handle this method being optional + + const id = getRandomString() + this.#listenToEventsForMethod(id, { + ...this.#studioUserContextMethods(context), + }) + + return this.#runProxied('studio_getRundownPlaylistInfo', id, { + identifier: context._contextIdentifier, + studioId: context.studioId, + studioConfig: context.getStudioConfig() as IBlueprintConfig, + + rundowns, + playlistExternalId, + }) + } async validateConfig(context0: ICommonContext, config: IBlueprintConfig): Promise> { const context = context0 as CommonContext - const id = getRandomString() // TODO - use this properly + const id = getRandomString() // TODO - handle this method being optional @@ -203,7 +270,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ): Promise { const context = context0 as CommonContext - const id = getRandomString() // TODO - use this properly + const id = getRandomString() // TODO - handle this method being optional @@ -222,7 +289,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ): Promise { const context = context0 as CommonContext - const id = getRandomString() // TODO - use this properly + const id = getRandomString() // TODO - handle this method being optional diff --git a/packages/job-worker/src/ingest/selectShowStyleVariant.ts b/packages/job-worker/src/ingest/selectShowStyleVariant.ts index 0ea3be40a1..e367932a50 100644 --- a/packages/job-worker/src/ingest/selectShowStyleVariant.ts +++ b/packages/job-worker/src/ingest/selectShowStyleVariant.ts @@ -49,7 +49,7 @@ export async function selectShowStyleVariant( let showStyleId: ShowStyleBaseId | null = null try { showStyleId = protectString( - studioBlueprint.blueprint.getShowStyleId( + await studioBlueprint.blueprint.getShowStyleId( blueprintContext, showStyleBases.map(convertShowStyleBaseToBlueprints), ingestRundown diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 2bfbf31c82..ebfc5359ec 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -175,7 +175,7 @@ export async function produceRundownPlaylistInfoFromRundown( let playlistInfo: BlueprintResultRundownPlaylist | null = null try { if (studioBlueprint?.blueprint?.getRundownPlaylistInfo) { - playlistInfo = studioBlueprint.blueprint.getRundownPlaylistInfo( + playlistInfo = await studioBlueprint.blueprint.getRundownPlaylistInfo( new StudioUserContext( { name: 'produceRundownPlaylistInfoFromRundown', diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 6e84729385..3059b202ed 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -9,6 +9,7 @@ { "path": "corelib" }, { "path": "shared-lib" }, { "path": "openapi" }, - { "path": "live-status-gateway" } + { "path": "live-status-gateway" }, + { "path": "blueprints-proxy" } ] } From 00efe118a46ad8efe2dcf0878777e99b76052f73 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 13:14:57 +0100 Subject: [PATCH 09/11] wip: tidy --- .../blueprints-integration/src/api/studio.ts | 17 +++++--------- .../src/blueprints/ProxiedStudioBlueprint.ts | 23 +++++++++++++++---- .../src/blueprints/defaults/studio.ts | 6 ++--- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 9f1fcedf7a..54b91b58cc 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -28,33 +28,28 @@ export interface StudioBlueprintManifest BlueprintResultStudioBaseline | Promise + getBaseline: (context: IStudioBaselineContext) => Promise /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ getShowStyleId: ( context: IStudioUserContext, showStyles: ReadonlyDeep>, ingestRundown: ExtendedIngestRundown - ) => string | null | Promise + ) => Promise /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ getRundownPlaylistInfo?: ( context: IStudioUserContext, rundowns: IBlueprintRundownDB[], playlistExternalId: string - ) => BlueprintResultRundownPlaylist | null | Promise + ) => Promise /** * Validate the config passed to this blueprint * In this you should do various sanity checks of the config and return a list of messages to display to the user. * These messages do not stop `applyConfig` from being called. */ - validateConfig?: ( - context: ICommonContext, - config: TRawConfig - ) => Array | Promise> + validateConfig?: (context: ICommonContext, config: TRawConfig) => Promise> /** * Apply the config by generating the data to be saved into the db. @@ -64,14 +59,14 @@ export interface StudioBlueprintManifest BlueprintResultApplyStudioConfig | Promise + ) => Promise /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ preprocessConfig?: ( context: ICommonContext, config: TRawConfig, coreConfig: BlueprintConfigCoreConfig - ) => TProcessedConfig | Promise + ) => Promise } export interface BlueprintResultStudioBaseline { diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index a10421d511..5839a100a4 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -22,7 +22,7 @@ import { MigrationStepStudio, StudioBlueprintManifest, } from '@sofie-automation/blueprints-integration' -import { getRandomString } from '@sofie-automation/corelib/dist/lib' +import { getRandomString, stringifyError } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' import * as SocketIOClient from 'socket.io-client' import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' @@ -100,12 +100,27 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { return handler(functionId, ...args) } + #handleListen2( + name: T, + functionId: string, + ...args: Parameters + ): void { + const handlers = this.#callHandlers.get(functionId) + const handler = handlers?.[name] as any + if (!handler) throw new Error(`Method "${name}" is not supported`) + + try { + handler(functionId, ...args) + } catch (e) { + logger.error(stringifyError(e)) + } + } #generateListenerRouter(): EventHandlers { return { - common_notifyUserError: async (...args) => this.#handleListen('common_notifyUserError', ...args), - common_notifyUserWarning: async (...args) => this.#handleListen('common_notifyUserWarning', ...args), - common_notifyUserInfo: async (...args) => this.#handleListen('common_notifyUserInfo', ...args), + common_notifyUserError: (...args) => this.#handleListen2('common_notifyUserError', ...args), + common_notifyUserWarning: (...args) => this.#handleListen2('common_notifyUserWarning', ...args), + common_notifyUserInfo: (...args) => this.#handleListen2('common_notifyUserInfo', ...args), packageInfo_getPackageInfo: async (...args) => this.#handleListen('packageInfo_getPackageInfo', ...args), packageInfo_hackGetMediaObjectDuration: async (...args) => diff --git a/packages/job-worker/src/blueprints/defaults/studio.ts b/packages/job-worker/src/blueprints/defaults/studio.ts index ff8a899cf8..ddc3a2030e 100644 --- a/packages/job-worker/src/blueprints/defaults/studio.ts +++ b/packages/job-worker/src/blueprints/defaults/studio.ts @@ -36,18 +36,18 @@ export const DefaultStudioBlueprint: ReadonlyDeep = dee }, /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ - getBaseline(_context: IStudioBaselineContext): BlueprintResultStudioBaseline { + async getBaseline(_context: IStudioBaselineContext): Promise { return { timelineObjects: [], } }, /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ - getShowStyleId( + async getShowStyleId( _context: IStudioUserContext, _showStyles: ReadonlyDeep>, _ingestRundown: ExtendedIngestRundown - ): string | null { + ): Promise { return null }, }) From 70cc5478b908cabd6ad574f2b85a09dd0719c18a Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 10 Jul 2023 13:39:56 +0100 Subject: [PATCH 10/11] chore: renaming --- .../blueprints-proxy/src/context/common.ts | 22 ++-- .../src/context/studioContext.ts | 4 +- .../src/context/studioUserContext.ts | 4 +- packages/blueprints-proxy/src/helper.ts | 12 +- packages/blueprints-proxy/src/host.ts | 6 +- packages/blueprints-proxy/src/index.ts | 5 +- .../src/routers/studio/baseline.ts | 8 +- .../src/routers/studio/config.ts | 12 +- .../src/routers/studio/rundown.ts | 8 +- packages/blueprints-proxy/src/routers/util.ts | 28 ++--- .../src/blueprints/ProxiedStudioBlueprint.ts | 110 +++++++++--------- 11 files changed, 112 insertions(+), 107 deletions(-) diff --git a/packages/blueprints-proxy/src/context/common.ts b/packages/blueprints-proxy/src/context/common.ts index c3edfe8a3f..e2d2625f75 100644 --- a/packages/blueprints-proxy/src/context/common.ts +++ b/packages/blueprints-proxy/src/context/common.ts @@ -2,7 +2,7 @@ import { ICommonContext, NoteSeverity } from '@sofie-automation/blueprints-integ import * as crypto from 'crypto' import { ParamsIfReturnIsNever, ParamsIfReturnIsValid } from '../helper' import { callHelper, emitHelper, MySocket } from '../routers/util' -import { ServerToClientEvents } from '..' +import { BlueprintToSofieMethods } from '..' function getHash(str: string): string { const hash = crypto.createHash('sha1') @@ -13,29 +13,29 @@ export class CommonContext implements ICommonContext { private readonly _contextName: string readonly #socket: MySocket - readonly #functionId: string + readonly #invocationId: string private hashI = 0 private hashed: { [hash: string]: string } = {} - constructor(identifier: string, socket: MySocket, functionId: string) { + constructor(identifier: string, socket: MySocket, invocationId: string) { this._contextName = identifier this.#socket = socket - this.#functionId = functionId + this.#invocationId = invocationId } - protected emitMessage( + protected emitMessage( name: T, - data: ParamsIfReturnIsNever[0] + data: ParamsIfReturnIsNever[0] ): void { - return emitHelper(this.#socket, this.#functionId, name, data) + return emitHelper(this.#socket, this.#invocationId, name, data) } - protected emitCall( + protected async emitCall( name: T, - data: ParamsIfReturnIsValid[0] - ): Promise> { - return callHelper(this.#socket, this.#functionId, name, data) + data: ParamsIfReturnIsValid[0] + ): Promise> { + return callHelper(this.#socket, this.#invocationId, name, data) } getHashId(str: string, isNotUnique?: boolean): string { diff --git a/packages/blueprints-proxy/src/context/studioContext.ts b/packages/blueprints-proxy/src/context/studioContext.ts index f9ad1b343b..d2f04ef44f 100644 --- a/packages/blueprints-proxy/src/context/studioContext.ts +++ b/packages/blueprints-proxy/src/context/studioContext.ts @@ -10,8 +10,8 @@ export class StudioContext extends CommonContext implements IStudioContext { return this.#data.studioId } - constructor(functionName: string, socket: MySocket, functionId: string, msg: StudioContextArgs) { - super(`${functionName} ${msg.identifier}`, socket, functionId) + constructor(functionName: string, socket: MySocket, invocationId: string, msg: StudioContextArgs) { + super(`${functionName} ${msg.identifier}`, socket, invocationId) this.#data = msg } diff --git a/packages/blueprints-proxy/src/context/studioUserContext.ts b/packages/blueprints-proxy/src/context/studioUserContext.ts index 19c981a8f8..1f04276ed1 100644 --- a/packages/blueprints-proxy/src/context/studioUserContext.ts +++ b/packages/blueprints-proxy/src/context/studioUserContext.ts @@ -4,8 +4,8 @@ import { StudioContextArgs } from '..' import { StudioContext } from './studioContext' export class StudioUserContext extends StudioContext implements IStudioUserContext { - constructor(functionName: string, socket: MySocket, functionId: string, msg: StudioContextArgs) { - super(functionName, socket, functionId, msg) + constructor(functionName: string, socket: MySocket, invocationId: string, msg: StudioContextArgs) { + super(functionName, socket, invocationId, msg) } notifyUserError(message: string, params?: { [key: string]: any } | undefined): void { diff --git a/packages/blueprints-proxy/src/helper.ts b/packages/blueprints-proxy/src/helper.ts index f33dfe8dc2..c18735fda6 100644 --- a/packages/blueprints-proxy/src/helper.ts +++ b/packages/blueprints-proxy/src/helper.ts @@ -3,7 +3,7 @@ */ type HandlerReturnType any> = ReturnType extends never ? void : Promise> type HandlerFunction any> = ( - functionId: string, + invocationId: string, ...args: Parameters ) => HandlerReturnType @@ -29,7 +29,7 @@ export function listenToEvents(socket: any, handlers: EventHan // const logger = createChildLogger(`module/${connectionId}`); for (const [event, handler] of Object.entries(handlers)) { - socket.on(event as any, async (functionId: string, msg: any, cb: ResultCallback) => { + socket.on(event as any, async (invocationId: string, msg: any, cb: ResultCallback) => { const doError = (msg: string) => { console.warn(msg) if (cb && typeof cb === 'function') { @@ -40,10 +40,10 @@ export function listenToEvents(socket: any, handlers: EventHan } // TODO - find/reject callback? - console.log('running', event, functionId, JSON.stringify(msg)) + console.log('running', event, invocationId, JSON.stringify(msg)) - if (!functionId || typeof functionId !== 'string') { - doError(`Received malformed functionId "${event}"`) + if (!invocationId || typeof invocationId !== 'string') { + doError(`Received malformed invocationId "${event}"`) return // Ignore messages without correct structure } if (!msg || typeof msg !== 'object') { @@ -58,7 +58,7 @@ export function listenToEvents(socket: any, handlers: EventHan try { // Run it const handler2 = handler as HandlerFunction<(msg: any) => any> - const result = await handler2(functionId, msg) + const result = await handler2(invocationId, msg) if (cb) cb(null, result) } catch (e: any) { diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts index 633fdad18c..65e2f6f14c 100644 --- a/packages/blueprints-proxy/src/host.ts +++ b/packages/blueprints-proxy/src/host.ts @@ -1,6 +1,6 @@ import { ShowStyleBlueprintManifest, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' import { createServer } from 'http' -import { ClientToServerEvents, ServerToClientEvents } from './index' +import { SofieToBlueprintMethods, BlueprintToSofieMethods } from './index' import { Server } from 'socket.io' import { listenToEvents } from './helper' import { studio_applyConfig, studio_preprocessConfig, studio_validateConfig } from './routers/studio/config' @@ -26,7 +26,7 @@ export function runForBlueprints( // } const httpServer = createServer() - const io = new Server(httpServer, { + const io = new Server(httpServer, { cors: { // Allow everything origin: (o, cb) => cb(null, o), @@ -39,7 +39,7 @@ export function runForBlueprints( console.log(`connection from ${socket.id}`) // subscribe to socket events from host - listenToEvents(socket, { + listenToEvents(socket, { studio_getBaseline: async (...args) => studio_getBaseline(studioBlueprint, socket, ...args), studio_getShowStyleId: async (...args) => studio_getShowStyleId(studioBlueprint, socket, ...args), studio_getRundownPlaylistInfo: async (...args) => diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts index ef5a56d09f..e9726c5945 100644 --- a/packages/blueprints-proxy/src/index.ts +++ b/packages/blueprints-proxy/src/index.ts @@ -17,10 +17,11 @@ export type ResultCallback = (err: any, res: T) => void export type EmptyArgs = Record -export interface ServerToClientEvents { +export interface BlueprintToSofieMethods { common_notifyUserError: (msg: NotifyUserArgs) => never common_notifyUserWarning: (msg: NotifyUserArgs) => never common_notifyUserInfo: (msg: NotifyUserArgs) => never + packageInfo_getPackageInfo: (msg: PackageInfoGetPackageInfoArgs) => Readonly packageInfo_hackGetMediaObjectDuration: (msg: PackageInfoHackGetMediaObjectDurationArgs) => number | undefined studio_getStudioMappings: (msg: EmptyArgs) => Readonly @@ -37,7 +38,7 @@ export interface PackageInfoHackGetMediaObjectDurationArgs { mediaId: string } -export interface ClientToServerEvents { +export interface SofieToBlueprintMethods { studio_getBaseline: (msg: StudioGetBaselineArgs) => BlueprintResultStudioBaseline studio_getShowStyleId: (msg: StudioGetShowStyleIdArgs) => string | null studio_getRundownPlaylistInfo: (msg: StudioGetRundownPlaylistInfo) => BlueprintResultRundownPlaylist | null diff --git a/packages/blueprints-proxy/src/routers/studio/baseline.ts b/packages/blueprints-proxy/src/routers/studio/baseline.ts index 28394f92f8..740e7caa52 100644 --- a/packages/blueprints-proxy/src/routers/studio/baseline.ts +++ b/packages/blueprints-proxy/src/routers/studio/baseline.ts @@ -10,8 +10,8 @@ import { StudioContext } from '../../context/studioContext' import { MySocket } from '../util' class StudioBaselineContext extends StudioContext implements IStudioBaselineContext { - constructor(msg: StudioGetBaselineArgs, socket: MySocket, functionId: string) { - super('getBaseline', socket, functionId, msg) + constructor(msg: StudioGetBaselineArgs, socket: MySocket, invocationId: string) { + super('getBaseline', socket, invocationId, msg) } async getStudioMappings(): Promise> { @@ -28,10 +28,10 @@ class StudioBaselineContext extends StudioContext implements IStudioBaselineCont export async function studio_getBaseline( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioGetBaselineArgs ): Promise { - const context = new StudioBaselineContext(msg, socket, id) + const context = new StudioBaselineContext(msg, socket, invocationId) return studioBlueprint.getBaseline(context) } diff --git a/packages/blueprints-proxy/src/routers/studio/config.ts b/packages/blueprints-proxy/src/routers/studio/config.ts index aafa79bc5f..db76f179f8 100644 --- a/packages/blueprints-proxy/src/routers/studio/config.ts +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -10,12 +10,12 @@ import { MySocket } from '../util' export async function studio_validateConfig( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioValidateConfigArgs ): Promise { if (!studioBlueprint.validateConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`validateConfig ${msg.identifier}`, socket, id) + const context = new CommonContext(`validateConfig ${msg.identifier}`, socket, invocationId) return studioBlueprint.validateConfig(context, msg.config) } @@ -23,12 +23,12 @@ export async function studio_validateConfig( export async function studio_applyConfig( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioApplyConfigArgs ): Promise { if (!studioBlueprint.applyConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, id) + const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, invocationId) return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) } @@ -36,12 +36,12 @@ export async function studio_applyConfig( export async function studio_preprocessConfig( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioPreprocessConfigArgs ): Promise { if (!studioBlueprint.preprocessConfig) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, id) + const context = new CommonContext(`applyConfig ${msg.identifier}`, socket, invocationId) return studioBlueprint.preprocessConfig(context, msg.config, msg.coreConfig) } diff --git a/packages/blueprints-proxy/src/routers/studio/rundown.ts b/packages/blueprints-proxy/src/routers/studio/rundown.ts index 96ca2df15d..38077af706 100644 --- a/packages/blueprints-proxy/src/routers/studio/rundown.ts +++ b/packages/blueprints-proxy/src/routers/studio/rundown.ts @@ -6,10 +6,10 @@ import { MySocket } from '../util' export async function studio_getShowStyleId( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioGetShowStyleIdArgs ): Promise { - const context = new StudioUserContext('getShowStyleId', socket, id, msg) + const context = new StudioUserContext('getShowStyleId', socket, invocationId, msg) return studioBlueprint.getShowStyleId(context, msg.showStyles, msg.ingestRundown) } @@ -17,12 +17,12 @@ export async function studio_getShowStyleId( export async function studio_getRundownPlaylistInfo( studioBlueprint: StudioBlueprintManifest, socket: MySocket, - id: string, + invocationId: string, msg: StudioGetRundownPlaylistInfo ): Promise { if (!studioBlueprint.getRundownPlaylistInfo) throw new Error('Not supported') // TODO - this will have broken our ability to know if it is implemented or not.. - const context = new StudioUserContext('getRundownPlaylistInfo', socket, id, msg) + const context = new StudioUserContext('getRundownPlaylistInfo', socket, invocationId, msg) return studioBlueprint.getRundownPlaylistInfo(context, msg.rundowns, msg.playlistExternalId) } diff --git a/packages/blueprints-proxy/src/routers/util.ts b/packages/blueprints-proxy/src/routers/util.ts index 7f69eb8d0c..ababa13e3a 100644 --- a/packages/blueprints-proxy/src/routers/util.ts +++ b/packages/blueprints-proxy/src/routers/util.ts @@ -1,46 +1,46 @@ import { Socket } from 'socket.io' import { ParamsIfReturnIsNever, ParamsIfReturnIsValid, ResultCallback } from '../helper' -import { ClientToServerEvents, ServerToClientEvents } from '..' +import { SofieToBlueprintMethods, BlueprintToSofieMethods } from '..' -export type MySocket = Socket +export type MySocket = Socket -export async function callHelper( +export async function callHelper( socket: MySocket, - functionId: string, + invocationId: string, name: T, - data: ParamsIfReturnIsValid[0] -): Promise> { + data: ParamsIfReturnIsValid[0] +): Promise> { if (!socket.connected) throw new Error('Blueprints are unavailable') // TODO - ensure #callHandlers is cleaned up // TODO - timeouts? - return new Promise>((resolve, reject) => { + return new Promise>((resolve, reject) => { const handleDisconnect = () => { reject('Lost connection') } socket.once('disconnect', handleDisconnect) - const innerCb: ResultCallback> = ( + const innerCb: ResultCallback> = ( err: any, - res: ReturnType + res: ReturnType ): void => { socket.off('disconnect', handleDisconnect) if (err) reject(err) else resolve(res) } - socket.emit(name as any, functionId, data, innerCb) + socket.emit(name as any, invocationId, data, innerCb) }) } -export function emitHelper( +export function emitHelper( socket: MySocket, - functionId: string, + invocationId: string, name: T, - data: ParamsIfReturnIsNever[0] + data: ParamsIfReturnIsNever[0] ): void { if (!socket.connected) throw new Error('Blueprints are unavailable') - socket.emit(name as any, functionId, data) + socket.emit(name as any, invocationId, data) } diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index 5839a100a4..c7abd90204 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -25,12 +25,16 @@ import { import { getRandomString, stringifyError } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' import * as SocketIOClient from 'socket.io-client' -import type { ClientToServerEvents, ResultCallback, ServerToClientEvents } from '@sofie-automation/blueprints-proxy' +import type { + SofieToBlueprintMethods, + ResultCallback, + BlueprintToSofieMethods, +} from '@sofie-automation/blueprints-proxy' import { ReadonlyDeep } from 'type-fest' import { CommonContext, StudioBaselineContext, StudioUserContext } from './context' import { EventHandlers, listenToEvents, ParamsIfReturnIsValid } from '@sofie-automation/blueprints-proxy/dist/helper' -type MyClient = SocketIOClient.Socket +type MyClient = SocketIOClient.Socket export class ProxiedStudioBlueprint implements StudioBlueprintManifest { readonly blueprintType = BlueprintManifestType.STUDIO // s @@ -41,7 +45,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { autoConnect: true, // transports: ['websocket'], }) as MyClient - readonly #callHandlers = new Map>>() + readonly #callHandlers = new Map>>() /** Unique id of the blueprint. This is used by core to check if blueprints are the same blueprint, but differing versions */ blueprintId?: string @@ -86,37 +90,37 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - abort any in-progress? }) - listenToEvents(this.#client, this.#generateListenerRouter()) + listenToEvents(this.#client, this.#generateListenerRouter()) } - async #handleListen( + async #handleListen( name: T, - functionId: string, - ...args: Parameters + invocationId: string, + ...args: Parameters ): Promise { - const handlers = this.#callHandlers.get(functionId) + const handlers = this.#callHandlers.get(invocationId) const handler = handlers?.[name] as any if (!handler) throw new Error(`Method "${name}" is not supported`) - return handler(functionId, ...args) + return handler(invocationId, ...args) } - #handleListen2( + #handleListen2( name: T, - functionId: string, - ...args: Parameters + invocationId: string, + ...args: Parameters ): void { - const handlers = this.#callHandlers.get(functionId) + const handlers = this.#callHandlers.get(invocationId) const handler = handlers?.[name] as any if (!handler) throw new Error(`Method "${name}" is not supported`) try { - handler(functionId, ...args) + handler(invocationId, ...args) } catch (e) { logger.error(stringifyError(e)) } } - #generateListenerRouter(): EventHandlers { + #generateListenerRouter(): EventHandlers { return { common_notifyUserError: (...args) => this.#handleListen2('common_notifyUserError', ...args), common_notifyUserWarning: (...args) => this.#handleListen2('common_notifyUserWarning', ...args), @@ -129,67 +133,67 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { } } - async #runProxied( + async #runProxied( name: T, - functionId: string, - data: ParamsIfReturnIsValid[0] - ): Promise> { + invocationId: string, + data: ParamsIfReturnIsValid[0] + ): Promise> { if (!this.#client.connected) throw new Error('Blueprints are unavailable') // TODO - ensure #callHandlers is cleaned up // TODO - timeouts? - return new Promise>((resolve, reject) => { + return new Promise>((resolve, reject) => { const handleDisconnect = () => { reject('Client disconnected') } this.#client.once('disconnect', handleDisconnect) - const innerCb: ResultCallback> = ( + const innerCb: ResultCallback> = ( err: any, - res: ReturnType + res: ReturnType ): void => { this.#client.off('disconnect', handleDisconnect) - this.#callHandlers.delete(functionId) + this.#callHandlers.delete(invocationId) if (err) reject(err) else resolve(res) } - this.#client.emit(name as any, functionId, data, innerCb) + this.#client.emit(name as any, invocationId, data, innerCb) }) } - #listenToEventsForMethod(functionId: string, handlers: EventHandlers>): void { - if (this.#callHandlers.has(functionId)) { - logger.warn(`Methods already registered for call ${functionId}`) + #listenToEventsForMethod(invocationId: string, handlers: EventHandlers>): void { + if (this.#callHandlers.has(invocationId)) { + logger.warn(`Methods already registered for call ${invocationId}`) } - this.#callHandlers.set(functionId, handlers) + this.#callHandlers.set(invocationId, handlers) } - #packageInfoContextMethods(context: IPackageInfoContext): EventHandlers> { + #packageInfoContextMethods(context: IPackageInfoContext): EventHandlers> { return { - packageInfo_getPackageInfo: async (_id, data) => context.getPackageInfo(data.packageId), - packageInfo_hackGetMediaObjectDuration: async (_id, data) => + packageInfo_getPackageInfo: async (_invocationId, data) => context.getPackageInfo(data.packageId), + packageInfo_hackGetMediaObjectDuration: async (_invocationId, data) => context.hackGetMediaObjectDuration(data.mediaId), } } - #studioContextMethods(context: IStudioContext): EventHandlers> { + #studioContextMethods(context: IStudioContext): EventHandlers> { return { - studio_getStudioMappings: async (_id) => context.getStudioMappings(), + studio_getStudioMappings: async (_invocationId) => context.getStudioMappings(), } } - #userNotesContextMethods(context: IUserNotesContext): EventHandlers> { + #userNotesContextMethods(context: IUserNotesContext): EventHandlers> { return { - common_notifyUserError: (_id, msg) => context.notifyUserError(msg.message, msg.params), - common_notifyUserWarning: (_id, msg) => context.notifyUserWarning(msg.message, msg.params), - common_notifyUserInfo: (_id, msg) => context.notifyUserInfo(msg.message, msg.params), + common_notifyUserError: (_invocationId, msg) => context.notifyUserError(msg.message, msg.params), + common_notifyUserWarning: (_invocationId, msg) => context.notifyUserWarning(msg.message, msg.params), + common_notifyUserInfo: (_invocationId, msg) => context.notifyUserInfo(msg.message, msg.params), } } - #studioUserContextMethods(context: IStudioUserContext): EventHandlers> { + #studioUserContextMethods(context: IStudioUserContext): EventHandlers> { return { ...this.#studioContextMethods(context), ...this.#userNotesContextMethods(context), @@ -200,13 +204,13 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { async getBaseline(context0: IStudioBaselineContext): Promise { const context = context0 as StudioBaselineContext - const id = getRandomString() - this.#listenToEventsForMethod(id, { + const invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { ...this.#studioContextMethods(context), ...this.#packageInfoContextMethods(context), }) - return this.#runProxied('studio_getBaseline', id, { + return this.#runProxied('studio_getBaseline', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -221,12 +225,12 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ): Promise { const context = context0 as StudioUserContext - const id = getRandomString() - this.#listenToEventsForMethod(id, { + const invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { ...this.#studioUserContextMethods(context), }) - return this.#runProxied('studio_getShowStyleId', id, { + return this.#runProxied('studio_getShowStyleId', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -246,12 +250,12 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - handle this method being optional - const id = getRandomString() - this.#listenToEventsForMethod(id, { + const invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { ...this.#studioUserContextMethods(context), }) - return this.#runProxied('studio_getRundownPlaylistInfo', id, { + return this.#runProxied('studio_getRundownPlaylistInfo', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -264,11 +268,11 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { async validateConfig(context0: ICommonContext, config: IBlueprintConfig): Promise> { const context = context0 as CommonContext - const id = getRandomString() + const invocationId = getRandomString() // TODO - handle this method being optional - return this.#runProxied('studio_validateConfig', id, { + return this.#runProxied('studio_validateConfig', invocationId, { identifier: context._contextIdentifier, config, }) @@ -285,11 +289,11 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ): Promise { const context = context0 as CommonContext - const id = getRandomString() + const invocationId = getRandomString() // TODO - handle this method being optional - return this.#runProxied('studio_applyConfig', id, { + return this.#runProxied('studio_applyConfig', invocationId, { identifier: context._contextIdentifier, config, coreConfig, @@ -304,11 +308,11 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ): Promise { const context = context0 as CommonContext - const id = getRandomString() + const invocationId = getRandomString() // TODO - handle this method being optional - return this.#runProxied('studio_preprocessConfig', id, { + return this.#runProxied('studio_preprocessConfig', invocationId, { identifier: context._contextIdentifier, config, coreConfig, From 5925c9398f9053718b9ca735d723d291db5d0764 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 13 Jul 2023 15:47:54 +0100 Subject: [PATCH 11/11] wip: cleanup --- .../src/blueprints/ProxiedStudioBlueprint.ts | 39 ++++++---- packages/job-worker/src/blueprints/cache.ts | 1 + packages/job-worker/src/workers/caches.ts | 73 ++++++++++++++----- 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts index c7abd90204..ea54ac86fb 100644 --- a/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -93,7 +93,15 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { listenToEvents(this.#client, this.#generateListenerRouter()) } - async #handleListen( + dispose = (): void => { + // TODO - more? + this.#callHandlers.clear() + + this.#client.disconnect() + this.#client.removeAllListeners() + } + + async #handleFunctionCall( name: T, invocationId: string, ...args: Parameters @@ -104,7 +112,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { return handler(invocationId, ...args) } - #handleListen2( + #handleVoidFunctionCall( name: T, invocationId: string, ...args: Parameters @@ -122,18 +130,19 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { #generateListenerRouter(): EventHandlers { return { - common_notifyUserError: (...args) => this.#handleListen2('common_notifyUserError', ...args), - common_notifyUserWarning: (...args) => this.#handleListen2('common_notifyUserWarning', ...args), - common_notifyUserInfo: (...args) => this.#handleListen2('common_notifyUserInfo', ...args), + common_notifyUserError: (...args) => this.#handleVoidFunctionCall('common_notifyUserError', ...args), + common_notifyUserWarning: (...args) => this.#handleVoidFunctionCall('common_notifyUserWarning', ...args), + common_notifyUserInfo: (...args) => this.#handleVoidFunctionCall('common_notifyUserInfo', ...args), - packageInfo_getPackageInfo: async (...args) => this.#handleListen('packageInfo_getPackageInfo', ...args), + packageInfo_getPackageInfo: async (...args) => + this.#handleFunctionCall('packageInfo_getPackageInfo', ...args), packageInfo_hackGetMediaObjectDuration: async (...args) => - this.#handleListen('packageInfo_hackGetMediaObjectDuration', ...args), - studio_getStudioMappings: async (...args) => this.#handleListen('studio_getStudioMappings', ...args), + this.#handleFunctionCall('packageInfo_hackGetMediaObjectDuration', ...args), + studio_getStudioMappings: async (...args) => this.#handleFunctionCall('studio_getStudioMappings', ...args), } } - async #runProxied( + async #callBlueprintMethod( name: T, invocationId: string, data: ParamsIfReturnIsValid[0] @@ -210,7 +219,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ...this.#packageInfoContextMethods(context), }) - return this.#runProxied('studio_getBaseline', invocationId, { + return this.#callBlueprintMethod('studio_getBaseline', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -230,7 +239,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ...this.#studioUserContextMethods(context), }) - return this.#runProxied('studio_getShowStyleId', invocationId, { + return this.#callBlueprintMethod('studio_getShowStyleId', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -255,7 +264,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { ...this.#studioUserContextMethods(context), }) - return this.#runProxied('studio_getRundownPlaylistInfo', invocationId, { + return this.#callBlueprintMethod('studio_getRundownPlaylistInfo', invocationId, { identifier: context._contextIdentifier, studioId: context.studioId, studioConfig: context.getStudioConfig() as IBlueprintConfig, @@ -272,7 +281,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - handle this method being optional - return this.#runProxied('studio_validateConfig', invocationId, { + return this.#callBlueprintMethod('studio_validateConfig', invocationId, { identifier: context._contextIdentifier, config, }) @@ -293,7 +302,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - handle this method being optional - return this.#runProxied('studio_applyConfig', invocationId, { + return this.#callBlueprintMethod('studio_applyConfig', invocationId, { identifier: context._contextIdentifier, config, coreConfig, @@ -312,7 +321,7 @@ export class ProxiedStudioBlueprint implements StudioBlueprintManifest { // TODO - handle this method being optional - return this.#runProxied('studio_preprocessConfig', invocationId, { + return this.#callBlueprintMethod('studio_preprocessConfig', invocationId, { identifier: context._contextIdentifier, config, coreConfig, diff --git a/packages/job-worker/src/blueprints/cache.ts b/packages/job-worker/src/blueprints/cache.ts index 6dafb174d0..9314e85e56 100644 --- a/packages/job-worker/src/blueprints/cache.ts +++ b/packages/job-worker/src/blueprints/cache.ts @@ -18,6 +18,7 @@ export interface WrappedStudioBlueprint { blueprintDoc: Blueprint | undefined blueprintId: BlueprintId blueprint: StudioBlueprintManifest + dispose: (() => void) | undefined } export interface WrappedShowStyleBlueprint { blueprintId: BlueprintId diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index e7dc2a6c0f..41da4ac350 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -1,5 +1,5 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { WrappedShowStyleBlueprint, WrappedStudioBlueprint } from '../blueprints/cache' +import { parseBlueprintDocument, WrappedShowStyleBlueprint, WrappedStudioBlueprint } from '../blueprints/cache' import { ReadonlyDeep } from 'type-fest' import { IDirectCollections } from '../db' import { @@ -136,7 +136,7 @@ export async function loadWorkerDataCache( // Load some 'static' data from the db const studio = deepFreeze(await collections.Studios.findOne(studioId)) if (!studio) throw new Error('Missing studio') - const studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, studio) + const studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, studio, null) return { studio, @@ -162,7 +162,7 @@ export async function invalidateWorkerDataCache( if (!newStudio) throw new Error(`Studio is missing during cache invalidation!`) cache.studio = deepFreeze(newStudio) - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio, cache.studioBlueprint) cache.studioBlueprintConfig = undefined cache.showStyleBases.clear() @@ -195,7 +195,7 @@ export async function invalidateWorkerDataCache( // Reload studioBlueprint if (updateStudioBlueprint) { logger.debug('WorkerDataCache: Reloading studioBlueprint') - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio, cache.studioBlueprint) cache.studioBlueprintConfig = undefined } @@ -291,32 +291,69 @@ export async function invalidateWorkerDataCache( async function loadStudioBlueprintOrPlaceholder( collections: IDirectCollections, - studio: ReadonlyDeep + studio: ReadonlyDeep, + existingBlueprint: ReadonlyDeep | null ): Promise> { if (!studio.blueprintId) { return Object.freeze({ blueprintDoc: undefined, blueprintId: protectString('__placeholder__'), blueprint: DefaultStudioBlueprint, + dispose: undefined, }) } const blueprintDoc = await collections.Blueprints.findOne(studio.blueprintId) - const blueprintManifest = new ProxiedStudioBlueprint() - // const blueprintManifest = await parseBlueprintDocument(blueprintDoc) - // if (!blueprintManifest) { - // throw new Error(`Blueprint "${studio.blueprintId}" not found! (referenced by Studio "${studio._id}")`) - // } - - if (blueprintManifest.blueprintType !== BlueprintManifestType.STUDIO) { + if (!blueprintDoc || blueprintDoc.blueprintType !== BlueprintManifestType.STUDIO) { throw new Error( - `Blueprint "${studio.blueprintId}" is not valid for a Studio "${studio._id}" (${blueprintManifest.blueprintType})!` + `Blueprint "${studio.blueprintId}" is not valid for a Studio "${studio._id}" (${blueprintDoc?.blueprintType})!` ) } - return Object.freeze({ - blueprintDoc: blueprintDoc, - blueprintId: studio.blueprintId, - blueprint: blueprintManifest, - }) + const useProxy = true // nocommit this needs to be behind a config flag on the blueprint doc. + if (useProxy) { + const existingBlueprintProxy = + existingBlueprint?.blueprint instanceof ProxiedStudioBlueprint ? existingBlueprint.blueprint : null + if (!existingBlueprintProxy || existingBlueprint?.blueprintId !== studio.blueprintId) { + if (existingBlueprint?.dispose) existingBlueprint.dispose() + + // Create a new proxy + + const newBlueprintProxy = new ProxiedStudioBlueprint() + return Object.freeze({ + blueprintDoc: blueprintDoc, + blueprintId: studio.blueprintId, + blueprint: newBlueprintProxy, + dispose: newBlueprintProxy.dispose, + }) + } else { + // Reuse the proxy + return Object.freeze({ + blueprintDoc: blueprintDoc, + blueprintId: studio.blueprintId, + blueprint: existingBlueprintProxy, + dispose: existingBlueprintProxy.dispose, + }) + } + } else { + const blueprintManifest = await parseBlueprintDocument(blueprintDoc) + if (!blueprintManifest) { + throw new Error(`Blueprint "${studio.blueprintId}" not found! (referenced by Studio "${studio._id}")`) + } + + if (blueprintManifest.blueprintType !== BlueprintManifestType.STUDIO) { + throw new Error( + `Blueprint "${studio.blueprintId}" is not valid for a Studio "${studio._id}" (${blueprintManifest.blueprintType})!` + ) + } + + if (existingBlueprint?.dispose) existingBlueprint.dispose() + + return Object.freeze({ + blueprintDoc: blueprintDoc, + blueprintId: studio.blueprintId, + blueprint: blueprintManifest, + dispose: undefined, + }) + } }