diff --git a/meteor/lib/api/methods.ts b/meteor/lib/api/methods.ts index f6aa1ced79..b3845ee192 100644 --- a/meteor/lib/api/methods.ts +++ b/meteor/lib/api/methods.ts @@ -65,6 +65,7 @@ export const MeteorCall: IMeteorCall = { organization: makeMethods(OrganizationAPIMethods), system: makeMethods(SystemAPIMethods), } + 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/meteor/yarn.lock b/meteor/yarn.lock index b952415c1f..b30aa8d1f8 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,17 @@ __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 + socket.io: ^4.7.1 + 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 +1680,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 +1690,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 @@ -1811,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" @@ -1823,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" @@ -2031,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" @@ -2633,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: @@ -3366,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" @@ -4395,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" @@ -4426,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" @@ -4589,7 +4656,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 +5175,44 @@ __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 + +"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" @@ -9585,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 @@ -11756,6 +11861,52 @@ __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" + 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 + +"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" @@ -13086,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 @@ -13453,6 +13604,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 +13668,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-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 5593f7fffb..54b91b58cc 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -28,28 +28,28 @@ export interface StudioBlueprintManifest BlueprintResultStudioBaseline + 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 /** 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 /** * 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 + validateConfig?: (context: ICommonContext, config: TRawConfig) => Promise> /** * Apply the config by generating the data to be saved into the db. @@ -59,14 +59,14 @@ export interface StudioBlueprintManifest 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?: ( context: ICommonContext, config: TRawConfig, coreConfig: BlueprintConfigCoreConfig - ) => TProcessedConfig + ) => Promise } export interface BlueprintResultStudioBaseline { 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/.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..8a0fb88b0b --- /dev/null +++ b/packages/blueprints-proxy/package.json @@ -0,0 +1,56 @@ +{ + "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", + "socket.io": "^4.7.1", + "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/context/common.ts b/packages/blueprints-proxy/src/context/common.ts new file mode 100644 index 0000000000..e2d2625f75 --- /dev/null +++ b/packages/blueprints-proxy/src/context/common.ts @@ -0,0 +1,80 @@ +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 { BlueprintToSofieMethods } from '..' + +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 + + readonly #socket: MySocket + readonly #invocationId: string + + private hashI = 0 + private hashed: { [hash: string]: string } = {} + + constructor(identifier: string, socket: MySocket, invocationId: string) { + this._contextName = identifier + + this.#socket = socket + this.#invocationId = invocationId + } + + protected emitMessage( + name: T, + data: ParamsIfReturnIsNever[0] + ): void { + return emitHelper(this.#socket, this.#invocationId, name, data) + } + protected async emitCall( + name: T, + data: ParamsIfReturnIsValid[0] + ): Promise> { + return callHelper(this.#socket, this.#invocationId, name, data) + } + + 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/context/studioContext.ts b/packages/blueprints-proxy/src/context/studioContext.ts new file mode 100644 index 0000000000..d2f04ef44f --- /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, invocationId: string, msg: StudioContextArgs) { + super(`${functionName} ${msg.identifier}`, socket, invocationId) + + 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..1f04276ed1 --- /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, invocationId: string, msg: StudioContextArgs) { + super(functionName, socket, invocationId, 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 new file mode 100644 index 0000000000..c18735fda6 --- /dev/null +++ b/packages/blueprints-proxy/src/helper.ts @@ -0,0 +1,71 @@ +/** + * Signature for the handler functions + */ +type HandlerReturnType any> = ReturnType extends never ? void : Promise> +type HandlerFunction any> = ( + invocationId: string, + ...args: Parameters +) => HandlerReturnType + +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 + +export type ParamsIfReturnIsValid any> = ReturnType extends never + ? 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 (invocationId: 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, invocationId, JSON.stringify(msg)) + + if (!invocationId || typeof invocationId !== 'string') { + doError(`Received malformed invocationId "${event}"`) + return // Ignore messages without correct structure + } + if (!msg || typeof msg !== 'object') { + doError(`Received malformed message object "${event}"`) + return // Ignore messages without correct structure + } + if (cb && typeof cb !== 'function') { + doError(`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(invocationId, 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) + else socket.close() + } + }) + } +} diff --git a/packages/blueprints-proxy/src/host.ts b/packages/blueprints-proxy/src/host.ts new file mode 100644 index 0000000000..65e2f6f14c --- /dev/null +++ b/packages/blueprints-proxy/src/host.ts @@ -0,0 +1,56 @@ +import { ShowStyleBlueprintManifest, StudioBlueprintManifest } from '@sofie-automation/blueprints-integration' +import { createServer } from 'http' +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' +import { studio_getBaseline } from './routers/studio/baseline' +import { studio_getRundownPlaylistInfo, studio_getShowStyleId } from './routers/studio/rundown' + +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, + }, + }) + + io.on('connection', (socket) => { + // ... + console.log(`connection from ${socket.id}`) + + // subscribe to socket events from host + listenToEvents(socket, { + 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), + }) + }) + + httpServer.listen(2345, () => { + console.log('Started server') + }) +} diff --git a/packages/blueprints-proxy/src/index.ts b/packages/blueprints-proxy/src/index.ts new file mode 100644 index 0000000000..e9726c5945 --- /dev/null +++ b/packages/blueprints-proxy/src/index.ts @@ -0,0 +1,77 @@ +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 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 +} + +export interface NotifyUserArgs { + message: string + params: { [key: string]: any } | undefined +} +export interface PackageInfoGetPackageInfoArgs { + packageId: string +} +export interface PackageInfoHackGetMediaObjectDurationArgs { + mediaId: string +} + +export interface SofieToBlueprintMethods { + 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 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 +} +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/baseline.ts b/packages/blueprints-proxy/src/routers/studio/baseline.ts new file mode 100644 index 0000000000..740e7caa52 --- /dev/null +++ b/packages/blueprints-proxy/src/routers/studio/baseline.ts @@ -0,0 +1,37 @@ +import { + BlueprintMappings, + BlueprintResultStudioBaseline, + IStudioBaselineContext, + PackageInfo, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { StudioGetBaselineArgs } from '@sofie-automation/shared-lib' +import { StudioContext } from '../../context/studioContext' +import { MySocket } from '../util' + +class StudioBaselineContext extends StudioContext implements IStudioBaselineContext { + constructor(msg: StudioGetBaselineArgs, socket: MySocket, invocationId: string) { + super('getBaseline', socket, invocationId, msg) + } + + async getStudioMappings(): Promise> { + return this.emitCall('studio_getStudioMappings', {}) + } + async getPackageInfo(packageId: string): Promise { + return this.emitCall('packageInfo_getPackageInfo', { packageId }) + } + async hackGetMediaObjectDuration(mediaId: string): Promise { + return this.emitCall('packageInfo_hackGetMediaObjectDuration', { mediaId }) + } +} + +export async function studio_getBaseline( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + invocationId: string, + msg: StudioGetBaselineArgs +): Promise { + 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 new file mode 100644 index 0000000000..db76f179f8 --- /dev/null +++ b/packages/blueprints-proxy/src/routers/studio/config.ts @@ -0,0 +1,47 @@ +import type { + BlueprintResultApplyStudioConfig, + IConfigMessage, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { CommonContext } from '../../context/common' +import type { StudioApplyConfigArgs, StudioPreprocessConfigArgs, StudioValidateConfigArgs } from '../../index' +import { MySocket } from '../util' + +export async function studio_validateConfig( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + 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, invocationId) + + return studioBlueprint.validateConfig(context, msg.config) +} + +export async function studio_applyConfig( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + 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, invocationId) + + return studioBlueprint.applyConfig(context, msg.config, msg.coreConfig) +} + +export async function studio_preprocessConfig( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + 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, 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 new file mode 100644 index 0000000000..38077af706 --- /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, + invocationId: string, + msg: StudioGetShowStyleIdArgs +): Promise { + const context = new StudioUserContext('getShowStyleId', socket, invocationId, msg) + + return studioBlueprint.getShowStyleId(context, msg.showStyles, msg.ingestRundown) +} + +export async function studio_getRundownPlaylistInfo( + studioBlueprint: StudioBlueprintManifest, + socket: MySocket, + 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, 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 new file mode 100644 index 0000000000..ababa13e3a --- /dev/null +++ b/packages/blueprints-proxy/src/routers/util.ts @@ -0,0 +1,46 @@ +import { Socket } from 'socket.io' +import { ParamsIfReturnIsNever, ParamsIfReturnIsValid, ResultCallback } from '../helper' +import { SofieToBlueprintMethods, BlueprintToSofieMethods } from '..' + +export type MySocket = Socket + +export async function callHelper( + socket: MySocket, + invocationId: 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, invocationId, data, innerCb) + }) +} + +export function emitHelper( + socket: MySocket, + invocationId: string, + name: T, + data: ParamsIfReturnIsNever[0] +): void { + if (!socket.connected) throw new Error('Blueprints are unavailable') + + socket.emit(name as any, invocationId, data) +} 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/__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 new file mode 100644 index 0000000000..ea54ac86fb --- /dev/null +++ b/packages/job-worker/src/blueprints/ProxiedStudioBlueprint.ts @@ -0,0 +1,330 @@ +import { + BlueprintConfigCoreConfig, + BlueprintManifestType, + BlueprintResultApplyStudioConfig, + BlueprintResultRundownPlaylist, + BlueprintResultStudioBaseline, + ExtendedIngestRundown, + IBlueprintConfig, + IBlueprintRundownDB, + IBlueprintShowStyleBase, + ICommonContext, + IConfigMessage, + IPackageInfoContext, + IShowStyleConfigPreset, + IStudioBaselineContext, + IStudioContext, + IStudioUserContext, + IUserNotesContext, + JSONBlob, + JSONBlobStringify, + JSONSchema, + MigrationStepStudio, + StudioBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { getRandomString, stringifyError } from '@sofie-automation/corelib/dist/lib' +import { logger } from '../logging' +import * as SocketIOClient from 'socket.io-client' +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 + +export class ProxiedStudioBlueprint implements StudioBlueprintManifest { + readonly blueprintType = BlueprintManifestType.STUDIO // s + + readonly #client: MyClient = SocketIOClient.io('http://localhost:2345', { + reconnection: true, + // timeout: 5000, + 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 + /** 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, err?.message, err?.toString()) + // TODO - load constants from blueprints + }) + + this.#client.on('disconnect', () => { + console.log('disconnect') + + this.#callHandlers.clear() + + // TODO - abort any in-progress? + }) + + listenToEvents(this.#client, this.#generateListenerRouter()) + } + + dispose = (): void => { + // TODO - more? + this.#callHandlers.clear() + + this.#client.disconnect() + this.#client.removeAllListeners() + } + + async #handleFunctionCall( + name: T, + invocationId: string, + ...args: Parameters + ): Promise { + const handlers = this.#callHandlers.get(invocationId) + const handler = handlers?.[name] as any + if (!handler) throw new Error(`Method "${name}" is not supported`) + + return handler(invocationId, ...args) + } + #handleVoidFunctionCall( + name: T, + invocationId: string, + ...args: Parameters + ): void { + const handlers = this.#callHandlers.get(invocationId) + const handler = handlers?.[name] as any + if (!handler) throw new Error(`Method "${name}" is not supported`) + + try { + handler(invocationId, ...args) + } catch (e) { + logger.error(stringifyError(e)) + } + } + + #generateListenerRouter(): EventHandlers { + return { + 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.#handleFunctionCall('packageInfo_getPackageInfo', ...args), + packageInfo_hackGetMediaObjectDuration: async (...args) => + this.#handleFunctionCall('packageInfo_hackGetMediaObjectDuration', ...args), + studio_getStudioMappings: async (...args) => this.#handleFunctionCall('studio_getStudioMappings', ...args), + } + } + + async #callBlueprintMethod( + name: T, + 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) => { + const handleDisconnect = () => { + reject('Client disconnected') + } + this.#client.once('disconnect', handleDisconnect) + + const innerCb: ResultCallback> = ( + err: any, + res: ReturnType + ): void => { + this.#client.off('disconnect', handleDisconnect) + this.#callHandlers.delete(invocationId) + + if (err) reject(err) + else resolve(res) + } + this.#client.emit(name as any, invocationId, data, innerCb) + }) + } + + #listenToEventsForMethod(invocationId: string, handlers: EventHandlers>): void { + if (this.#callHandlers.has(invocationId)) { + logger.warn(`Methods already registered for call ${invocationId}`) + } + + this.#callHandlers.set(invocationId, handlers) + } + + #packageInfoContextMethods(context: IPackageInfoContext): EventHandlers> { + return { + packageInfo_getPackageInfo: async (_invocationId, data) => context.getPackageInfo(data.packageId), + packageInfo_hackGetMediaObjectDuration: async (_invocationId, data) => + context.hackGetMediaObjectDuration(data.mediaId), + } + } + + #studioContextMethods(context: IStudioContext): EventHandlers> { + return { + studio_getStudioMappings: async (_invocationId) => context.getStudioMappings(), + } + } + + #userNotesContextMethods(context: IUserNotesContext): EventHandlers> { + return { + 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> { + 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 invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { + ...this.#studioContextMethods(context), + ...this.#packageInfoContextMethods(context), + }) + + return this.#callBlueprintMethod('studio_getBaseline', invocationId, { + 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 */ + async getShowStyleId( + context0: IStudioUserContext, + showStyles: ReadonlyDeep>, + ingestRundown: ExtendedIngestRundown + ): Promise { + const context = context0 as StudioUserContext + + const invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { + ...this.#studioUserContextMethods(context), + }) + + return this.#callBlueprintMethod('studio_getShowStyleId', invocationId, { + 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 */ + async getRundownPlaylistInfo( + context0: IStudioUserContext, + rundowns: IBlueprintRundownDB[], + playlistExternalId: string + ): Promise { + const context = context0 as StudioUserContext + + // TODO - handle this method being optional + + const invocationId = getRandomString() + this.#listenToEventsForMethod(invocationId, { + ...this.#studioUserContextMethods(context), + }) + + return this.#callBlueprintMethod('studio_getRundownPlaylistInfo', invocationId, { + 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 invocationId = getRandomString() + + // TODO - handle this method being optional + + return this.#callBlueprintMethod('studio_validateConfig', invocationId, { + identifier: 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 + */ + async applyConfig( + context0: ICommonContext, + config: IBlueprintConfig, + coreConfig: BlueprintConfigCoreConfig + ): Promise { + const context = context0 as CommonContext + + const invocationId = getRandomString() + + // TODO - handle this method being optional + + return this.#callBlueprintMethod('studio_applyConfig', invocationId, { + 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 */ + async preprocessConfig( + context0: ICommonContext, + config: IBlueprintConfig, + coreConfig: BlueprintConfigCoreConfig + ): Promise { + const context = context0 as CommonContext + + const invocationId = getRandomString() + + // TODO - handle this method being optional + + return this.#callBlueprintMethod('studio_preprocessConfig', invocationId, { + 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/cache.ts b/packages/job-worker/src/blueprints/cache.ts index 9762497c18..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 @@ -50,6 +51,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/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/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/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 9af2a4d83a..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 @@ -67,7 +69,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..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) ) @@ -71,7 +73,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..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,13 +12,14 @@ 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 } - 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/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/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 }, }) 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/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/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 82270e41bc..da8eb3d3f6 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -108,10 +108,11 @@ 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, + 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/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 2d73192d26..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) @@ -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/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 4567c0fcdc..ebfc5359ec 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -164,18 +164,18 @@ 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) { - playlistInfo = studioBlueprint.blueprint.getRundownPlaylistInfo( + playlistInfo = await studioBlueprint.blueprint.getRundownPlaylistInfo( new StudioUserContext( { name: 'produceRundownPlaylistInfoFromRundown', @@ -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/caches.ts b/packages/job-worker/src/workers/caches.ts index f816a8f407..41da4ac350 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -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 @@ -135,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, @@ -161,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() @@ -194,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 } @@ -290,31 +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 = 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, + }) + } } 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 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/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" } ] } diff --git a/packages/yarn.lock b/packages/yarn.lock index 18a712d731..418d80570b 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,17 @@ __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 + socket.io: ^4.7.1 + 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 +4921,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 +4932,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 @@ -5689,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" @@ -5965,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" @@ -6159,6 +6203,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" @@ -7657,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" @@ -9165,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" @@ -9243,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" @@ -9650,7 +9727,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 +10473,44 @@ 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 + +"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" @@ -17205,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 @@ -20900,6 +21015,52 @@ 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" + 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 + +"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" @@ -23303,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 @@ -24074,6 +24235,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 +24306,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"